# PHPStan ignores

The `phpstan-ignores` plugin is for a project that also runs PHPStan. It lets a `@phpstan-ignore` comment drop the Mago issues that report the same finding, so the line doesn't need a `@mago-expect` on top.

## Turning it on

The plugin is off by default. Turn it on in `mago.toml`:

```
[analyzer]
plugins = ["phpstan-ignores"]
```

```
// @phpstan-ignore return.type (the caller handles NULL)
return NULL;  // no invalid-return-statement
```

## Comment forms

The plugin reads PHPStan's forms:

- `@phpstan-ignore` with identifiers separated by commas, and an optional reason in parentheses after any of them. It sits at the end of the line, or above it, where it covers the next line of code;
- `@phpstan-ignore-next-line`;
- `@phpstan-ignore-line`.

PHPStan wants that code on the very next line, and calls a comment with anything in between unused. The plugin skips blank lines and other comments, so a `@mago-expect` can sit between the two.

A comment over several lines covers what PHPStan covers. In a `/* */` comment, the line forms count from the comment's last line, and when the comment holds both, it only covers the next line. In a docblock, they count from the line that holds the tag, and a `@phpstan-ignore-next-line` that is the docblock's last tag covers the line after the docblock. A comment that holds a line form doesn't read identifiers from `@phpstan-ignore`.

```
return NULL; /* The caller handles NULL.
  @phpstan-ignore-line */            // covers this line, not the `return`
```

## Which issues a comment drops

An identifier drops only the Mago codes listed for it by the plugin, so `@phpstan-ignore return.type` still lets a missing method on the same line through. The two line forms drop every listed code, and an identifier not listed by the plugin doesn't drop anything.

The list covers PHPStan's identifiers for argument, return and property types, argument counts, undefined symbols, static calls to instance methods, deprecations, always-true and always-false conditions, comparisons, `isset()`, `empty()`, `??` and type checks with a known result, operand types, docblock types, property defaults, unreachable code, missing types, offset access, and the values that a `foreach`, a call, a `match`, a `clone` or a destructuring can't take. It also covers the deprecation identifiers of phpstan-deprecation-rules. [PHPStan identifiers](<https://project.pages.drupalcode.org/mago/phpstan/identifiers/index.md>) lists each identifier with its Mago codes.

**Not dropped:**

- phpstan-drupal's identifiers, which aren't in the list;
- the `drupal/` and `phpunit/` checks. They report after Mago runs the plugin, so a comment never drops one of them;
- the issues of linter rules. See [Linter rules](<#linter-rules>) for the ones that port a PHPStan check.

An issue counts as being on the covered line when it starts there. Unreachable code is the one exception. PHPStan reports the first unreachable statement of a block, and Mago reports every one of them, so a comment that ignores `deadCode.unreachable` also drops Mago's `unevaluated-code` on the lines after it, up to the end of its block. The block ends at its closing brace or at the `endif`, `endforeach` or other end keyword of the alternative syntax. An alternative syntax `else:` or `elseif`, and the next `case` or `default` label of a `switch`, end it too. When the covered line holds a `return`, `throw`, `exit`, `continue` or `break` statement, the run starts at the statement after it. So on a line such as `if ($flag): return; echo 'a';`, the run starts at the `echo` and ends with the `if` block.

```
$this->markTestSkipped('Blocked by a core issue.');
// @phpstan-ignore deadCode.unreachable
$result = $this->build();          // no unevaluated-code
$this->assertSame('x', $result);   // none here either
```

## Pragmas

Mago matches `@mago-expect` pragmas before the plugin runs. So a pragma next to a `@phpstan-ignore` for the same issue still counts as used: it can be removed, but Mago doesn't point it out.

## Linter rules

Some linter rules port a PHPStan check, such as phpstan-drupal's `RenderCallbackRule`. The plugin runs in the analyzer, so it can't drop their issues. With the `--phpstan-ignores` worker argument, these rules read `@phpstan-ignore` comments themselves. It's off by default, as the plugin is, and `drupal-core.mago.toml` turns it on. See [The worker](<https://project.pages.drupalcode.org/mago/setup/worker/index.md>).

The comments cover the same lines as for the plugin. `@phpstan-ignore-line` and `@phpstan-ignore-next-line` drop the issues of these rules, and an identifier drops the issues of the rule listed with it:

- [`drupal/discouraged-function`](<https://project.pages.drupalcode.org/mago/rules/phpstan-drupal/index.md#drupaldiscouraged-function>): `discouragedFunctions.discouragedFunctionCalled`
- [`drupal/render-callback`](<https://project.pages.drupalcode.org/mago/rules/phpstan-drupal/index.md#drupalrender-callback>): `renderCallback.expectsArrayOfCallbacks`, `renderCallback.expectsCallableArrayWithArguments`, `renderCallback.nonTrustedCallback`
- [`drupal/symfony-yaml-parse`](<https://project.pages.drupalcode.org/mago/rules/phpstan-drupal/index.md#drupalsymfony-yaml-parse>): `drupal.symfonyYamlParse`
- [`drupal/test-class-suffix`](<https://project.pages.drupalcode.org/mago/rules/naming-and-imports/index.md#drupaltest-class-suffix>): `nonAbstractTestClass.NameNoSuffixTest`
- [`drupal/todo-current-issue`](<https://project.pages.drupalcode.org/mago/rules/phpstan-drupal/index.md#drupaltodo-current-issue>): `drupal.todoCurrentIssue`
- [`drupal/weak-hash`](<https://project.pages.drupalcode.org/mago/rules/bugs-and-security/index.md#drupalweak-hash>): `disallowed.function`

`drupal/weak-hash` ports the disallowed calls of core's `phpstan.neon.dist`, which phpstan-disallowed-calls reports as `disallowed.function`. The other rules don't read `@phpstan-ignore` comments. Unlike for the plugin, the issue is dropped before Mago sees it, so a `@mago-expect` for it is reported as unused.
