├── GUIDELINES.md
├── LICENSE
├── NeutronStandard
├── SniffHelpers.php
├── Sniffs
│ ├── Arrays
│ │ └── DisallowLongformArraySniff.php
│ ├── AssignAlign
│ │ └── DisallowAssignAlignSniff.php
│ ├── Conditions
│ │ └── DisallowConditionAssignWithoutConditionalSniff.php
│ ├── Constants
│ │ └── DisallowDefineSniff.php
│ ├── Extract
│ │ └── DisallowExtractSniff.php
│ ├── Functions
│ │ ├── DisallowCallUserFuncSniff.php
│ │ ├── LongFunctionSniff.php
│ │ ├── TypeHintSniff.php
│ │ └── VariableFunctionsSniff.php
│ ├── Globals
│ │ └── DisallowGlobalFunctionsSniff.php
│ ├── MagicMethods
│ │ ├── DisallowMagicGetSniff.php
│ │ ├── DisallowMagicSerializeSniff.php
│ │ ├── DisallowMagicSetSniff.php
│ │ └── RiskyMagicMethodSniff.php
│ ├── StrictTypes
│ │ └── RequireStrictTypesSniff.php
│ └── Whitespace
│ │ ├── DisallowMultipleNewlinesSniff.php
│ │ ├── RequireNewlineBetweenFunctionsSniff.php
│ │ └── RequireParenthesisSpacingSniff.php
└── ruleset.xml
├── README.md
└── composer.json
/GUIDELINES.md:
--------------------------------------------------------------------------------
1 | # Neutron PHP Coding Guidelines
2 |
3 | The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
4 |
5 | ## Strict types
6 |
7 | **New PHP files MUST include a strict types directive.**
8 |
9 | The directive looks like this: `declare( strict_types=1 );`
10 |
11 | This prevents automatic scalar type conversion when validating types (so-called "weak typing"). You can [read about this directive here](http://php.net/manual/en/migration70.new-features.php#migration70.new-features.scalar-type-declarations).
12 |
13 | ## Extract
14 |
15 | **New code MUST NOT use the extract function.**
16 |
17 | Using `extract()` declares variables without declaration statements which obfuscates where those variables are defined.
18 |
19 | ## Magic methods
20 |
21 | **New code MUST NOT use the magic methods __get, __set, or __serialize.**
22 |
23 | **New code SHOULD NOT use the magic methods __invoke, __call, or __callStatic.**
24 |
25 | Magic methods can hide behavior which can mislead future developers. Traditional getters and setters are usually easier to understand.
26 |
27 | We allow the possibility of legitimate use-cases of the "callable" magic methods which can be used to increase readability without increasing complexity. This is subjective and must be considered on a per-case basis.
28 |
29 | ## Global functions
30 |
31 | **New code MUST NOT add functions to the global namespace.**
32 |
33 | Global functions risk causing namespace collisions, and thus require explicit namespacing in the function name itself. Since the language supports namespaces for organization, we can use them to increase readability and reduce that risk.
34 |
35 | The remaining advantage of global functions is portability across a wide codebase that may require versions of PHP which did not support namespaces. This should not be an issue for new code.
36 |
37 | ## Static methods
38 |
39 | **New code SHOULD NOT introduce new static methods.**
40 |
41 | In many cases, a namespaced function (a function inside a namespace but not inside a class) is more clear than a static method. They also do not have a dependency on a class itself. Exceptions certainly exist, for example, factory functions to create new object instances.
42 |
43 | Another exception is when several stand-alone functions must call each other and most of those calls should be private. Since it is not possible to make a namespaced function private, static methods are a better choice. Similarly, any stand-alone function which requires access to properties of an object which cannot be instance properties would do better with static definition (although there is a strong argument for avoiding static properties since they are global variables).
44 |
45 | ## Functions with side effects
46 |
47 | **New code SHOULD NOT introduce non-pure functions that are not methods of an object except within classes whose specific purpose are to make side effects.**
48 |
49 | Side effects include but are not limited to database writes, network requests, writing files, changing global variables, sending an email, or writing to IRC or Slack.
50 |
51 | To be more specific, a function should not make a call like `update_database()` directly. Instead it should call `$this->database->update_database()`. In this example, `$this->database` should be an instance of a class whose sole purpose is to update a database. `$this->database` should be injected into the class or function which uses it by using some form of dependency injection.
52 |
53 | Calling a function with side effects creates an implicit and tightly-coupled dependency on that function. It is implicit because it may not be obvious to the developer using the code, and it is tightly coupled because it cannot be mocked.
54 |
55 | On the other hand, if the code being tested is explicitly in a class whose sole purpose is to send messages or update a database, the risk is reduced. In this situation, special measures can be taken to mock the side effects, or they can just be assumed to be correct. Such classes should have as little business logic as possible in order to reduce the need for automated testing.
56 |
57 | ## Global variables
58 |
59 | **New code MUST NOT use global variables except within classes whose specific purpose are to make side effects.**
60 |
61 | Global variables create implicit and tightly coupled dependencies on arbitrary data. These dependencies cannot be easily mocked and may not be obvious to someone using the code.
62 |
63 | On the other hand, if the code being tested is explicitly in a class whose sole purpose is to use a global variable, the risk is reduced. In this situation, special measures can be taken to mock the side effects, or they can just be assumed to be correct. Such classes should have as little business logic as possible in order to reduce the need for automated testing.
64 |
65 | ## Explicit Types
66 |
67 | **New code SHOULD use argument and return types when possible.**
68 |
69 | Strong types allow the compiler to spot errors when PHP code is being executed, preventing subtle bugs by making usage explicit. It also allows the compiler to optimize execution of function calls. Finally, it aids in function design by exposing implicit assumptions about the data being passed between parts of a code base and making them explicit. This can help future developers (or our future selves) from mistaking these assumptions.
70 |
71 | This is complicated by types which can be multiple values. For example, some functions can return `WP_Error` or `string`. It can also be non-specific in the case of generics such as `array`, since the type does not specify what the array contains. In those cases, PHPDoc comments should be used to explain types.
72 |
73 | ## Constants
74 |
75 | **New code MUST NOT use the define keyword.**
76 |
77 | Often using the `define` keyword is actually just a way of global variables, except worse because they cannot be changed for testing without special hacks. Any conditions which rely on those constants cannot be tested.
78 |
79 | Replacing strings, magic numbers, etc., that are easy to misspell or confuse is a valid use of constants. Using the `const` keyword for defining these within a class is a good way of making sure these are isolated and namespaced. Using `define` creates global constants, which can certainly be useful in PHP code, but it's rare that new ones need to be created.
80 |
81 | The problem appears when trying to test code whose flow is dependent on these constants. For example:
82 |
83 | ```php
84 | function doSomething() {
85 | if (FOOBAR) {
86 | doX();
87 | } else {
88 | doY();
89 | }
90 | }
91 | ```
92 |
93 | In this case it’s really hard to write tests for `doSomething()` because `FOOBAR` is being used as an implicit global variable. When using some testing architectures (like phpunit), it’s not possible to change a constant even between different tests (because they’re run in the same PHP process and constants, by definition, cannot change).
94 |
95 | ## Function naming
96 |
97 | **New functions MUST begin with a verb.**
98 |
99 | **New functions SHOULD have names which describe the purpose, arguments, and return value of the function as explicitly as possible.**
100 |
101 | Start all functions with get..., is..., does..., update... etc. as appropriate. Ideally a function name will explain basically what arguments it requires and what it does or what it returns. This is not always possible, but if you find it hard to craft an appropriate name it might be a sign that the function does too much and should be split.
102 |
103 | For example, consider a function which handles a WordPress shortcode (the second argument of the `add_shortcode()` function). Assuming the shortcode is `foobar`, then what should we call the handler function?
104 |
105 | We might call it `foobar_shortcode`, but that doesn't really say what it does.
106 |
107 | `process_foobar_shortcode` is better, but still ambiguous about what the function returns.
108 |
109 | `get_markup_from_foobar_shortcode` is great because it tells us what the input and the output will be and what the function does.
110 |
111 | ## Function size
112 |
113 | **New functions SHOULD be fewer than 40 lines, excluding comments and whitespace.**
114 |
115 | Long functions contain code that cannot be seen all at once, and therefore often require scrolling up and down in order to follow the flow of execution. In this way they tend to resemble a program itself, and can hide bugs using the same patterns that hide bugs in code which does not use functions at all.
116 |
117 | For one example, a 50-line function which uses the value of the variable `$foo` on line 35 means that a developer might need to scroll up to line 1 to find its definition. If the definition is changed or removed, or the variable modified between the definition and usage, bugs can easily appear. This can be helped by using linters, but readability is still sacrificed.
118 |
119 | The code in functions naturally will grow over time, but as it does so it is important to reconsider if any of the function's parts deserve to be moved into their own functions. Helper functions or private methods are excellent ways to do this.
120 |
121 | ## Array functions
122 |
123 | **New code SHOULD use PHP array functions when possible to clarify the purpose of a loop.**
124 |
125 | Using `foreach` is convenient, but can hide the purpose of a loop. This is subjective, and so it's hard to have a hard rule, but in general it's worth considering what you actually want to do with a loop and see if it's possible to make its meaning explicit by using an array function (`array_map`, `array_filter`, `array_reduce`, etc.).
126 |
127 | If a loop does multiple things, then we must consider if it's worthwhile to split it into multiple loops, each which only do one thing. This might at first seem less efficient, but in many cases the arrays in question are quite small and multiple loops will increase readability much more than they will affect performance.
128 |
129 | So why does `foreach` exist in the first place? In olden times program execution flowed from one statement to the next. To repeat a block of statements more than once you’d just fiddle with the program counter manually using goto, and this was the way of the world.
130 |
131 | The problem with goto is that it is a blunt instrument. It masks intent. The semantics of goto are “jump to line N and continue with the current state”. But in practice we don’t often really want to jump to an arbitrary line. Maybe we want to branch if some condition is met, or repeat a block of code a fixed number of times, or abandon a block of code if some condition is met. Thus control structures like if, while, switch, try/catch, and foreach were born. They provide a more precise vocabulary for expressing intent. Which, as a bonus, is easier to optimize, since the compiler/interpreter can make stronger assumptions about what the program means.
132 |
133 | Specifically, foreach expresses the intent “repeat this block of code for each entry of an array”. That sounds a lot like what array map and reduce do except that they are more precise. The intent of array_map is “apply this function to each item in an array and preserve the array shape”, while the intent of reduce is “consume the items in this array to get a summary value”.
134 |
135 | ## Side effects in files
136 |
137 | **New code MUST NOT have side effects in class constructors.**
138 |
139 | **New PHP files MUST NOT have side effects outside of a function.**
140 |
141 | Class constructors are meant to initialize a new object, setting default values and preparing any data which was passed in as a dependency. They also happen to be a "free" function call which happens when the class is instantiated, which means that when a class's purpose is simple, they're often used to start doing what that class is designed to do.
142 |
143 | Because class constructors are typically called explicitly with the `new` keyword and the class name, this latter pattern is effectively using the class constructor as a global or static function. If the function has side effects, we create implicit and tightly coupled dependencies between the instantiating code and the side effects.
144 |
145 | When writing tests, we might want to avoid side effects (like Slack messages or database writing) but if they are in a constructor we may not know about them, and we may not be able to mock them. Even worse, any code which creates the class will probably not expect them either.
146 |
147 | If a class has only one purpose, it's best to create a single instance method, like `run()` or `activate()` (ideally one that actually describes the purpose of the function) and use that to activate the side effects.
148 |
149 | Even more challenging than side effects in a constructor is when just requiring a PHP file performs side effects. This is almost always unexpected and can be a real challenge when testing. File imports should be a totally pure operation.
150 |
151 | ## Array shorthand
152 |
153 | **New code MUST NOT use `array()` to create array primitives; use the `[]` shorthand syntax instead.**
154 |
155 | This is just for the sake of consistency. The shorthand is fewer characters to type and is commonly used outside of code that must support PHP < 5.4.
156 |
157 | ## Yoda conditions
158 |
159 | **New code MAY use Yoda conditions, but it is not required.**
160 |
161 | **New code MUST NOT use an assignment inside a condition without also including an explicit comparison operator.**
162 |
163 | This replaces the [Yoda conditions rule](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#yoda-conditions) of the WordPress coding guidelines.
164 |
165 | Yoda conditions are not as natural to write, and in many cases are unnecessary to protect against accidental assignment. A more accurate protection is to require explicit comparisons inside conditional statements.
166 |
167 | In PHP, an assignment expression evaluates to the the value of the variable being assigned. Secondly, a condition expression without an explicit comparator checks for the "truthiness" of the expression. This allows for the common pattern of:
168 |
169 | ```php
170 | if ($foo = getFoo())
171 | ```
172 |
173 | This pattern is a shortcut for:
174 |
175 | ```php
176 | $foo = getFoo();
177 | if ($foo == true)
178 | ```
179 |
180 | There is nothing wrong with this shortcut, but Yoda conditions were introduced to protect against the case where this was actually intended to be:
181 |
182 | ```php
183 | if($foo == getFoo())
184 | ```
185 |
186 | To solve this problem and still allow the shortcut, we can require that any condition that has an assignment must also have a comparator. This makes the shortcut explicit and can easily be checked by a linter. So the above shortcut must now be written as:
187 |
188 | ```php
189 | if(($foo = getFoo()) == true)
190 | ```
191 |
192 | ## Inheritance
193 |
194 | **New code SHOULD NOT use inheritance when sharing methods is the only purpose.**
195 |
196 | **New code SHOULD limit the depth of inheritance to no more than one level.**
197 |
198 | Class inheritance is a mechanism for sharing code, but it also implies identity. If the purpose of inheritance is just to bring in helper functions or pure functions, then inheritance is probably not the best method. In these cases, the saying "composition over inheritance" becomes relevant; it's possible to make external functions available to a class by injecting instances of other classes.
199 |
200 | In the case of PHP, this applies both to direct inheritance with the `extends` keyword and mixin inheritance with the `trait` and `use` keywords.
201 |
202 | This is not to say that inheritance is bad. It's just that it's easy to reach for it as a code sharing mechanism when it is not appropriate.
203 |
204 | When inheritance is appropriately used, it's important to use it carefully because it can greatly increase the cognitive load of reading the code.
205 |
206 | If a class inherits from another class, which itself inherits from a third class, then it starts to become hard to keep track of behavior and where that behavior is defined.
207 |
208 | For example, if you were to read the following class, could you guess where the method `getData()` is defined?
209 |
210 | ```php
211 | class MyClass extends ClassC {
212 | use ClassB;
213 | public function doSomething() {
214 | echo $this->getData();
215 | }
216 | }
217 | ```
218 |
219 | It could be either `ClassC` or `ClassB`. But what if `ClassC` inherits from `ClassA`? In that case it could be in `ClassA`. But that's only if `ClassC` does not override the method; and if it does override the method and calls `parent::getData`, then we have to consider `ClassA` anyway.
220 |
221 | Occasionally this sort of inheritance is necessary, but more often it is a form of coupling classes together that will cause problems later. In effect, calling an inherited method is a form of tight coupling because the method creates a dependency between two classes that cannot be easily mocked.
222 |
223 | ## Passing IDs
224 |
225 | **New code SHOULD pass complete objects rather than database IDs as function arguments.**
226 |
227 | A common WordPress pattern is to pass a site ID or a user ID into a function that manipulates site or user data; then that function will query the database to get the data it needs to use.
228 |
229 | The problem with this pattern is that it turns what might be a pure data-manipulation function into a non-pure function with a dependency on the database. Pure functions are much easier to test, easier to move around, and often easier to understand. Passing an ID also may cause a single chain of function calls to make the same database call many times, once for each function which needs access to the data, leading to large performance issues.
230 |
231 | It's often possible to fetch the data from the database first (possibly in a separate function) and then inject the entire object into the function that manipulates it. This makes it so that the functions each do only one thing, increasing readability and decreasing dependencies.
232 |
233 | ## Composition Roots
234 |
235 | **New code SHOULD NOT instantiate objects except for value objects.**
236 |
237 | **New code SHOULD instantiate objects in as few places as possible.**
238 |
239 | Dependency injection of classes means creating instances outside of the class that needs it, and passing the instance in as an argument. This decouples the class from its dependencies by moving the responsibility up one level. In the case of classes using classes which use other classes, this becomes a tree of dependency injection.
240 |
241 | At the top of a such a tree of injection is a "root" class which kicks off the whole chain. This class must create all the dependencies used by the functions it calls. This is called the "composition root". In real-world code there is often multiple such roots and sometimes the distinction isn't entirely clear, but attempting to keep the number of roots as small as possible makes it easier to alter dependencies and to find where classes are defined.
242 |
243 | When a class is instantiated multiple places, it must have all its dependencies provided in each of those places. If one of those dependencies changes, or a new one is added, it means finding all the places where the instances are created and changing them all. This is risky and time-consuming.
244 |
245 | Instead, if a single function is used to instantiate a class (typically a static function called a "factory"), then it becomes possible to just make the change in one place. If a new configuration of dependencies is desired, it's possible to just create a new factory.
246 |
247 | This does not apply to "value objects" whose sole purpose is to represent some data type and have no dependencies themselves.
248 |
249 | ## Newlines
250 |
251 | **New code MUST NOT have more than one adjacent blank line.**
252 |
253 | Whitespace is useful for separating logical sections of code, but excess whitespace takes up too much of the screen. One empty line is usually sufficient to separate sections of code.
254 |
255 | ## Spacing
256 |
257 | **New code SHOULD NOT use whitespace to align assignments or associative arrays.**
258 |
259 | Assignment alignment looks nice sometimes, but it vastly complicates writing and modifying code. If there are twenty adjacent assignments, and one of them gets longer than the others, it requires the developer to go back and adjust the spacing of all the other lines. It's also not always consistent since different typeface and displays will have different visual widths of spaces and other characters.
260 |
261 | ## Variable Functions
262 |
263 | **New code SHOULD NOT call Variable Functions.**
264 |
265 | Having variable function names prevents easily tracing the usage and definition of a function. If a function signature needs to be changed or removed, for example, a developer would typically search the code base for uses of that function name. With variable functions, a function name could be created by joining strings together or otherwise manipulating a string, making it nearly impossible to find that use. Even if the string is unmodified, it may be defined somewhere far away from the place where it is called, again making it hard to trace its use. Lastly, with a function name as a string, it's possible for the string to be accidentally modified or to be set to something unexpected, potentially causing a fatal error.
266 |
267 | Instead, we can use a mapping function to transform a string into a hard-coded function call. For example, here are three ways to call the function stored in `$myFunction`; notice how the third option actually has the function name in the code where it is called.
268 |
269 | This one uses `call_user_func`.
270 |
271 | ```php
272 | call_user_func($myFunction, 'hello');
273 | ```
274 |
275 | The next one uses the new syntax.
276 |
277 | ```php
278 | $myFunction('hello');
279 | ```
280 |
281 | The following version actually does not call a variable function at all.
282 |
283 | ```php
284 | switch($myFunction) {
285 | case 'speak':
286 | speak('hello');
287 | break;
288 | }
289 | ```
290 |
291 | ## Boolean arguments
292 |
293 | **New code SHOULD NOT use boolean arguments.**
294 |
295 | Since function arguments are not named in PHP, there is no way to know what the meaning of an argument from the call-site. For example, `process_data( true );` does not give us any clue what `true` is referring to. This can be mitigated by first putting the boolean argument in a variable (eg: `process_data( $strictly );`), but even better is passing a constant or string argument (eg: `process_data( 'strictly' );`).
296 |
297 | ## Naming Conventions
298 |
299 | **New code MUST use snake-case for variable and function names.**
300 |
301 | **New files MUST NOT be prefixed with "class-".**
302 |
303 | We should follow the [WordPress coding standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions) for naming with the following exception:
304 |
305 | Class files will not be prefixed with `class-`. This is unnecessary if a class is well-named.
306 |
307 | ## Namespaces
308 |
309 | **Namespaces SHOULD be represented in the file structure.**
310 |
311 | **Namespaces SHOULD follow class naming guidelines.**
312 |
313 | To make it easier to find files, namespaces should more-or-less mirror file paths. Namespaced directories and files should be kebab-case.
314 |
315 | Namespaces (in code) should follow class naming conventions, with an underscore if the namespace has multiple words.
316 |
317 | ## Classes
318 |
319 | **Class files SHOULD only ever contain one class.**
320 |
321 | **Class files SHOULD NOT contain any functions outside of the class.**
322 |
323 | ## Functions outside of classes
324 |
325 | **Functions that do not make sense in a class MUST go in a namespace.**
326 |
327 | **Functions in a namespace should go in a file with same name as the namespace.**
328 |
329 | For example, `receipts/receipts.php` for functions in the `Receipts` namespace.
330 |
331 | ## Database access
332 |
333 | **All database queries MUST check if the query failed.**
334 |
335 | Never assume that a database query succeeds, or that it contains the data requested. There are many cases which can cause a query to fail.
336 |
337 | Use a wrapper function like `Db\throw_on_wpdb_error( $wpdb->query( ... ) )` to check if a database query failed, or a Transaction wrapper like `Db\Transaction`.
338 |
339 | ## Value Objects
340 |
341 | **Value objects SHOULD NOT have any logic or methods except accessors.**
342 |
343 | When creating objects to represent data values, those objects should have public properties and no methods, unless methods are necessary to provide read-only access to a property. If any processing is required on such an object, that processing should be placed in a different class whose purpose is to manipulate the value object.
344 |
345 | If value objects gain methods, they become more than just values and their purpose becomes less clear. They can also quickly become cluttered since there are often an infinite number of operations that might be performed on a value. Keeping the operations in separate places allows those operations to be organized using classes and namespaces. The value classes themselves should ideally remain as simple as a string or an integer. That way they are also easy to serialize or compare if needed.
346 |
347 | ## Database query return values
348 |
349 | **Database queries MUST explicitly set values of a value object.**
350 |
351 | Database calls can return an object or an array, and those often have sub-properties which are either objects or arrays themselves. This makes consistency among code complicated because there is no common way for that data to be represented. It also means that it's not clear what properties are expected or available, instead putting that responsibility on the database; the developer must then assume that the data they need is present (an implicit dependency) or constantly guard against missing properties. It's safer for everyone if the query explicitly assigns the results to object properties, ideally of a custom value object, even if that means updating those assignments when the database schema changes.
352 |
353 | ## Clear Public API
354 |
355 | **New code SHOULD be divided into modules.**
356 |
357 | **New code SHOULD NOT access functions or classes outside the public API of another module.**
358 |
359 | **New functions SHOULD use namespacing to make it clear if they are part of a module's public API.**
360 |
361 | Code should be divided into modules of responsibility, where each module has one purpose. Each code module (the definition of which is subjective depending on the situation) should have a set of functions and/or classes which are intended to be the interface to that module from other modules. Modules should only use the public API of other modules.
362 |
363 | Sometimes implementation details of a module can be hidden using private functions in a class, but not always. Therefore each module should have a clear namespace for its public API so that developers know where to look to use that module and avoid accidentally using code which is subject to change.
364 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Automattic
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/NeutronStandard/SniffHelpers.php:
--------------------------------------------------------------------------------
1 | getTokens();
11 | $nextNonWhitespacePtr = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true, null, false);
12 | // if the next non-whitespace token is not a paren, then this is not a function call
13 | if ($tokens[$nextNonWhitespacePtr]['type'] !== 'T_OPEN_PARENTHESIS') {
14 | return false;
15 | }
16 | // if the previous non-whitespace token is a function, then this is not a function call
17 | $prevNonWhitespacePtr = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true, null, false);
18 | if ($tokens[$prevNonWhitespacePtr]['type'] === 'T_FUNCTION') {
19 | return false;
20 | }
21 | return true;
22 | }
23 |
24 | // From https://stackoverflow.com/questions/619610/whats-the-most-efficient-test-of-whether-a-php-string-ends-with-another-string
25 | public function doesStringEndWith(string $string, string $test): bool {
26 | $strlen = strlen($string);
27 | $testlen = strlen($test);
28 | if ($testlen > $strlen) {
29 | return false;
30 | }
31 | return substr_compare($string, $test, $strlen - $testlen, $testlen) === 0;
32 | }
33 |
34 | public function getNextNonWhitespace(File $phpcsFile, $stackPtr) {
35 | $tokens = $phpcsFile->getTokens();
36 | $nextNonWhitespacePtr = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true, null, true);
37 | return $nextNonWhitespacePtr ? $tokens[$nextNonWhitespacePtr] : null;
38 | }
39 |
40 | public function getNextNewlinePtr(File $phpcsFile, $stackPtr) {
41 | return $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, false, "\n");
42 | }
43 |
44 | public function getArgumentTypePtr(File $phpcsFile, $stackPtr) {
45 | $ignoredTypes = [
46 | T_WHITESPACE,
47 | T_ELLIPSIS,
48 | ];
49 | $openParenPtr = $phpcsFile->findPrevious(T_OPEN_PARENTHESIS, $stackPtr - 1, null, false);
50 | if (! $openParenPtr) {
51 | return false;
52 | }
53 | return $phpcsFile->findPrevious($ignoredTypes, $stackPtr - 1, $openParenPtr, true);
54 | }
55 |
56 | public function isReturnValueVoid(File $phpcsFile, $stackPtr) {
57 | $tokens = $phpcsFile->getTokens();
58 | if (! in_array($tokens[$stackPtr]['code'], [T_RETURN, T_YIELD], false)) {
59 | return false;
60 | }
61 | $returnValue = $this->getNextNonWhitespace($phpcsFile, $stackPtr);
62 | return ! $returnValue || $returnValue['code'] === 'PHPCS_T_SEMICOLON';
63 | }
64 |
65 | public function getNextReturnTypePtr(File $phpcsFile, $stackPtr) {
66 | $startOfFunctionPtr = $this->getStartOfFunctionPtr($phpcsFile, $stackPtr);
67 | $colonPtr = $phpcsFile->findNext(T_COLON, $stackPtr, $startOfFunctionPtr);
68 | if (! $colonPtr) {
69 | return false;
70 | }
71 | $endOfTypePtr = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET, T_SEMICOLON], $colonPtr + 1);
72 | if (! $endOfTypePtr) {
73 | throw new \Exception('Found colon for return type but no end-of-line');
74 | }
75 | return $phpcsFile->findPrevious([T_WHITESPACE], $endOfTypePtr - 1, $colonPtr, true);
76 | }
77 |
78 | public function getNextSemicolonPtr(File $phpcsFile, $stackPtr) {
79 | return $phpcsFile->findNext(T_SEMICOLON, $stackPtr + 1);
80 | }
81 |
82 | public function getEndOfFunctionPtr(File $phpcsFile, $stackPtr) {
83 | $tokens = $phpcsFile->getTokens();
84 | if ($this->isFunctionJustSignature($phpcsFile, $stackPtr)) {
85 | return $this->getNextSemicolonPtr($phpcsFile, $stackPtr);
86 | }
87 | $openFunctionBracketPtr = $phpcsFile->findNext(T_OPEN_CURLY_BRACKET, $stackPtr + 1);
88 | return $openFunctionBracketPtr && isset($tokens[$openFunctionBracketPtr]['bracket_closer'])
89 | ? $tokens[$openFunctionBracketPtr]['bracket_closer']
90 | : $this->getNextSemicolonPtr($phpcsFile, $stackPtr);
91 | }
92 |
93 | public function getStartOfFunctionPtr(File $phpcsFile, $stackPtr) {
94 | $openFunctionBracketPtr = $phpcsFile->findNext(T_OPEN_CURLY_BRACKET, $stackPtr + 1);
95 | $nextSemicolonPtr = $this->getNextSemicolonPtr($phpcsFile, $stackPtr);
96 | if ($openFunctionBracketPtr && $nextSemicolonPtr && $openFunctionBracketPtr > $nextSemicolonPtr) {
97 | return $nextSemicolonPtr;
98 | }
99 | return $openFunctionBracketPtr
100 | ? $openFunctionBracketPtr + 1
101 | : $this->getEndOfFunctionPtr($phpcsFile, $stackPtr);
102 | }
103 |
104 | public function isFunctionJustSignature(File $phpcsFile, $stackPtr) {
105 | $openFunctionBracketPtr = $phpcsFile->findNext(T_OPEN_CURLY_BRACKET, $stackPtr + 1);
106 | $nextSemicolonPtr = $this->getNextSemicolonPtr($phpcsFile, $stackPtr);
107 | if ($openFunctionBracketPtr && $nextSemicolonPtr && $openFunctionBracketPtr > $nextSemicolonPtr) {
108 | return true;
109 | }
110 | return ! $openFunctionBracketPtr;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/Arrays/DisallowLongformArraySniff.php:
--------------------------------------------------------------------------------
1 | getTokens();
16 | $functionName = $tokens[$stackPtr]['content'];
17 | $helper = new SniffHelpers();
18 | if ($functionName === 'array' && $helper->isFunctionCall($phpcsFile, $stackPtr)) {
19 | $error = 'Longform array is not allowed';
20 | $shouldFix = $phpcsFile->addFixableError($error, $stackPtr, 'LongformArray');
21 | if ($shouldFix) {
22 | $this->fixTokens($phpcsFile, $stackPtr);
23 | }
24 | }
25 | }
26 |
27 | private function fixTokens(File $phpcsFile, $stackPtr) {
28 | $tokens = $phpcsFile->getTokens();
29 | $openParenPtr = $tokens[$stackPtr]['parenthesis_opener'];
30 | $closeParenPtr = $tokens[$stackPtr]['parenthesis_closer'];
31 | $phpcsFile->fixer->beginChangeset();
32 | $phpcsFile->fixer->replaceToken($stackPtr, '');
33 | $phpcsFile->fixer->replaceToken($openParenPtr, '[');
34 | $phpcsFile->fixer->replaceToken($closeParenPtr, ']');
35 | $phpcsFile->fixer->endChangeset();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/AssignAlign/DisallowAssignAlignSniff.php:
--------------------------------------------------------------------------------
1 | getTokens();
16 | // If the next non-whitespace after multiples spaces is an equal sign or double arrow, mark a warning
17 | if (strlen($tokens[$stackPtr]['content']) <= 1) {
18 | return;
19 | }
20 | $nextNonWhitespacePtr = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true, null, false);
21 | if ($nextNonWhitespacePtr !== false && $this->isTokenAnAssignment($tokens[$nextNonWhitespacePtr])) {
22 | $error = 'Assignment alignment is not allowed';
23 | $shouldFix = $phpcsFile->addFixableWarning($error, $stackPtr, 'Aligned');
24 | if ($shouldFix) {
25 | $this->fixTokens($phpcsFile, $stackPtr);
26 | }
27 | }
28 | }
29 |
30 | private function isTokenAnAssignment($token) {
31 | $assignOperators = [
32 | T_EQUAL,
33 | T_DOUBLE_ARROW,
34 | ];
35 | return in_array($token['code'], $assignOperators, true);
36 | }
37 |
38 | private function fixTokens(File $phpcsFile, $stackPtr) {
39 | $phpcsFile->fixer->beginChangeset();
40 | $phpcsFile->fixer->replaceToken($stackPtr, ' ');
41 | $phpcsFile->fixer->endChangeset();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/Conditions/DisallowConditionAssignWithoutConditionalSniff.php:
--------------------------------------------------------------------------------
1 | getTokens();
15 | // if previous non-whitespace token is `T_IF`
16 | $prevNonWhitespacePtr = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true, null, false);
17 | if ($tokens[$prevNonWhitespacePtr]['type'] !== 'T_IF') {
18 | return;
19 | }
20 | // if there is a T_EQUAL after this before the end of statement
21 | $endOfStatementPtr = $phpcsFile->findEndOfStatement($stackPtr + 1);
22 | $nextAssignPtr = $phpcsFile->findNext(T_EQUAL, $stackPtr + 1, $endOfStatementPtr, false, null, false);
23 | if (! $nextAssignPtr) {
24 | return;
25 | }
26 | // if there is not a T_IS_EQUAL (or any other comparator!) before the end of statement
27 | $comparators = [
28 | T_IS_EQUAL,
29 | T_IS_NOT_EQUAL,
30 | T_IS_IDENTICAL,
31 | T_IS_NOT_IDENTICAL,
32 | T_IS_SMALLER_OR_EQUAL,
33 | T_IS_GREATER_OR_EQUAL,
34 | T_LESS_THAN,
35 | T_GREATER_THAN,
36 | T_SPACESHIP,
37 | ];
38 | $nextEqualPtr = $phpcsFile->findNext($comparators, $stackPtr + 1, $endOfStatementPtr, false, null, false);
39 | if ($nextEqualPtr) {
40 | return;
41 | }
42 | // mark an error
43 | $error = 'Conditions that contain assignments must have explicit comparators';
44 | $phpcsFile->addError($error, $stackPtr, 'ConditionAssignWithoutConditional');
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/Constants/DisallowDefineSniff.php:
--------------------------------------------------------------------------------
1 | isFunctionCall($phpcsFile, $stackPtr)) {
17 | return;
18 | }
19 | $tokens = $phpcsFile->getTokens();
20 | $functionName = $tokens[$stackPtr]['content'];
21 | if ($functionName === 'define') {
22 | $error = 'Define is not allowed';
23 | $phpcsFile->addError($error, $stackPtr, 'Define');
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/Extract/DisallowExtractSniff.php:
--------------------------------------------------------------------------------
1 | getTokens();
16 | $functionName = $tokens[$stackPtr]['content'];
17 | $helper = new SniffHelpers();
18 | if ($functionName === 'extract' && $helper->isFunctionCall($phpcsFile, $stackPtr)) {
19 | $error = 'Extract is not allowed';
20 | $phpcsFile->addError($error, $stackPtr, 'Extract');
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/Functions/DisallowCallUserFuncSniff.php:
--------------------------------------------------------------------------------
1 | getTokens();
16 | $functionName = $tokens[$stackPtr]['content'];
17 | $helper = new SniffHelpers();
18 | $disallowedFunctions = [
19 | 'call_user_func',
20 | 'call_user_func_array',
21 | ];
22 | if (in_array($functionName, $disallowedFunctions) && $helper->isFunctionCall($phpcsFile, $stackPtr)) {
23 | $error = 'call_user_func and call_user_func_array are not allowed';
24 | $phpcsFile->addError($error, $stackPtr, 'CallUserFunc');
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/Functions/LongFunctionSniff.php:
--------------------------------------------------------------------------------
1 | isFunctionJustSignature($phpcsFile, $stackPtr)) {
19 | return;
20 | }
21 | $tokens = $phpcsFile->getTokens();
22 | $startOfFunctionPtr = $helper->getStartOfFunctionPtr(
23 | $phpcsFile,
24 | $stackPtr
25 | );
26 | $endOfFunctionPtr = $helper->getEndOfFunctionPtr($phpcsFile, $stackPtr);
27 | $newlineCount = 0;
28 | $commentTokens = [
29 | T_DOC_COMMENT_OPEN_TAG,
30 | T_DOC_COMMENT_CLOSE_TAG,
31 | T_DOC_COMMENT_STRING,
32 | T_COMMENT,
33 | T_DOC_COMMENT_STAR,
34 | T_DOC_COMMENT_WHITESPACE,
35 | ];
36 | $newlineContainingTokens = [T_WHITESPACE, T_COMMENT];
37 | $currentLinePtr =
38 | $phpcsFile->findNext(
39 | T_WHITESPACE,
40 | $startOfFunctionPtr,
41 | $endOfFunctionPtr,
42 | false,
43 | "\n"
44 | ) + 2;
45 | $foundNonComment = false;
46 | for ($index = $currentLinePtr; $index < $endOfFunctionPtr; $index++) {
47 | $token = $tokens[$index];
48 | if (!in_array($token['code'], $commentTokens)) {
49 | if (
50 | $token['code'] !== T_WHITESPACE ||
51 | $token['content'] !== "\n"
52 | ) {
53 | $foundNonComment = true;
54 | }
55 | }
56 | if (
57 | in_array($token['code'], $newlineContainingTokens) &&
58 | $helper->doesStringEndWith($token['content'], "\n")
59 | ) {
60 | if ($foundNonComment) {
61 | $newlineCount++;
62 | }
63 | $foundNonComment = false;
64 | }
65 | }
66 | if (intval($newlineCount) > $this->maxFunctionLines) {
67 | $error = "Function is longer than {$this->maxFunctionLines} lines";
68 | $phpcsFile->addWarning($error, $stackPtr, 'LongFunction');
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/Functions/TypeHintSniff.php:
--------------------------------------------------------------------------------
1 | checkForMissingArgumentHints($phpcsFile, $stackPtr, $helper);
17 | $this->checkForMissingReturnHints($phpcsFile, $stackPtr, $helper);
18 | }
19 |
20 | private function checkForMissingArgumentHints(File $phpcsFile, $stackPtr, SniffHelpers $helper) {
21 | $tokens = $phpcsFile->getTokens();
22 | $openParenPtr = $tokens[$stackPtr]['parenthesis_opener'];
23 | $closeParenPtr = $tokens[$stackPtr]['parenthesis_closer'];
24 | $hintTypes = [
25 | T_STRING,
26 | T_CALLABLE,
27 | T_SELF,
28 | ];
29 | // Support for phpcs < 3.3; see https://github.com/Automattic/phpcs-neutron-standard/issues/62
30 | if (defined('T_ARRAY_HINT')) {
31 | $hintTypes[] = T_ARRAY_HINT;
32 | }
33 |
34 | for ($ptr = ($openParenPtr + 1); $ptr < $closeParenPtr; $ptr++) {
35 | if ($tokens[$ptr]['code'] === T_VARIABLE) {
36 | $tokenBeforePtr = $helper->getArgumentTypePtr($phpcsFile, $ptr);
37 | $tokenBefore = $tokens[$tokenBeforePtr];
38 | if (!$tokenBeforePtr || !in_array($tokenBefore['code'], $hintTypes, true)) {
39 | $error = 'Argument type is missing';
40 | $phpcsFile->addWarning($error, $stackPtr, 'NoArgumentType');
41 | }
42 | }
43 | }
44 | }
45 |
46 | private function checkForMissingReturnHints(File $phpcsFile, $stackPtr, SniffHelpers $helper) {
47 | $tokens = $phpcsFile->getTokens();
48 | if ($helper->isFunctionJustSignature($phpcsFile, $stackPtr)) {
49 | return;
50 | }
51 | $endOfFunctionPtr = $helper->getEndOfFunctionPtr($phpcsFile, $stackPtr);
52 | $startOfFunctionPtr = $helper->getStartOfFunctionPtr($phpcsFile, $stackPtr);
53 | $returnTypePtr = $helper->getNextReturnTypePtr($phpcsFile, $stackPtr);
54 | $returnType = $tokens[$returnTypePtr];
55 |
56 | $colonPtr = $phpcsFile->findNext(T_COLON, $stackPtr, $startOfFunctionPtr);
57 | if ($colonPtr) {
58 | if ($tokens[$colonPtr - 1]['type'] !== 'T_CLOSE_PARENTHESIS') {
59 | $phpcsFile->addError(
60 | 'Return type colon should be right after closing function parenthesis',
61 | $colonPtr,
62 | 'ExtraSpace'
63 | );
64 | }
65 | if ($tokens[$colonPtr + 1]['type'] !== 'T_WHITESPACE') {
66 | $phpcsFile->addError('Missing space before return type', $colonPtr, 'MissingSpace');
67 | }
68 | if ($tokens[$returnTypePtr+1]['type'] !== 'T_WHITESPACE') {
69 | $phpcsFile->addError('Missing space after return type', $colonPtr, 'MissingSpace');
70 | }
71 | }
72 |
73 | $nonVoidReturnCount = 0;
74 | $voidReturnCount = 0;
75 | $scopeClosers = [];
76 | for ($ptr = $startOfFunctionPtr; $ptr < $endOfFunctionPtr; $ptr++) {
77 | $token = $tokens[$ptr];
78 | if (!empty($scopeClosers) && $ptr === $scopeClosers[0]) {
79 | array_shift($scopeClosers);
80 | }
81 | if ($token['code'] === T_CLOSURE) {
82 | array_unshift($scopeClosers, $token['scope_closer']);
83 | }
84 | if (empty($scopeClosers) && in_array($token['code'], [T_RETURN, T_YIELD], true)) {
85 | $helper->isReturnValueVoid($phpcsFile, $ptr) ? $voidReturnCount++ : $nonVoidReturnCount++;
86 | }
87 | }
88 |
89 | $hasNonVoidReturnType = $returnTypePtr && $returnType['content'] !== 'void';
90 | $hasVoidReturnType = $returnTypePtr && $returnType['content'] === 'void';
91 | $hasNoReturnType = ! $returnTypePtr;
92 |
93 | if ($hasNonVoidReturnType
94 | && ($nonVoidReturnCount === 0 || $voidReturnCount > 0)
95 | ) {
96 | $errorMessage = $voidReturnCount > 0
97 | ? 'Return type with void return'
98 | : 'Return type with no return';
99 |
100 | $errorType = $voidReturnCount > 0
101 | ? 'IncorrectVoidReturn'
102 | : 'UnusedReturnType';
103 |
104 | $phpcsFile->addError($errorMessage, $stackPtr, $errorType);
105 | }
106 | if ($hasNoReturnType && $nonVoidReturnCount > 0) {
107 | $error = 'Return type is missing';
108 | $phpcsFile->addWarning($error, $stackPtr, 'NoReturnType');
109 | }
110 | if ($hasVoidReturnType && $nonVoidReturnCount > 0) {
111 | $error = 'Void return type when returning non-void';
112 | $phpcsFile->addError($error, $stackPtr, 'IncorrectVoidReturnType');
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/Functions/VariableFunctionsSniff.php:
--------------------------------------------------------------------------------
1 | getTokens();
16 | $nextNonWhitespacePtr = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true, null, false);
17 | if (! $nextNonWhitespacePtr) {
18 | return;
19 | }
20 | if ($tokens[$nextNonWhitespacePtr]['code'] !== T_OPEN_PARENTHESIS) {
21 | return;
22 | }
23 | $message = 'Variable functions are discouraged';
24 | $phpcsFile->addWarning($message, $stackPtr, 'VariableFunction');
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/Globals/DisallowGlobalFunctionsSniff.php:
--------------------------------------------------------------------------------
1 | getTokens();
15 | $currentToken = $tokens[$stackPtr];
16 | $namespaceTokenPtr = $phpcsFile->findPrevious(T_NAMESPACE, $stackPtr);
17 | if (! empty($currentToken['conditions'])) {
18 | return;
19 | }
20 | if ($namespaceTokenPtr) {
21 | return;
22 | }
23 | $error = 'Global functions are not allowed';
24 | $phpcsFile->addError($error, $stackPtr, 'GlobalFunctions');
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/MagicMethods/DisallowMagicGetSniff.php:
--------------------------------------------------------------------------------
1 | getDeclarationName($stackPtr);
15 | if ($functionName === '__get') {
16 | $error = 'Magic getters are not allowed';
17 | $phpcsFile->addError($error, $stackPtr, 'MagicGet');
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/MagicMethods/DisallowMagicSerializeSniff.php:
--------------------------------------------------------------------------------
1 | getDeclarationName($stackPtr);
15 | if ($functionName === '__serialize') {
16 | $error = 'Magic serialize is not allowed';
17 | $phpcsFile->addError($error, $stackPtr, 'MagicSerialize');
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/MagicMethods/DisallowMagicSetSniff.php:
--------------------------------------------------------------------------------
1 | getDeclarationName($stackPtr);
15 | if ($functionName === '__set') {
16 | $error = 'Magic setters are not allowed';
17 | $phpcsFile->addError($error, $stackPtr, 'MagicSet');
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/MagicMethods/RiskyMagicMethodSniff.php:
--------------------------------------------------------------------------------
1 | getDeclarationName($stackPtr);
15 | $riskyMagicMethods = [
16 | '__invoke',
17 | '__call',
18 | '__callStatic',
19 | ];
20 | if (in_array($functionName, $riskyMagicMethods)) {
21 | $error = 'Magic methods are discouraged';
22 | $phpcsFile->addWarning($error, $stackPtr, 'RiskyMagicMethod');
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/StrictTypes/RequireStrictTypesSniff.php:
--------------------------------------------------------------------------------
1 | isFirstOpenTag($phpcsFile, $stackPtr)
16 | || $this->isFileOnlyAnInterface($phpcsFile, $stackPtr)
17 | ) {
18 | return;
19 | }
20 | $declarePtr = $this->getNextDeclarePtr($phpcsFile, $stackPtr);
21 | if (! $declarePtr
22 | || ! $this->isDeclareStrictTypes($phpcsFile, $declarePtr)
23 | || ! $this->isDeclareTurnedOn($phpcsFile, $declarePtr)
24 | ) {
25 | $this->addStrictTypeError($phpcsFile, $stackPtr);
26 | }
27 | }
28 |
29 | private function isFirstOpenTag(File $phpcsFile, $stackPtr): bool {
30 | $previousOpenTagPtr = $phpcsFile->findPrevious(T_OPEN_TAG, $stackPtr);
31 | return ! $previousOpenTagPtr;
32 | }
33 |
34 | private function isFileOnlyAnInterface(File $phpcsFile, $stackPtr): bool {
35 | $interfacePtr = $phpcsFile->findNext(T_INTERFACE, $stackPtr);
36 | if (! $interfacePtr) {
37 | return false;
38 | }
39 | $tokens = $phpcsFile->getTokens();
40 | $ignoredTokenTypes = [
41 | T_WHITESPACE,
42 | T_COMMENT,
43 | ];
44 | $skipExpressionTokenTypes = [
45 | T_USE,
46 | T_NAMESPACE,
47 | ];
48 | $helper = new SniffHelpers();
49 | for ($ptr = ($stackPtr + 1); isset($tokens[$ptr]); $ptr++) {
50 | $token = $tokens[$ptr];
51 | if ($token['level'] > 0) {
52 | continue;
53 | }
54 | if ($token['code'] === T_INTERFACE) {
55 | $ptr = $this->getEndOfBlockPtr($phpcsFile, $ptr);
56 | continue;
57 | }
58 | if (isset($token['comment_closer'])) {
59 | $ptr = $token['comment_closer'];
60 | continue;
61 | }
62 | if (in_array($token['code'], $skipExpressionTokenTypes)) {
63 | $ptr = $helper->getNextSemicolonPtr($phpcsFile, $ptr);
64 | continue;
65 | }
66 | if (! in_array($token['code'], $ignoredTokenTypes)) {
67 | return false;
68 | }
69 | }
70 | return true;
71 | }
72 |
73 | private function getEndOfBlockPtr(File $phpcsFile, $stackPtr) {
74 | $tokens = $phpcsFile->getTokens();
75 | return $tokens[$stackPtr]['scope_closer'];
76 | }
77 |
78 | private function isDeclareStrictTypes(File $phpcsFile, $declarePtr): bool {
79 | $tokens = $phpcsFile->getTokens();
80 | $declareStringPtr = $phpcsFile->findNext(T_STRING, $declarePtr, null, false, null, true);
81 | $declareStringToken = $tokens[$declareStringPtr];
82 | return ($declareStringToken && $declareStringToken['content'] === 'strict_types');
83 | }
84 |
85 | private function isDeclareTurnedOn(File $phpcsFile, $declarePtr): bool {
86 | $tokens = $phpcsFile->getTokens();
87 | $declareNumPtr = $phpcsFile->findNext(T_LNUMBER, ($declarePtr + 1), null, false, null, true);
88 | $declareNumToken = $tokens[$declareNumPtr];
89 | return ($declareNumToken && $declareNumToken['content'] === '1');
90 | }
91 |
92 | private function addStrictTypeError(File $phpcsFile, $stackPtr) {
93 | $error = 'File must start with a strict types declaration';
94 | $phpcsFile->addError($error, $stackPtr, 'StrictTypes');
95 | }
96 |
97 | private function getNextDeclarePtr(File $phpcsFile, $stackPtr) {
98 | return $phpcsFile->findNext(T_DECLARE, $stackPtr, null, false, null, true);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/Whitespace/DisallowMultipleNewlinesSniff.php:
--------------------------------------------------------------------------------
1 | getTokens();
16 | if ($tokens[$stackPtr]['content'] !== "\n") {
17 | return;
18 | }
19 | if ($stackPtr < 3) {
20 | return;
21 | }
22 | if ($tokens[$stackPtr - 1]['content'] !== "\n") {
23 | return;
24 | }
25 | if ($tokens[$stackPtr - 2]['content'] !== "\n") {
26 | return;
27 | }
28 | $error = 'Multiple adjacent blank lines are not allowed';
29 | $shouldFix = $phpcsFile->addFixableError($error, $stackPtr, 'MultipleNewlines');
30 | if ($shouldFix) {
31 | $this->fixTokens($phpcsFile, $stackPtr);
32 | }
33 | }
34 |
35 | private function fixTokens(File $phpcsFile, $stackPtr) {
36 | $phpcsFile->fixer->beginChangeset();
37 | $phpcsFile->fixer->replaceToken($stackPtr, '');
38 | $phpcsFile->fixer->endChangeset();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/Whitespace/RequireNewlineBetweenFunctionsSniff.php:
--------------------------------------------------------------------------------
1 | getTokens();
16 | $helper = new SniffHelpers();
17 | if ($helper->isFunctionJustSignature($phpcsFile, $stackPtr)) {
18 | return;
19 | }
20 | $endofFuncPtr = $helper->getEndOfFunctionPtr($phpcsFile, $stackPtr);
21 | if (! $endofFuncPtr) {
22 | return;
23 | }
24 | $endOfLinePtr = $helper->getNextNewlinePtr($phpcsFile, $endofFuncPtr);
25 | if (! $endOfLinePtr || ! isset($tokens[$endOfLinePtr + 1])) {
26 | return;
27 | }
28 | $nextToken = $tokens[$endOfLinePtr + 1];
29 | if (! isset($nextToken['content']) || $nextToken['content'] === "\n") {
30 | return;
31 | }
32 | // Only trigger if the next line contains a function definition
33 | // or a comment
34 | $nextEndOfLinePtr = $helper->getNextNewlinePtr($phpcsFile, $endofFuncPtr + 1);
35 | $forbiddenTypes = [
36 | T_FUNCTION,
37 | T_COMMENT,
38 | ];
39 | $nextForbiddenToken = $phpcsFile->findNext($forbiddenTypes, $endOfLinePtr + 1, null, false, null, true);
40 | if (! $nextForbiddenToken || $nextForbiddenToken > $nextEndOfLinePtr) {
41 | return;
42 | }
43 |
44 | $error = 'Functions must be separated by a blank line';
45 | $shouldFix = $phpcsFile->addFixableError($error, $endofFuncPtr, 'MissingNewline');
46 | if ($shouldFix) {
47 | $this->fixTokens($phpcsFile, $stackPtr);
48 | }
49 | }
50 |
51 | private function fixTokens(File $phpcsFile, $stackPtr) {
52 | $helper = new SniffHelpers();
53 | $endofFuncPtr = $helper->getEndOfFunctionPtr($phpcsFile, $stackPtr);
54 | $phpcsFile->fixer->beginChangeset();
55 | $phpcsFile->fixer->addNewline($endofFuncPtr);
56 | $phpcsFile->fixer->endChangeset();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/NeutronStandard/Sniffs/Whitespace/RequireParenthesisSpacingSniff.php:
--------------------------------------------------------------------------------
1 | getTokens();
16 | $token = $tokens[$stackPtr];
17 | $isBefore = ($token['type'] === 'T_OPEN_PARENTHESIS');
18 | $nextTokenPtr = $isBefore ? $stackPtr + 1 : $stackPtr - 1;
19 | $nextToken = $tokens[$nextTokenPtr];
20 | if (! isset($nextToken['type'])) {
21 | return;
22 | }
23 | $allowedTypes = ['T_WHITESPACE'];
24 | $allowedTypes[] = $isBefore ? 'T_CLOSE_PARENTHESIS' : 'T_OPEN_PARENTHESIS';
25 | if (in_array($nextToken['type'], $allowedTypes, true)) {
26 | return;
27 | }
28 | $error = 'Parenthesis content must be padded by a space';
29 | $shouldFix = $phpcsFile->addFixableError($error, $stackPtr, 'Missing');
30 | if ($shouldFix) {
31 | $this->fixTokens($phpcsFile, $nextTokenPtr, $isBefore);
32 | }
33 | }
34 |
35 | private function fixTokens(File $phpcsFile, $stackPtr, $isBefore) {
36 | $phpcsFile->fixer->beginChangeset();
37 | if ($isBefore) {
38 | $phpcsFile->fixer->addContentBefore($stackPtr, ' ');
39 | } else {
40 | $phpcsFile->fixer->addContent($stackPtr, ' ');
41 | }
42 | $phpcsFile->fixer->endChangeset();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/NeutronStandard/ruleset.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Coding guidelines for Neutron.
4 |
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Neutron PHP Standard
2 |
3 | > **Warning**
4 | >
5 | > **IMPORTANT NOTE:** This project is no longer actively developed. At Automattic we've switched to using the WordPress Coding Standards.
6 |
7 | -----
8 |
9 | These are a set of modern (PHP >7) linting guidelines meant to be applied in addition to the [the WordPress coding standards](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards) for WordPress development. Because of the newer PHP version, it is not suitable for work on Core WordPress, but may be useful for those who are not bound by PHP 5.2.
10 |
11 | These guidelines are being developed primarily for a team within [Automattic](https://automattic.com/), but anyone is free to use them, suggest changes, or report bugs.
12 |
13 | This project is a [phpcs](https://github.com/squizlabs/PHP_CodeSniffer) "standard" (a collection of rules or "sniffs") that can be included in any project.
14 |
15 | ## Installation
16 |
17 | To use these rules in a project which is set up using [composer](https://href.li/?https://getcomposer.org/), we recommend using the [phpcodesniffer-composer-installer library](https://href.li/?https://github.com/DealerDirect/phpcodesniffer-composer-installer) which will automatically use all installed standards in the current project with the composer type `phpcodesniffer-standard` when you run phpcs.
18 |
19 | ```
20 | composer require --dev squizlabs/php_codesniffer dealerdirect/phpcodesniffer-composer-installer
21 | composer require --dev automattic/phpcs-neutron-standard
22 | ```
23 |
24 | If you want this standard, the WordPress standard, the VariableAnalysis standard, and other customizations, you can install the meta-standard [NeutronRuleset](https://github.com/Automattic/phpcs-neutron-ruleset) instead.
25 |
26 | ```
27 | composer require --dev squizlabs/php_codesniffer dealerdirect/phpcodesniffer-composer-installer
28 | composer require --dev automattic/phpcs-neutron-ruleset
29 | ```
30 |
31 | ## Configuration
32 |
33 | When installing sniff standards in a project, you edit a `phpcs.xml` file with the `rule` tag inside the `ruleset` tag. The `ref` attribute of that tag should specify a standard, category, sniff, or error code to enable. It’s also possible to use these tags to disable or modify certain rules. The [official annotated file](https://href.li/?https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml) explains how to do this.
34 |
35 | ```xml
36 |
37 |
38 | My library.
39 |
40 |
41 | ```
42 |
43 | ## Usage
44 |
45 | Most editors have a phpcs plugin available, but you can also run phpcs manually. To run phpcs on a file in your project, just use the command-line as follows (the `-s` causes the sniff code to be shown, which is very important for learning about an error).
46 |
47 | ```
48 | vendor/bin/phpcs -s src/MyProject/MyClass.php
49 | ```
50 |
51 | -----
52 |
53 | # Guidelines
54 |
55 | View the Guidelines [here](./GUIDELINES.md).
56 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "automattic/phpcs-neutron-standard",
3 | "description": "A set of phpcs sniffs for modern php development.",
4 | "type": "phpcodesniffer-standard",
5 | "keywords" : [ "phpcs", "static analysis" ],
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Payton Swick",
10 | "email": "payton@foolord.com"
11 | }
12 | ],
13 | "minimum-stability": "dev",
14 | "prefer-stable": true,
15 | "scripts": {
16 | "test": "./vendor/bin/phpunit --testdox",
17 | "lint": "./vendor/bin/phpcs NeutronStandard"
18 | },
19 | "support" : {
20 | "issues": "https://github.com/sirbrillig/phpcs-neutron-standard/issues",
21 | "wiki" : "https://github.com/sirbrillig/phpcs-neutron-standard/wiki",
22 | "source": "https://github.com/sirbrillig/phpcs-neutron-standard"
23 | },
24 | "config": {
25 | "sort-order": true,
26 | "allow-plugins": {
27 | "dealerdirect/phpcodesniffer-composer-installer": true
28 | }
29 | },
30 | "autoload": {
31 | "psr-4": {
32 | "NeutronStandard\\": "NeutronStandard/"
33 | }
34 | },
35 | "autoload-dev": {
36 | "psr-4": {
37 | "NeutronStandard\\Tests\\": "tests/"
38 | }
39 | },
40 | "require": {
41 | "php": "^7.0 || ^8.0",
42 | "squizlabs/php_codesniffer": "^3.3.0"
43 | },
44 | "require-dev": {
45 | "sirbrillig/phpcs-variable-analysis": "^2.0.1",
46 | "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4 || ^0.5 || ^0.6 || ^0.7",
47 | "phpunit/phpunit": "^5.0 || ^6.5 || ^7.0 || ^8.0"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------