[{"data":1,"prerenderedAt":1853},["ShallowReactive",2],{"\u002Fblog\u002Fusing-phpstan-to-enforce-declare-strict-types-1":3},{"id":4,"title":5,"body":6,"description":58,"extension":1843,"meta":1844,"navigation":80,"path":1845,"publishedAt":1846,"seo":1847,"stem":1848,"summary":1849,"tags":1850,"__hash__":1852},"blog\u002Fblog\u002F20230325.md","Using PHPStan to enforce declare(strict_types=1)",{"type":7,"value":8,"toc":1836},"minimark",[9,14,27,37,41,48,170,175,297,302,308,312,315,475,497,500,642,663,666,782,792,888,899,994,997,1339,1342,1346,1349,1356,1373,1376,1739,1742,1746,1753,1832],[10,11,13],"h2",{"id":12},"introduction","Introduction",[15,16,17,18,22,23,26],"p",{},"I think that everyone these days agrees that typecasting is iffy (to say the least) and that relying on typecasting can be a major source of bugs, which sometimes are very hard to debug. In PHP a solution to this problem is placing a ",[19,20,21],"code",{},"declare(strict_types=1)"," statement right after the classic ",[19,24,25],{},"\u003C?php",".",[15,28,29,30,26],{},"Recently while navigating older codebases I noticed that some files did not include such statement. The first thing I thought was looking up a rule in PHPStan to enforce this on the whole codebase, but after some Googling I couldn't find a ready to use solution. So after browsing PHPStan documentation for a few minutes I discovered Custom Rules, which as the name suggests are rules you write yourself by implementing an interface provided by PHPStan. You can read more about it on this ",[31,32,36],"a",{"href":33,"rel":34},"https:\u002F\u002Fphpstan.org\u002Fdeveloping-extensions\u002Frules",[35],"nofollow","link",[10,38,40],{"id":39},"where-were-headed","Where we're headed:",[42,43,44],"ul",{},[45,46,47],"li",{},"Our example class:",[49,50,59],"pre",{"className":51,"code":52,"filename":53,"highlights":54,"language":56,"meta":57,"style":58},"language-php shiki shiki-themes github-light github-dark github-light","\u003C?php\n\nnamespace App;\n\nclass Mouth\n{\n    public function shout(string $phrase): void\n    {\n        echo $phrase;\n    }\n}\n","file.php",[55],2,"php","meta-info=val","",[19,60,61,74,82,96,101,110,116,143,149,158,164],{"__ignoreMap":58},[62,63,66,70],"span",{"class":64,"line":65},"line",1,[62,67,69],{"class":68},"s77lx","\u003C?",[62,71,73],{"class":72},"s-VKZ","php\n",[62,75,78],{"class":76,"line":55},[64,77],"highlight",[62,79,81],{"emptyLinePlaceholder":80},true,"\n",[62,83,85,88,92],{"class":64,"line":84},3,[62,86,87],{"class":68},"namespace",[62,89,91],{"class":90},"sXzdU"," App",[62,93,95],{"class":94},"sov1W",";\n",[62,97,99],{"class":64,"line":98},4,[62,100,81],{"emptyLinePlaceholder":80},[62,102,104,107],{"class":64,"line":103},5,[62,105,106],{"class":68},"class",[62,108,109],{"class":90}," Mouth\n",[62,111,113],{"class":64,"line":112},6,[62,114,115],{"class":94},"{\n",[62,117,119,122,125,128,131,134,137,140],{"class":64,"line":118},7,[62,120,121],{"class":68},"    public",[62,123,124],{"class":68}," function",[62,126,127],{"class":90}," shout",[62,129,130],{"class":94},"(",[62,132,133],{"class":68},"string",[62,135,136],{"class":94}," $phrase)",[62,138,139],{"class":68},":",[62,141,142],{"class":68}," void\n",[62,144,146],{"class":64,"line":145},8,[62,147,148],{"class":94},"    {\n",[62,150,152,155],{"class":64,"line":151},9,[62,153,154],{"class":72},"        echo",[62,156,157],{"class":94}," $phrase;\n",[62,159,161],{"class":64,"line":160},10,[62,162,163],{"class":94},"    }\n",[62,165,167],{"class":64,"line":166},11,[62,168,169],{"class":94},"}\n",[42,171,172],{},[45,173,174],{},"The output from running the PHPStan binary:",[49,176,180],{"className":177,"code":178,"language":179,"meta":58,"style":58},"language-bash shiki shiki-themes github-light github-dark github-light","php .\u002Fvendor\u002Fbin\u002Fphpstan\nNote: Using configuration file \u002Fhome\u002Fdavi\u002Fdev\u002Fstrict-types\u002Fphpstan.neon.\n 4\u002F4 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n ------ ---------------------------------------------------\n  Line   src\u002FMouth.php\n ------ ---------------------------------------------------\n  3      File is missing declare(strict_types=1) statement\n ------ ---------------------------------------------------\n\n [ERROR] Found 1 error\n","bash",[19,181,182,190,207,215,219,227,235,241,273,279,283],{"__ignoreMap":58},[62,183,184,186],{"class":64,"line":65},[62,185,56],{"class":90},[62,187,189],{"class":188},"sSPdU"," .\u002Fvendor\u002Fbin\u002Fphpstan\n",[62,191,192,195,198,201,204],{"class":64,"line":55},[62,193,194],{"class":90},"Note:",[62,196,197],{"class":188}," Using",[62,199,200],{"class":188}," configuration",[62,202,203],{"class":188}," file",[62,205,206],{"class":188}," \u002Fhome\u002Fdavi\u002Fdev\u002Fstrict-types\u002Fphpstan.neon.\n",[62,208,209,212],{"class":64,"line":84},[62,210,211],{"class":90}," 4\u002F4",[62,213,214],{"class":94}," [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n",[62,216,217],{"class":64,"line":98},[62,218,81],{"emptyLinePlaceholder":80},[62,220,221,224],{"class":64,"line":103},[62,222,223],{"class":90}," ------",[62,225,226],{"class":72}," ---------------------------------------------------\n",[62,228,229,232],{"class":64,"line":112},[62,230,231],{"class":90},"  Line",[62,233,234],{"class":188},"   src\u002FMouth.php\n",[62,236,237,239],{"class":64,"line":118},[62,238,223],{"class":90},[62,240,226],{"class":72},[62,242,243,246,249,252,255,258,261,264,267,270],{"class":64,"line":145},[62,244,245],{"class":90},"  3",[62,247,248],{"class":188},"      File",[62,250,251],{"class":188}," is",[62,253,254],{"class":188}," missing",[62,256,257],{"class":188}," declare",[62,259,260],{"class":94},"(strict_types",[62,262,263],{"class":68},"=",[62,265,266],{"class":188},"1",[62,268,269],{"class":94},") ",[62,271,272],{"class":188},"statement\n",[62,274,275,277],{"class":64,"line":151},[62,276,223],{"class":90},[62,278,226],{"class":72},[62,280,281],{"class":64,"line":160},[62,282,81],{"emptyLinePlaceholder":80},[62,284,285,288,291,294],{"class":64,"line":166},[62,286,287],{"class":94}," [ERROR] ",[62,289,290],{"class":90},"Found",[62,292,293],{"class":72}," 1",[62,295,296],{"class":188}," error\n",[42,298,299],{},[45,300,301],{},"An IDE pinpointing the error:",[15,303,304],{},[305,306],"img",{"alt":58,"src":307},"\u002Fimg\u002F20230121-1.png",[10,309,311],{"id":310},"implementing-our-custom-rule","Implementing our custom rule:",[15,313,314],{},"This is the interface defined by PHPStan which we need to implement:",[49,316,318],{"className":51,"code":317,"language":56,"meta":58,"style":58},"\u002F**\n * @api\n * @phpstan-template TNodeType of Node\n *\u002F\ninterface Rule\n{\n    \u002F**\n     * @phpstan-return class-string\u003CTNodeType>\n     *\u002F\n    public function getNodeType(): string;\n\n    \u002F**\n     * @phpstan-param TNodeType $node\n     * @return (string|RuleError)[] errors\n     *\u002F\n    public function processNode(Node $node, Scope $scope): array;\n}\n",[19,319,320,326,334,339,344,352,356,361,366,371,390,394,399,405,434,439,470],{"__ignoreMap":58},[62,321,322],{"class":64,"line":65},[62,323,325],{"class":324},"srMjj","\u002F**\n",[62,327,328,331],{"class":64,"line":55},[62,329,330],{"class":324}," * ",[62,332,333],{"class":68},"@api\n",[62,335,336],{"class":64,"line":84},[62,337,338],{"class":324}," * @phpstan-template TNodeType of Node\n",[62,340,341],{"class":64,"line":98},[62,342,343],{"class":324}," *\u002F\n",[62,345,346,349],{"class":64,"line":103},[62,347,348],{"class":68},"interface",[62,350,351],{"class":90}," Rule\n",[62,353,354],{"class":64,"line":112},[62,355,115],{"class":94},[62,357,358],{"class":64,"line":118},[62,359,360],{"class":324},"    \u002F**\n",[62,362,363],{"class":64,"line":145},[62,364,365],{"class":324},"     * @phpstan-return class-string\u003CTNodeType>\n",[62,367,368],{"class":64,"line":151},[62,369,370],{"class":324},"     *\u002F\n",[62,372,373,375,377,380,383,385,388],{"class":64,"line":160},[62,374,121],{"class":68},[62,376,124],{"class":68},[62,378,379],{"class":90}," getNodeType",[62,381,382],{"class":94},"()",[62,384,139],{"class":68},[62,386,387],{"class":68}," string",[62,389,95],{"class":94},[62,391,392],{"class":64,"line":166},[62,393,81],{"emptyLinePlaceholder":80},[62,395,397],{"class":64,"line":396},12,[62,398,360],{"class":324},[62,400,402],{"class":64,"line":401},13,[62,403,404],{"class":324},"     * @phpstan-param TNodeType $node\n",[62,406,408,411,414,417,419,422,425,428,431],{"class":64,"line":407},14,[62,409,410],{"class":324},"     * ",[62,412,413],{"class":68},"@return",[62,415,416],{"class":324}," (",[62,418,133],{"class":68},[62,420,421],{"class":324},"|",[62,423,424],{"class":72},"RuleError",[62,426,427],{"class":324},")",[62,429,430],{"class":68},"[]",[62,432,433],{"class":324}," errors\n",[62,435,437],{"class":64,"line":436},15,[62,438,370],{"class":324},[62,440,442,444,446,449,451,454,457,460,463,465,468],{"class":64,"line":441},16,[62,443,121],{"class":68},[62,445,124],{"class":68},[62,447,448],{"class":90}," processNode",[62,450,130],{"class":94},[62,452,453],{"class":72},"Node",[62,455,456],{"class":94}," $node, ",[62,458,459],{"class":72},"Scope",[62,461,462],{"class":94}," $scope)",[62,464,139],{"class":68},[62,466,467],{"class":68}," array",[62,469,95],{"class":94},[62,471,473],{"class":64,"line":472},17,[62,474,169],{"class":94},[15,476,477,478,481,482,485,486,488,489,492,493,496],{},"The ",[19,479,480],{},"getNodeType"," method tells on which nodes our ",[19,483,484],{},"processNode"," method will be called. Our ",[19,487,484],{}," method should return an array of RuleError objects or an empty array in case everything is ok.\nSince we are dealing with files our ",[19,490,491],{},"TNodeType"," should be the ",[19,494,495],{},"PHPStan\\Node\\FileNode"," class, that way our rule will be called on every file, we should also update our generic Docblock, that way PHPStan won't complain about our rule class itself.",[15,498,499],{},"Here's how the implementation looks like so far:",[49,501,503],{"className":51,"code":502,"language":56,"meta":58,"style":58},"use PHPStan\\Node\\FileNode;\n\n\u002F**\n * @implements Rule\u003CFileNode>\n *\u002F\nclass EnforceStrictTypes implements Rule\n{\n    public function getNodeType(): string\n    {\n        return FileNode::class;\n    }\n\n    public function processNode(Node $node, Scope $scope): array\n    {\n        throw new \\Exception('To be implemented');\n    }\n}\n",[19,504,505,515,519,523,528,532,544,548,563,567,580,584,588,611,615,634,638],{"__ignoreMap":58},[62,506,507,510,513],{"class":64,"line":65},[62,508,509],{"class":68},"use",[62,511,512],{"class":72}," PHPStan\\Node\\FileNode",[62,514,95],{"class":94},[62,516,517],{"class":64,"line":55},[62,518,81],{"emptyLinePlaceholder":80},[62,520,521],{"class":64,"line":84},[62,522,325],{"class":324},[62,524,525],{"class":64,"line":98},[62,526,527],{"class":324}," * @implements Rule\u003CFileNode>\n",[62,529,530],{"class":64,"line":103},[62,531,343],{"class":324},[62,533,534,536,539,542],{"class":64,"line":112},[62,535,106],{"class":68},[62,537,538],{"class":90}," EnforceStrictTypes",[62,540,541],{"class":68}," implements",[62,543,351],{"class":90},[62,545,546],{"class":64,"line":118},[62,547,115],{"class":94},[62,549,550,552,554,556,558,560],{"class":64,"line":145},[62,551,121],{"class":68},[62,553,124],{"class":68},[62,555,379],{"class":90},[62,557,382],{"class":94},[62,559,139],{"class":68},[62,561,562],{"class":68}," string\n",[62,564,565],{"class":64,"line":151},[62,566,148],{"class":94},[62,568,569,572,575,578],{"class":64,"line":160},[62,570,571],{"class":68},"        return",[62,573,574],{"class":72}," FileNode",[62,576,577],{"class":68},"::class",[62,579,95],{"class":94},[62,581,582],{"class":64,"line":166},[62,583,163],{"class":94},[62,585,586],{"class":64,"line":396},[62,587,81],{"emptyLinePlaceholder":80},[62,589,590,592,594,596,598,600,602,604,606,608],{"class":64,"line":401},[62,591,121],{"class":68},[62,593,124],{"class":68},[62,595,448],{"class":90},[62,597,130],{"class":94},[62,599,453],{"class":72},[62,601,456],{"class":94},[62,603,459],{"class":72},[62,605,462],{"class":94},[62,607,139],{"class":68},[62,609,610],{"class":68}," array\n",[62,612,613],{"class":64,"line":407},[62,614,148],{"class":94},[62,616,617,620,623,626,628,631],{"class":64,"line":436},[62,618,619],{"class":68},"        throw",[62,621,622],{"class":68}," new",[62,624,625],{"class":72}," \\Exception",[62,627,130],{"class":94},[62,629,630],{"class":188},"'To be implemented'",[62,632,633],{"class":94},");\n",[62,635,636],{"class":64,"line":441},[62,637,163],{"class":94},[62,639,640],{"class":64,"line":472},[62,641,169],{"class":94},[15,643,644,645,647,648,650,651,653,654,656,657,659,660,26],{},"Now on to the ",[19,646,484],{}," implementation. We're gonna receive two objects: ",[19,649,453],{}," and ",[19,652,459],{},", we are only interested in the first one. Our ",[19,655,453],{}," object is an instance of what we defined on the ",[19,658,480],{}," method, so in this case a ",[19,661,662],{},"FileNode",[15,664,665],{},"Since we are dealing with a file our node will consist of a bunch of sub-nodes. Luckily we are looking for exactly the first node, because declare statements must be put at the start of files. I decided that empty files should not be checked, here's how our code looks so far:",[49,667,669],{"className":51,"code":668,"language":56,"meta":58,"style":58},"public function processNode(Node $node, Scope $scope): array\n{\n    $nodes = $node->getNodes();\n\n    if (empty($nodes)) {\n        return [];\n    }\n\n    $firstStatement = $nodes[0] ?? null;\n    \u002F\u002F check if $firstStatement is a declare block\n}\n",[19,670,671,694,698,717,721,734,741,745,749,773,778],{"__ignoreMap":58},[62,672,673,676,678,680,682,684,686,688,690,692],{"class":64,"line":65},[62,674,675],{"class":68},"public",[62,677,124],{"class":68},[62,679,448],{"class":90},[62,681,130],{"class":94},[62,683,453],{"class":72},[62,685,456],{"class":94},[62,687,459],{"class":72},[62,689,462],{"class":94},[62,691,139],{"class":68},[62,693,610],{"class":68},[62,695,696],{"class":64,"line":55},[62,697,115],{"class":94},[62,699,700,703,705,708,711,714],{"class":64,"line":84},[62,701,702],{"class":94},"    $nodes ",[62,704,263],{"class":68},[62,706,707],{"class":94}," $node",[62,709,710],{"class":68},"->",[62,712,713],{"class":90},"getNodes",[62,715,716],{"class":94},"();\n",[62,718,719],{"class":64,"line":98},[62,720,81],{"emptyLinePlaceholder":80},[62,722,723,726,728,731],{"class":64,"line":103},[62,724,725],{"class":68},"    if",[62,727,416],{"class":94},[62,729,730],{"class":72},"empty",[62,732,733],{"class":94},"($nodes)) {\n",[62,735,736,738],{"class":64,"line":112},[62,737,571],{"class":68},[62,739,740],{"class":94}," [];\n",[62,742,743],{"class":64,"line":118},[62,744,163],{"class":94},[62,746,747],{"class":64,"line":145},[62,748,81],{"emptyLinePlaceholder":80},[62,750,751,754,756,759,762,765,768,771],{"class":64,"line":151},[62,752,753],{"class":94},"    $firstStatement ",[62,755,263],{"class":68},[62,757,758],{"class":94}," $nodes[",[62,760,761],{"class":72},"0",[62,763,764],{"class":94},"] ",[62,766,767],{"class":68},"??",[62,769,770],{"class":72}," null",[62,772,95],{"class":94},[62,774,775],{"class":64,"line":160},[62,776,777],{"class":324},"    \u002F\u002F check if $firstStatement is a declare block\n",[62,779,780],{"class":64,"line":166},[62,781,169],{"class":94},[15,783,784,785,788,789,26],{},"Now it's basically done, all we have to do is check if the ",[19,786,787],{},"$firstStatement"," variable is defined and is an instance of ",[19,790,791],{},"PhpParser\\Node\\Stmt\\Declare_",[49,793,795],{"className":51,"code":794,"language":56,"meta":58,"style":58},"$firstStatement = $nodes[0];\n$fileDeclaresStrictTypes = $firstStatement instanceof Declare_;\n\nif (!$fileDeclaresStrictTypes) {\n    return [RuleErrorBuilder::message(\n        'File is missing declare(strict_types=1) statement'\n    )->build()];\n}\n",[19,796,797,811,829,833,846,866,871,884],{"__ignoreMap":58},[62,798,799,802,804,806,808],{"class":64,"line":65},[62,800,801],{"class":94},"$firstStatement ",[62,803,263],{"class":68},[62,805,758],{"class":94},[62,807,761],{"class":72},[62,809,810],{"class":94},"];\n",[62,812,813,816,818,821,824,827],{"class":64,"line":55},[62,814,815],{"class":94},"$fileDeclaresStrictTypes ",[62,817,263],{"class":68},[62,819,820],{"class":94}," $firstStatement ",[62,822,823],{"class":68},"instanceof",[62,825,826],{"class":72}," Declare_",[62,828,95],{"class":94},[62,830,831],{"class":64,"line":84},[62,832,81],{"emptyLinePlaceholder":80},[62,834,835,838,840,843],{"class":64,"line":98},[62,836,837],{"class":68},"if",[62,839,416],{"class":94},[62,841,842],{"class":68},"!",[62,844,845],{"class":94},"$fileDeclaresStrictTypes) {\n",[62,847,848,851,854,857,860,863],{"class":64,"line":103},[62,849,850],{"class":68},"    return",[62,852,853],{"class":94}," [",[62,855,856],{"class":72},"RuleErrorBuilder",[62,858,859],{"class":68},"::",[62,861,862],{"class":90},"message",[62,864,865],{"class":94},"(\n",[62,867,868],{"class":64,"line":112},[62,869,870],{"class":188},"        'File is missing declare(strict_types=1) statement'\n",[62,872,873,876,878,881],{"class":64,"line":118},[62,874,875],{"class":94},"    )",[62,877,710],{"class":68},[62,879,880],{"class":90},"build",[62,882,883],{"class":94},"()];\n",[62,885,886],{"class":64,"line":145},[62,887,169],{"class":94},[15,889,890,891,894,895,898],{},"We also need to check if ",[19,892,893],{},"strict_types"," is set to 1. AFAIK PhpParser generates some code during runtime while it parses the code, that caused PHPStan to not recognize some properties of the object, hence the ",[19,896,897],{},"@phpstan-ignore-next-line",". Anyway, here's how it looks:",[49,900,902],{"className":51,"code":901,"language":56,"meta":58,"style":58},"\u002F* @phpstan-ignore-next-line *\u002F\n$strictTypesSetAsOne = $firstStatement->declares[0]->value->value === 1;\n\nif (!$strictTypesSetAsOne) {\n    return [RuleErrorBuilder::message(\n        'declare(strict_types=1) statement value must be set as 1'\n    )->build()];\n}\n",[19,903,904,909,946,950,961,975,980,990],{"__ignoreMap":58},[62,905,906],{"class":64,"line":65},[62,907,908],{"class":324},"\u002F* @phpstan-ignore-next-line *\u002F\n",[62,910,911,914,916,919,921,924,926,929,931,934,936,939,942,944],{"class":64,"line":55},[62,912,913],{"class":94},"$strictTypesSetAsOne ",[62,915,263],{"class":68},[62,917,918],{"class":94}," $firstStatement",[62,920,710],{"class":68},[62,922,923],{"class":94},"declares[",[62,925,761],{"class":72},[62,927,928],{"class":94},"]",[62,930,710],{"class":68},[62,932,933],{"class":94},"value",[62,935,710],{"class":68},[62,937,938],{"class":94},"value ",[62,940,941],{"class":68},"===",[62,943,293],{"class":72},[62,945,95],{"class":94},[62,947,948],{"class":64,"line":84},[62,949,81],{"emptyLinePlaceholder":80},[62,951,952,954,956,958],{"class":64,"line":98},[62,953,837],{"class":68},[62,955,416],{"class":94},[62,957,842],{"class":68},[62,959,960],{"class":94},"$strictTypesSetAsOne) {\n",[62,962,963,965,967,969,971,973],{"class":64,"line":103},[62,964,850],{"class":68},[62,966,853],{"class":94},[62,968,856],{"class":72},[62,970,859],{"class":68},[62,972,862],{"class":90},[62,974,865],{"class":94},[62,976,977],{"class":64,"line":112},[62,978,979],{"class":188},"        'declare(strict_types=1) statement value must be set as 1'\n",[62,981,982,984,986,988],{"class":64,"line":118},[62,983,875],{"class":94},[62,985,710],{"class":68},[62,987,880],{"class":90},[62,989,883],{"class":94},[62,991,992],{"class":64,"line":145},[62,993,169],{"class":94},[15,995,996],{},"And here's the final code:",[49,998,1000],{"className":51,"code":999,"language":56,"meta":58,"style":58},"\u002F**\n * @implements Rule\u003CFileNode>\n *\u002F\nclass EnforceStrictTypes implements Rule\n{\n    public function getNodeType(): string\n    {\n        return FileNode::class;\n    }\n\n    public function processNode(Node $node, Scope $scope): array\n    {\n        $nodes = $node->getNodes();\n\n        if (empty($nodes)) {\n            return [];\n        }\n\n        $firstStatement = $nodes[0];\n        $fileDeclaresStrictTypes = $firstStatement instanceof Declare_;\n\n        if (!$fileDeclaresStrictTypes) {\n            return [RuleErrorBuilder::message(\n                'File is missing declare(strict_types=1) statement'\n            )->build()];\n        }\n\n        \u002F* @phpstan-ignore-next-line *\u002F\n        $strictTypesSetAsOne = $firstStatement->declares[0]->value->value === 1;\n\n        if (!$strictTypesSetAsOne) {\n            return [RuleErrorBuilder::message(\n                'declare(strict_types=1) statement value must be set as 1'\n            )->build()];\n        }\n\n        return [];\n    }\n}\n",[19,1001,1002,1006,1010,1014,1024,1028,1042,1046,1056,1060,1064,1086,1090,1105,1109,1120,1127,1132,1137,1151,1167,1172,1183,1198,1204,1216,1221,1226,1232,1264,1269,1280,1295,1301,1312,1317,1322,1329,1334],{"__ignoreMap":58},[62,1003,1004],{"class":64,"line":65},[62,1005,325],{"class":324},[62,1007,1008],{"class":64,"line":55},[62,1009,527],{"class":324},[62,1011,1012],{"class":64,"line":84},[62,1013,343],{"class":324},[62,1015,1016,1018,1020,1022],{"class":64,"line":98},[62,1017,106],{"class":68},[62,1019,538],{"class":90},[62,1021,541],{"class":68},[62,1023,351],{"class":90},[62,1025,1026],{"class":64,"line":103},[62,1027,115],{"class":94},[62,1029,1030,1032,1034,1036,1038,1040],{"class":64,"line":112},[62,1031,121],{"class":68},[62,1033,124],{"class":68},[62,1035,379],{"class":90},[62,1037,382],{"class":94},[62,1039,139],{"class":68},[62,1041,562],{"class":68},[62,1043,1044],{"class":64,"line":118},[62,1045,148],{"class":94},[62,1047,1048,1050,1052,1054],{"class":64,"line":145},[62,1049,571],{"class":68},[62,1051,574],{"class":72},[62,1053,577],{"class":68},[62,1055,95],{"class":94},[62,1057,1058],{"class":64,"line":151},[62,1059,163],{"class":94},[62,1061,1062],{"class":64,"line":160},[62,1063,81],{"emptyLinePlaceholder":80},[62,1065,1066,1068,1070,1072,1074,1076,1078,1080,1082,1084],{"class":64,"line":166},[62,1067,121],{"class":68},[62,1069,124],{"class":68},[62,1071,448],{"class":90},[62,1073,130],{"class":94},[62,1075,453],{"class":72},[62,1077,456],{"class":94},[62,1079,459],{"class":72},[62,1081,462],{"class":94},[62,1083,139],{"class":68},[62,1085,610],{"class":68},[62,1087,1088],{"class":64,"line":396},[62,1089,148],{"class":94},[62,1091,1092,1095,1097,1099,1101,1103],{"class":64,"line":401},[62,1093,1094],{"class":94},"        $nodes ",[62,1096,263],{"class":68},[62,1098,707],{"class":94},[62,1100,710],{"class":68},[62,1102,713],{"class":90},[62,1104,716],{"class":94},[62,1106,1107],{"class":64,"line":407},[62,1108,81],{"emptyLinePlaceholder":80},[62,1110,1111,1114,1116,1118],{"class":64,"line":436},[62,1112,1113],{"class":68},"        if",[62,1115,416],{"class":94},[62,1117,730],{"class":72},[62,1119,733],{"class":94},[62,1121,1122,1125],{"class":64,"line":441},[62,1123,1124],{"class":68},"            return",[62,1126,740],{"class":94},[62,1128,1129],{"class":64,"line":472},[62,1130,1131],{"class":94},"        }\n",[62,1133,1135],{"class":64,"line":1134},18,[62,1136,81],{"emptyLinePlaceholder":80},[62,1138,1140,1143,1145,1147,1149],{"class":64,"line":1139},19,[62,1141,1142],{"class":94},"        $firstStatement ",[62,1144,263],{"class":68},[62,1146,758],{"class":94},[62,1148,761],{"class":72},[62,1150,810],{"class":94},[62,1152,1154,1157,1159,1161,1163,1165],{"class":64,"line":1153},20,[62,1155,1156],{"class":94},"        $fileDeclaresStrictTypes ",[62,1158,263],{"class":68},[62,1160,820],{"class":94},[62,1162,823],{"class":68},[62,1164,826],{"class":72},[62,1166,95],{"class":94},[62,1168,1170],{"class":64,"line":1169},21,[62,1171,81],{"emptyLinePlaceholder":80},[62,1173,1175,1177,1179,1181],{"class":64,"line":1174},22,[62,1176,1113],{"class":68},[62,1178,416],{"class":94},[62,1180,842],{"class":68},[62,1182,845],{"class":94},[62,1184,1186,1188,1190,1192,1194,1196],{"class":64,"line":1185},23,[62,1187,1124],{"class":68},[62,1189,853],{"class":94},[62,1191,856],{"class":72},[62,1193,859],{"class":68},[62,1195,862],{"class":90},[62,1197,865],{"class":94},[62,1199,1201],{"class":64,"line":1200},24,[62,1202,1203],{"class":188},"                'File is missing declare(strict_types=1) statement'\n",[62,1205,1207,1210,1212,1214],{"class":64,"line":1206},25,[62,1208,1209],{"class":94},"            )",[62,1211,710],{"class":68},[62,1213,880],{"class":90},[62,1215,883],{"class":94},[62,1217,1219],{"class":64,"line":1218},26,[62,1220,1131],{"class":94},[62,1222,1224],{"class":64,"line":1223},27,[62,1225,81],{"emptyLinePlaceholder":80},[62,1227,1229],{"class":64,"line":1228},28,[62,1230,1231],{"class":324},"        \u002F* @phpstan-ignore-next-line *\u002F\n",[62,1233,1235,1238,1240,1242,1244,1246,1248,1250,1252,1254,1256,1258,1260,1262],{"class":64,"line":1234},29,[62,1236,1237],{"class":94},"        $strictTypesSetAsOne ",[62,1239,263],{"class":68},[62,1241,918],{"class":94},[62,1243,710],{"class":68},[62,1245,923],{"class":94},[62,1247,761],{"class":72},[62,1249,928],{"class":94},[62,1251,710],{"class":68},[62,1253,933],{"class":94},[62,1255,710],{"class":68},[62,1257,938],{"class":94},[62,1259,941],{"class":68},[62,1261,293],{"class":72},[62,1263,95],{"class":94},[62,1265,1267],{"class":64,"line":1266},30,[62,1268,81],{"emptyLinePlaceholder":80},[62,1270,1272,1274,1276,1278],{"class":64,"line":1271},31,[62,1273,1113],{"class":68},[62,1275,416],{"class":94},[62,1277,842],{"class":68},[62,1279,960],{"class":94},[62,1281,1283,1285,1287,1289,1291,1293],{"class":64,"line":1282},32,[62,1284,1124],{"class":68},[62,1286,853],{"class":94},[62,1288,856],{"class":72},[62,1290,859],{"class":68},[62,1292,862],{"class":90},[62,1294,865],{"class":94},[62,1296,1298],{"class":64,"line":1297},33,[62,1299,1300],{"class":188},"                'declare(strict_types=1) statement value must be set as 1'\n",[62,1302,1304,1306,1308,1310],{"class":64,"line":1303},34,[62,1305,1209],{"class":94},[62,1307,710],{"class":68},[62,1309,880],{"class":90},[62,1311,883],{"class":94},[62,1313,1315],{"class":64,"line":1314},35,[62,1316,1131],{"class":94},[62,1318,1320],{"class":64,"line":1319},36,[62,1321,81],{"emptyLinePlaceholder":80},[62,1323,1325,1327],{"class":64,"line":1324},37,[62,1326,571],{"class":68},[62,1328,740],{"class":94},[62,1330,1332],{"class":64,"line":1331},38,[62,1333,163],{"class":94},[62,1335,1337],{"class":64,"line":1336},39,[62,1338,169],{"class":94},[15,1340,1341],{},"Right now everything should be working and when you analyse the code it will complain about files not declaring strict types.",[10,1343,1345],{"id":1344},"testing-our-custom-rule","Testing our custom rule",[15,1347,1348],{},"Writing a custom rule is not enough, we should also unit test it to make sure the rule itself will do what it's suppose to do. PHPStan paired with PHPUnit gives us a very easy to use API to test custom rules.",[15,1350,1351,1352,1355],{},"To test our rule we need to create a PHPUnit test and extend the ",[19,1353,1354],{},"PHPStan\\Testing\\RuleTestCase"," class. We also need to create fixture files that will be used as examples for testing our rule. I created 5 examples (the names are pretty self-explanatory):",[42,1357,1358,1361,1364,1367,1370],{},[45,1359,1360],{},"ClassWithStrictTypes.php",[45,1362,1363],{},"ClassWithoutStrictTypes.php",[45,1365,1366],{},"ClassWithStrictTypesSetAsFalse.php",[45,1368,1369],{},"empty-file.php",[45,1371,1372],{},"file-without-strict-types.php",[15,1374,1375],{},"The tests I wrote:",[49,1377,1379],{"className":51,"code":1378,"language":56,"meta":58,"style":58},"\u002F**\n * @extends RuleTestCase\u003CEnforceStrictTypes>\n *\u002F\nclass EnforceStrictTypesTest extends RuleTestCase\n{\n    protected function getRule(): Rule\n    {\n        return new EnforceStrictTypes();\n    }\n\n    #[Test]\n    public function itEnforcesStrictTypesOnAllFiles(): void\n    {\n        $fixtures = [\n            __DIR__ . '\u002F..\u002F..\u002FData\u002FClassWithoutStrictTypes.php',\n            __DIR__ . '\u002F..\u002F..\u002FData\u002Ffile-without-strict-types.php',\n            __DIR__ . '\u002F..\u002F..\u002FData\u002FClassWithStrictTypes.php', \u002F\u002F this one is ok\n        ];\n\n        $this->analyse($fixtures, [\n            ['File is missing declare(strict_types=1) statement', 3],\n            ['File is missing declare(strict_types=1) statement', 3],\n        ]);\n    }\n\n    #[Test]\n    public function itChecksIfStrictTypesIsSetToAValueDifferentThanOne(): void\n    {\n        $fixtures = [__DIR__ . '\u002F..\u002F..\u002FData\u002FClassWithStrictTypesSetAsFalse.php'];\n\n        $this->analyse($fixtures, [['declare(strict_types=1) statement value must be set as 1', 3]]);\n    }\n\n    #[Test]\n    public function itDoesNotCheckEmptyFiles(): void\n    {\n        $fixtures = [__DIR__ . '\u002F..\u002F..\u002FData\u002Fempty-file.php'];\n\n        $this->analyse($fixtures, []);\n    }\n}\n",[19,1380,1381,1385,1390,1394,1407,1411,1427,1431,1441,1445,1449,1460,1475,1479,1489,1503,1514,1529,1534,1538,1551,1567,1579,1584,1588,1592,1600,1615,1619,1637,1641,1662,1666,1670,1678,1693,1697,1714,1718,1729,1734],{"__ignoreMap":58},[62,1382,1383],{"class":64,"line":65},[62,1384,325],{"class":324},[62,1386,1387],{"class":64,"line":55},[62,1388,1389],{"class":324}," * @extends RuleTestCase\u003CEnforceStrictTypes>\n",[62,1391,1392],{"class":64,"line":84},[62,1393,343],{"class":324},[62,1395,1396,1398,1401,1404],{"class":64,"line":98},[62,1397,106],{"class":68},[62,1399,1400],{"class":90}," EnforceStrictTypesTest",[62,1402,1403],{"class":68}," extends",[62,1405,1406],{"class":90}," RuleTestCase\n",[62,1408,1409],{"class":64,"line":103},[62,1410,115],{"class":94},[62,1412,1413,1416,1418,1421,1423,1425],{"class":64,"line":112},[62,1414,1415],{"class":68},"    protected",[62,1417,124],{"class":68},[62,1419,1420],{"class":90}," getRule",[62,1422,382],{"class":94},[62,1424,139],{"class":68},[62,1426,351],{"class":72},[62,1428,1429],{"class":64,"line":118},[62,1430,148],{"class":94},[62,1432,1433,1435,1437,1439],{"class":64,"line":145},[62,1434,571],{"class":68},[62,1436,622],{"class":68},[62,1438,538],{"class":72},[62,1440,716],{"class":94},[62,1442,1443],{"class":64,"line":151},[62,1444,163],{"class":94},[62,1446,1447],{"class":64,"line":160},[62,1448,81],{"emptyLinePlaceholder":80},[62,1450,1451,1454,1457],{"class":64,"line":166},[62,1452,1453],{"class":94},"    #[",[62,1455,1456],{"class":72},"Test",[62,1458,1459],{"class":94},"]\n",[62,1461,1462,1464,1466,1469,1471,1473],{"class":64,"line":396},[62,1463,121],{"class":68},[62,1465,124],{"class":68},[62,1467,1468],{"class":90}," itEnforcesStrictTypesOnAllFiles",[62,1470,382],{"class":94},[62,1472,139],{"class":68},[62,1474,142],{"class":68},[62,1476,1477],{"class":64,"line":401},[62,1478,148],{"class":94},[62,1480,1481,1484,1486],{"class":64,"line":407},[62,1482,1483],{"class":94},"        $fixtures ",[62,1485,263],{"class":68},[62,1487,1488],{"class":94}," [\n",[62,1490,1491,1494,1497,1500],{"class":64,"line":436},[62,1492,1493],{"class":72},"            __DIR__",[62,1495,1496],{"class":68}," .",[62,1498,1499],{"class":188}," '\u002F..\u002F..\u002FData\u002FClassWithoutStrictTypes.php'",[62,1501,1502],{"class":94},",\n",[62,1504,1505,1507,1509,1512],{"class":64,"line":441},[62,1506,1493],{"class":72},[62,1508,1496],{"class":68},[62,1510,1511],{"class":188}," '\u002F..\u002F..\u002FData\u002Ffile-without-strict-types.php'",[62,1513,1502],{"class":94},[62,1515,1516,1518,1520,1523,1526],{"class":64,"line":472},[62,1517,1493],{"class":72},[62,1519,1496],{"class":68},[62,1521,1522],{"class":188}," '\u002F..\u002F..\u002FData\u002FClassWithStrictTypes.php'",[62,1524,1525],{"class":94},", ",[62,1527,1528],{"class":324},"\u002F\u002F this one is ok\n",[62,1530,1531],{"class":64,"line":1134},[62,1532,1533],{"class":94},"        ];\n",[62,1535,1536],{"class":64,"line":1139},[62,1537,81],{"emptyLinePlaceholder":80},[62,1539,1540,1543,1545,1548],{"class":64,"line":1153},[62,1541,1542],{"class":72},"        $this",[62,1544,710],{"class":68},[62,1546,1547],{"class":90},"analyse",[62,1549,1550],{"class":94},"($fixtures, [\n",[62,1552,1553,1556,1559,1561,1564],{"class":64,"line":1169},[62,1554,1555],{"class":94},"            [",[62,1557,1558],{"class":188},"'File is missing declare(strict_types=1) statement'",[62,1560,1525],{"class":94},[62,1562,1563],{"class":72},"3",[62,1565,1566],{"class":94},"],\n",[62,1568,1569,1571,1573,1575,1577],{"class":64,"line":1174},[62,1570,1555],{"class":94},[62,1572,1558],{"class":188},[62,1574,1525],{"class":94},[62,1576,1563],{"class":72},[62,1578,1566],{"class":94},[62,1580,1581],{"class":64,"line":1185},[62,1582,1583],{"class":94},"        ]);\n",[62,1585,1586],{"class":64,"line":1200},[62,1587,163],{"class":94},[62,1589,1590],{"class":64,"line":1206},[62,1591,81],{"emptyLinePlaceholder":80},[62,1593,1594,1596,1598],{"class":64,"line":1218},[62,1595,1453],{"class":94},[62,1597,1456],{"class":72},[62,1599,1459],{"class":94},[62,1601,1602,1604,1606,1609,1611,1613],{"class":64,"line":1223},[62,1603,121],{"class":68},[62,1605,124],{"class":68},[62,1607,1608],{"class":90}," itChecksIfStrictTypesIsSetToAValueDifferentThanOne",[62,1610,382],{"class":94},[62,1612,139],{"class":68},[62,1614,142],{"class":68},[62,1616,1617],{"class":64,"line":1228},[62,1618,148],{"class":94},[62,1620,1621,1623,1625,1627,1630,1632,1635],{"class":64,"line":1234},[62,1622,1483],{"class":94},[62,1624,263],{"class":68},[62,1626,853],{"class":94},[62,1628,1629],{"class":72},"__DIR__",[62,1631,1496],{"class":68},[62,1633,1634],{"class":188}," '\u002F..\u002F..\u002FData\u002FClassWithStrictTypesSetAsFalse.php'",[62,1636,810],{"class":94},[62,1638,1639],{"class":64,"line":1266},[62,1640,81],{"emptyLinePlaceholder":80},[62,1642,1643,1645,1647,1649,1652,1655,1657,1659],{"class":64,"line":1271},[62,1644,1542],{"class":72},[62,1646,710],{"class":68},[62,1648,1547],{"class":90},[62,1650,1651],{"class":94},"($fixtures, [[",[62,1653,1654],{"class":188},"'declare(strict_types=1) statement value must be set as 1'",[62,1656,1525],{"class":94},[62,1658,1563],{"class":72},[62,1660,1661],{"class":94},"]]);\n",[62,1663,1664],{"class":64,"line":1282},[62,1665,163],{"class":94},[62,1667,1668],{"class":64,"line":1297},[62,1669,81],{"emptyLinePlaceholder":80},[62,1671,1672,1674,1676],{"class":64,"line":1303},[62,1673,1453],{"class":94},[62,1675,1456],{"class":72},[62,1677,1459],{"class":94},[62,1679,1680,1682,1684,1687,1689,1691],{"class":64,"line":1314},[62,1681,121],{"class":68},[62,1683,124],{"class":68},[62,1685,1686],{"class":90}," itDoesNotCheckEmptyFiles",[62,1688,382],{"class":94},[62,1690,139],{"class":68},[62,1692,142],{"class":68},[62,1694,1695],{"class":64,"line":1319},[62,1696,148],{"class":94},[62,1698,1699,1701,1703,1705,1707,1709,1712],{"class":64,"line":1324},[62,1700,1483],{"class":94},[62,1702,263],{"class":68},[62,1704,853],{"class":94},[62,1706,1629],{"class":72},[62,1708,1496],{"class":68},[62,1710,1711],{"class":188}," '\u002F..\u002F..\u002FData\u002Fempty-file.php'",[62,1713,810],{"class":94},[62,1715,1716],{"class":64,"line":1331},[62,1717,81],{"emptyLinePlaceholder":80},[62,1719,1720,1722,1724,1726],{"class":64,"line":1336},[62,1721,1542],{"class":72},[62,1723,710],{"class":68},[62,1725,1547],{"class":90},[62,1727,1728],{"class":94},"($fixtures, []);\n",[62,1730,1732],{"class":64,"line":1731},40,[62,1733,163],{"class":94},[62,1735,1737],{"class":64,"line":1736},41,[62,1738,169],{"class":94},[15,1740,1741],{},"The analyse method second parameter is an array of errors that should return, it should contain the message and the line where the error occurred.",[10,1743,1745],{"id":1744},"bonus","Bonus",[15,1747,1748,1749,1752],{},"PHPStan will also complain about our fixture files, to ignore them set this to your ",[19,1750,1751],{},"phpstan.neon"," config file.",[49,1754,1758],{"className":1755,"code":1756,"language":1757,"meta":58,"style":58},"language-yaml shiki shiki-themes github-light github-dark github-light","parameters:\n    level: 8\n    paths:\n        - src\n        - tests\n    excludePaths:\n        - *\u002Ftests\u002FData\u002F*.php\nrules:\n    - App\\Rules\\EnforceStrictTypes\n","yaml",[19,1759,1760,1769,1780,1787,1795,1802,1809,1817,1824],{"__ignoreMap":58},[62,1761,1762,1766],{"class":64,"line":65},[62,1763,1765],{"class":1764},"sxbZF","parameters",[62,1767,1768],{"class":94},":\n",[62,1770,1771,1774,1777],{"class":64,"line":55},[62,1772,1773],{"class":1764},"    level",[62,1775,1776],{"class":94},": ",[62,1778,1779],{"class":72},"8\n",[62,1781,1782,1785],{"class":64,"line":84},[62,1783,1784],{"class":1764},"    paths",[62,1786,1768],{"class":94},[62,1788,1789,1792],{"class":64,"line":98},[62,1790,1791],{"class":94},"        - ",[62,1793,1794],{"class":188},"src\n",[62,1796,1797,1799],{"class":64,"line":103},[62,1798,1791],{"class":94},[62,1800,1801],{"class":188},"tests\n",[62,1803,1804,1807],{"class":64,"line":112},[62,1805,1806],{"class":1764},"    excludePaths",[62,1808,1768],{"class":94},[62,1810,1811,1814],{"class":64,"line":118},[62,1812,1813],{"class":94},"        - *",[62,1815,1816],{"class":188},"\u002Ftests\u002FData\u002F*.php\n",[62,1818,1819,1822],{"class":64,"line":145},[62,1820,1821],{"class":1764},"rules",[62,1823,1768],{"class":94},[62,1825,1826,1829],{"class":64,"line":151},[62,1827,1828],{"class":94},"    - ",[62,1830,1831],{"class":188},"App\\Rules\\EnforceStrictTypes\n",[1833,1834,1835],"style",{},"html pre.shiki code .s77lx, html code.shiki .s77lx{--shiki-light:#D73A49;--shiki-dark:#F97583;--shiki-default:#D73A49}html pre.shiki code .s-VKZ, html code.shiki .s-VKZ{--shiki-light:#005CC5;--shiki-dark:#79B8FF;--shiki-default:#005CC5}html pre.shiki code .sXzdU, html code.shiki .sXzdU{--shiki-light:#6F42C1;--shiki-dark:#B392F0;--shiki-default:#6F42C1}html pre.shiki code .sov1W, html code.shiki .sov1W{--shiki-light:#24292E;--shiki-dark:#E1E4E8;--shiki-default:#24292E}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sSPdU, html code.shiki .sSPdU{--shiki-light:#032F62;--shiki-dark:#9ECBFF;--shiki-default:#032F62}html pre.shiki code .srMjj, html code.shiki .srMjj{--shiki-light:#6A737D;--shiki-dark:#6A737D;--shiki-default:#6A737D}html pre.shiki code .sxbZF, html code.shiki .sxbZF{--shiki-light:#22863A;--shiki-dark:#85E89D;--shiki-default:#22863A}",{"title":58,"searchDepth":55,"depth":55,"links":1837},[1838,1839,1840,1841,1842],{"id":12,"depth":55,"text":13},{"id":39,"depth":55,"text":40},{"id":310,"depth":55,"text":311},{"id":1344,"depth":55,"text":1345},{"id":1744,"depth":55,"text":1745},"md",{},"\u002Fblog\u002Fusing-phpstan-to-enforce-declare-strict-types-1","2023-03-25",{"title":5,"description":58},"blog\u002F20230325","Leveraging PHPStan Custom Rules to set standards across a codebase",[56,1851],"static analysis","hhF4BD3YDJKKtWNEYRm_G7hvN7n_ML5IPwrXNK98UPk",1775693075983]