Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace inline docblock declarations with assertions #47

Merged
merged 5 commits into from
Apr 30, 2019

Conversation

Ocramius
Copy link
Member

@Ocramius Ocramius commented Mar 28, 2018

This RFC proposes replacing the typical /** @var Something $something */ with assert($something instanceof Something) wherever possible.

This has a few nice side-effects that @DASPRiD exposed:

  1. the type declaration is no longer a comment, but AST nodes that can be inspected and have well defined meaning in the language itself
  2. assert() can report when these assumptions fail (once the CS rule is applied to a codebase)
  3. assert() can be turned off, making this an informational addition

I didn't dig into PHPStan's new type declarations, which support foo[], (foo|bar)[] and union types (foo&bar) too much, so this is just a first sketch of the idea I stole from @DASPRiD.

/** @var Type1|Type2 $unionType */
$unionType = expression();

/** @var Type1&Type2 $intersectionType */
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure if we should deal with this now. I can see it growing in usage since it was first introduced a couple months ago, so it may be worth exploring after poking the @phpstorm folks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue for intersection types is pending and being ignored: https://youtrack.jetbrains.com/issue/WI-39419 for three months already. :(
Unfortunately until then, it's a no-go, it completely breaks assumptions in PhpStorm.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that one actually a problem?

assert($intersectionType instanceof Type1 && $intersectionType instacenof Type2);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem here is that contemporary tooling does not support this except for phpstan.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@morozov the CS tool simply needs to learn to parse them in order to replace them. These annotations are still valid in docblocks.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@muglug awesome! Now we just need to convince JetBrains 😂

Copy link
Member

@morozov morozov Mar 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These annotations are still valid in docblocks.

Do you also want to replicate complex argument types with assertions? Otherwise, unlike local variables, they remain unenforced. For instance:

/**
 * @param string|int $a
 */
function ($a) {
    assert(is_string($a) || is_int($a));
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be interesting, but unlikely for this PR

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too sure about how I feel about assertions for public API parameters. Personally of course I always avoid complex types in public APIs and only use them in private APIs when I want to keep things simple.

The thing with assertions is, that they are not ran by default (heck, PHP throws away the assertion op-codes when assertions are disabled). Thus most users wouldn't get those checks. IMHO a public API should do parameter checks the proper way (and throw exceptions itself) for such stuff.

Assertions come in handy when you have methods which are supposed to return something specific, but the API cannot specify the return type (like PSR containers). Thus you assert on the return type.

/** @var int|float|bool|string|null|array $multipleScalarTypes */
$multipleScalarTypes = expression();

/** @var Potato $variableThatIsNowhereToBeFound */
Copy link
Member

@morozov morozov Mar 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it for the cases like extract() or eval()? If the standard doesn't allow them (and why would it?), then this case doesn't need to be handled.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replacing an inline definition with an assertion still challenges the assumption, which is good

assert($nullableType instanceof Type || $nullableType === null);

$multipleScalarTypes = expression();
assert(is_int($multipleScalarTypes) || is_float($multipleScalarTypes) || is_bool($multipleScalarTypes) || is_string($multipleScalarTypes) || $multipleScalarTypes === null || is_array($multipleScalarTypes));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about assert(in_array(gettype($multipleScalarTypes), ['int', 'float', 'bool', 'string', 'null', 'array'], true)), isn't a more readable/cheaper way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's harder to parse for the IDE to infer the type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed: let's keep it to very well known constructs

@mikeSimonson
Copy link
Contributor

@Ocramius I like the idea but I don't what's better, holding off the part that are not supported by phpstorm or going all in and see how it goes.

@Ocramius
Copy link
Member Author

holding off the part that are not supported by phpstorm or going all in and see how it goes.

Yeah, I think this will be the initial approach

@Majkl578
Copy link
Contributor

$ git grep -E '/\*\*\s+@var\s+[^\|]+\s+\$' lib tests | wc -l

There is currently 174 affected non-compound occurrences in ORM.

 git grep -E '/\*\*\s+@var\s+\S+\s+\$' lib tests | wc -l

There is currently 249 afftected compound or non-compound occurrences in ORM.

This may (or may not) have serious impact.

assert() can be turned off, making this an informational addition

This assumption is not valid, since you can't turn it off per-library or so. There are projects out there that build business asserts using assert().

@DASPRiD
Copy link

DASPRiD commented Mar 28, 2018

Having business logic depend on assertions is a seriously bad practice, and they should feel bad.

@Ocramius
Copy link
Member Author

@Majkl578 we'd obviously have to turn assertions on in the test suite.

@Majkl578
Copy link
Contributor

Majkl578 commented Mar 28, 2018

@DASPRiD Not really. Please consult your opinion with beberlei/assert and webmozart/assert.

@Ocramius
Copy link
Member Author

Those two libs are not at all matching assert() from PHP. I use them extensively in business logic precisely because you can't turn them off.

@Ocramius
Copy link
Member Author

@Majkl578
Copy link
Contributor

I can easily use assert() for the same purpose (and avoid method call overheads) with simple pre-condition: asserts are always enabled.
This pre-condition is totally ok for a project, but absolute no-go for a library.

@Ocramius
Copy link
Member Author

Indeed, but (assuming th docblocks are correct) whether the assertions are enabled or disabled makes no functional difference with the proposed patch approach

@Majkl578
Copy link
Contributor

Not functional, but could have performance impact, depending on the amount & # of uses of course.

@Majkl578
Copy link
Contributor

In other words, simply adding 250 assert()s just because you don't like phpDoc is not enough to justify that.
Argument 1. is functionally irrelevant.
Argument 2. is valid, but you can achieve the same with SA for @var.
Argument 3. is where I disagree most (see above).

Putting `/** @var foo $foo */` after `$foo = bar();` is now a fixable
violation as per slevomat/coding-standard@63a8186

Thanks @kukulich!

Meanwhile, also pinning down `slevomat/coding-standard` to the specific
commit that includes all required changes for this test to run.

Ref: f797ff6#commitcomment-33331234
@Ocramius Ocramius force-pushed the feature/assertions-over-inline-type-docs branch from 40cae4d to c8e32d3 Compare April 29, 2019 08:34
@@ -28,7 +28,7 @@
"require": {
"php": "^7.1",
"dealerdirect/phpcodesniffer-composer-installer": "^0.5.0",
"slevomat/coding-standard": "^5.0",
"slevomat/coding-standard": "dev-master#63a8186b129ee96d1277e68c80cf87c8cdb356d1",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@malarzm @deeky666 I'm likely raising a separate PR for stable slevomat release pinning by @kukulich.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC pinning a commit could lead to weird side effects where we can end up with vendor requirements from current dev-master while code in pinned commit could require something else.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes: this is not for a stable release yet. I'd need a SemVer selector for the release to be tagged.

@Ocramius
Copy link
Member Author

@doctrine/coding-standard-approvers please shoot a 👍 or 👎 my way.

As mentioned in #47 (comment), I'll pin to a stable release after this (chore/release work).

@alcaeus
Copy link
Member

alcaeus commented Apr 29, 2019

👍 on the rule itself. Not sure if we want to merge with commit pinning in composer.json, I'll defer to others for that decision.

@@ -311,6 +311,8 @@
<rule ref="SlevomatCodingStandard.PHP.UselessParentheses"/>
<!-- Forbid useless semicolon `;` -->
<rule ref="SlevomatCodingStandard.PHP.UselessSemicolon"/>
<!-- Require /* @var type $foo */ and similar simple inline annotations to be replaced by assert() -->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this sniff support /* @var ArrayIterator $iterator */ with one * too like in this comment? IDEs like PHPStorm support those too, even though it doesn't autocomplete on "/*". In case it supports the single-*, some tests for those would be nice.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not. However it is reported (and fixed) by

<rule ref="SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration"/>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that both can be used? The first one fixing a /* and the other one handles it for the assert()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A CS rule enforcing /** style would be preferable: please note that current static analysis tools fail to parse /* AFAIK

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InvalidDocCommentDeclaration fixes /* to /** in first round and in second round RequireExplictionAssertion changes doccomment to assertion :)

Copy link

@DASPRiD DASPRiD Apr 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A CS rule enforcing /** style would be preferable: please note that current static analysis tools fail to parse /* AFAIK

Last time I used NetBeans (granted, a while ago), it only supported vardoc syntax (/*) but not /**.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That changed a long time ago: at the moment, the ecosystem is pushing for /**, and enabling that sniff could put a tombstone on this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ocramius Not sure I undrestand you well but the InvalidInlineDocCommentDeclaration is already enabled ;)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kukulich yeah, this was mostly for @DASPRiD 👍

Copy link
Member

@lcobucci lcobucci left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind the commit pinning on composer.json, we just have to remember that we shouldn't release anything before removing it.

Thanks @kukulich!

Copy link
Member

@ostrolucky ostrolucky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was review re-requested from me when nothing changed?

Copy link
Member

@SenseException SenseException left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A usage of SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration can be discussed another time.

@Ocramius Ocramius dismissed stale reviews from morozov and alcaeus April 30, 2019 14:23

Dismissing current feedback due to approvals quota reached

@Ocramius Ocramius merged commit 89a1294 into master Apr 30, 2019
@Ocramius Ocramius deleted the feature/assertions-over-inline-type-docs branch April 30, 2019 14:24
@Ocramius Ocramius self-assigned this Apr 30, 2019
@Ocramius Ocramius added this to the 7.0.0 milestone Apr 30, 2019
@lcobucci
Copy link
Member

Why was review re-requested from me when nothing changed?

@ostrolucky my bad (or maybe GH UI issues), sorry. I was asking re-reviews from people that requested changes but didn't look at it again, interface moved and I miss clicked 😓

@ostrolucky
Copy link
Member

No worries I just wanted to know. Explanation makes sense :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.