Skip to content

Refactor suggestion / move utility functions from File to static classes #2189

Closed

Description

@jrfnl

Since PHPCS 3.x now has a minimum PHP requirement of PHP 5.4, I was wondering whether it might be an idea to refactor a number of groups of methods in the File class to traits.

The reasoning behind this is two-fold:

  1. Only a limited number of sniffs use the methods I have in mind for this, so having them in traits rather than in the File class makes the re-useable code more modular and easier to maintain.
  2. Over the years I have written a number of additional token walking utility methods which I think belong in PHPCS rather than in the WPCS/PHPCompatibility standards in which they now live.
    Traits would allow for these methods to be added to PHPCS more easily without making the File class even larger than it already is.

Timing

The transition could be made in PHPCS 3.x.
In PHPCS 3.x, the File class would in that case use the new traits and for the transitioned methods just call the method in the trait under the hood.
The methods in the File class would be deprecated and could be removed in PHPCS 4.x.
This would allow external standards some time to implement the new way of doing things and make the BC break in PHPCS 4.x smaller as it can already be mitigated beforehand.

Proposal details

To be concrete, I'm currently thinking of the following Traits:

  • DeclarationName, containing:
    • getName(), previously called getDeclarationName()
  • FunctionDeclaration, containing:
    • getParameters(), previously called getMethodParameters()
    • getProperties(), previously called getMethodProperties()
    • and using DeclarationName for the getName() method
  • PropertyDeclaration, containing:
    • getMemberProperties()
  • ObjectDeclaration containing:
  • Generic containing:
    • isReference()
  • ScopeConditions, containing:
    • hasCondition()
    • getCondition()

Additionally, I'd be happy to pull the following based on battle-tested code I've written for WPCS/PHPCompatibility:

  • FunctionCall, containing:
    • hasParameters()
    • getParameterCount()
    • getParameters()
    • getParameter() (to get the details for the parameter at a specific position)
      These methods all work on both function calls as well as for splitting an array into the individual items and could be extended to cover isset(), unset() and list() as well.
  • And if there would be any interest in it - FQClassNames, containing:
    • getNameFromNewToken()
    • getNameFromDoubleColonToken()
    • getExtendedName()
    • getFQName()
    • isNamespaced()
    • determineNamespace()
    • getDeclaredNamespaceName()

For the Generic trait, I could also offer an additional isShortList() method.

I'm also still working on the logic for a TStringAnalyser which should reliably determine whether, for instance, a T_STRING token represents a function call, the use of a global constant or for instance a use statement alias.

Open questions

What I'd currently like to know, is:

  • Does the principle of this sound like a good idea for PHPCS ?
  • If so, does the implementation proposal for PHPCS 3.x/4.x sound like a good idea ?
  • If so, where should the traits live ? I.e. , should they be placed in a dedicated Traits directory or, for instance, in Util/Traits or should the directory be called something completely different ?
  • If so, feedback on the proposed initial Traits as outline above (trait names, function groupings etc)
  • And would there be interest in the additional traits/methods as outlined above ?

Activity

  1. gsherwood commented on Oct 18, 2018

    @gsherwood
    Member
    • Does the principle of this sound like a good idea for PHPCS ?

    Yeah, I think I like the idea. The File class has always been bloated but I've never put thought into refactoring it due to the large BC break it would cause.

    • If so, does the implementation proposal for PHPCS 3.x/4.x sound like a good idea ?

    My initial thought was v4 only, but your point about developers being able to migrate sniffs before v4 is a good one, so I think you are right. This feels like something that can go into v3 with the transitional methods and in File deprecated immediately and removed in v4.

    • If so, where should the traits live ? I.e. , should they be placed in a dedicated Traits directory or, for instance, in Util/Traits or should the directory be called something completely different ?

    I would normally throw them under a src/Traits folder. I do prefer trait class to include the word "Trait" at the end of the file and trait name, but having Traits in the FQ name makes this redundant. So I don't know. Maybe they should live in src/Files or src/Sniffs somewhere?

    • If so, feedback on the proposed initial Traits as outline above (trait names, function groupings etc)

    They look ok to me. I don't have a strong opinion on this yet, but I imagine I will once I see some code being used.

    • And would there be interest in the additional traits/methods as outlined above ?
      Yep.

    This one's a big job and something I'd probably want to solely focus on for a single PHPCS release as it is also going to be a big information dump for sniff developers. So maybe this is the change in v3.5 or v3.6.

  2. jrfnl commented on Oct 18, 2018

    @jrfnl
    ContributorAuthor

    @gsherwood Thanks for your response and feedback. I'd happily work on this, but as you say you'd prefer to have this in a dedicated release, I'll hold off for now until 3.4.0 has come out. That would also allow for PR #2128 to be merged before this refactor.

  3. jrfnl commented on Nov 15, 2018

    @jrfnl
    ContributorAuthor

    I've been thinking about this some more and while traits would be a good solution for some of the external standards I work on, which have a base sniff class from which all sniffs extend, for PHPCS itself, the implementation will be simpler and cleaner as classes with static properties and static methods.

    This will prevent naming conflicts over imported traits when both an abstract sniff as well as a child sniff would use the same trait, or when one trait would use another.

    With that mind, putting these static classes in the Util or in a Helper directory seems logical.


    For anyone wondering about the difference/choice for static classes:
    Nearly all of these methods will need to be passed the $phpcsFile object and will need the tokens array.

    In some of the external standards, a base sniff class is used which makes those available via $this to save having to pass these around on every function call to utility functions, like so:

    abstract class Sniff implements PHPCS_Sniff {
        public $phpcsFile;
        public $tokens;
    
        public function process(File $phpcsFile, $stackPtr) {
            $this->phpcsFile = $phpcsFile;
            $this->tokens    = $phpcsFile->getTokens();
            return processToken($stackPtr);
        }
    
        abstract public function processToken($stackPtr);
    }

    However, as this is not standard in PHPCS itself, traits would not have access to $phpcsFile or $tokens via $this, which removes the reason for using traits instead of static classes.

  4. jrfnl commented on Dec 17, 2018

    @jrfnl
    ContributorAuthor

    Just FYI: I've started work on this as, as soon as I started work on the AbstractDocblockSniff - #2222 -, I realized some things would be easier if some of the new utility functions would already be available.

    I hope to slowly but surely get a complete WIP branch ready by the time 3.4.0/3.4.1 is released, so I can start pulling the individual bits from it as soon as 3.5.0 opens for commits.

    Once I've cleaned up what I've done so far, I'll put a WIP branch online somewhere so anyone interested can have a look at the direction this is going in.

  5. jrfnl commented on Jan 10, 2019

    @jrfnl
    ContributorAuthor

    Little status update for those interested:

    I'm working on this on and off, but making steady progress. I estimate that about 70% is done at this moment.

    Notes:

    • All existing, as well as all new, utility methods mentioned below have dedicated unit tests based on PR Core tests: set up abstract class and remove code duplication #2295.
    • I've adjusted all existing calls to the "old" methods in the current code base.
      Depending on what gets merged before this refactor gets merged, some additional adjustments may still be needed, but I'm keeping a close eye on that.
    • All "old" methods (and the below mentioned properties) are deprecated, but will still work, calling the new methods in the utility classes under the hood.
    • All properties and methods mentioned are public static and can be called from anywhere (unless otherwise indicated).

    Current structure as things are shaping up:

    • Class Util\ConditionUtils
      • getCondition() (moved from File) - this method has been enhanced a lot to be more flexible and remove code duplication.
      • hasCondition() (moved from File)
      • New: getFirstCondition()
      • New: getLastCondition()
      • New: isOOProperty()
      • New: isOOConstant()
      • New: isOOMethod()
      • New: validDirectScope()
    • Class Util\FunctionDeclarationUtils
      • $magicMethods (moved from Generic.NamingConventions.CamelCapsFunctionName + PEAR.NamingConventions.ValidFunctionName)
      • $methodsDoubleUnderscore (moved from Generic.NamingConventions.CamelCapsFunctionName)
      • $magicFunctions (moved from Generic.NamingConventions.CamelCapsFunctionName + PEAR.NamingConventions.ValidFunctionName)
      • getMethodParameters() (moved from File)
      • getMethodProperties() (moved from File)
      • New isMagicFunction()
      • New isMagicFunctionName()
      • New isMagicMethod()
      • New isMagicMethodName()
      • New isPHPDoubleUnderscoreMethod()
      • New isPHPDoubleUnderscoreMethodName()
      • New isSpecialMethod()
      • New isSpecialMethodName()
    • Class Util\ObjectDeclarationUtils
    • Class Util\PassedParametersUtils
      • New hasParameters()
      • New getParameters()
      • New getParameter()
      • New getParameterCount()

    Currently working on:

    • Class Util\VariableUtils
      • $phpReservedVars (moved from AbstractVariableSniff + enhanced and made complete)
      • getMemberProperties() (moved from File)
      • New isPHPReservedVarName()
      • New isSuperglobal()
      • New isSuperglobalName()
      • New isForeachAs()
      • WIP: New isAssignment()
      • .... (to be determined)
    • Class Util\NameUtils
      • getDeclarationName() (moved from File)
      • isCamelCaps() (moved from Util\Common)
      • WIP: New toCamelCaps()
      • WIP: New isPascalCase()
      • WIP: New toPascalCase()
      • WIP: New isUpperCaseUnderscores()
      • WIP: New toUpperCaseUnderscores()
      • WIP: New isSnakeCase()
      • WIP: New toSnakeCase()
      • Suggestions for more/other common conversions welcome. Please include a link to the specs with your suggestion.
    • Class Util\TextStringUtils
      • REGEX_COMPLEX_VARS
      • New stripQuotes()
      • New stripInterpolatedVariables()
      • New getInterpolatedVariables()
      • .... (to be determined)
    • Class Util\... (name to be determined)
      • isReference() (moved from File)
      • New isShortList()
      • New getUseType()
      • New getAsType()
      • .... (to be determined)

    On the back-burner for later in combination with the AbstractDocblockSniff (see #2222):

    • Class Util\CommentUtils
      • $allowedTypes (moved from Common)
      • suggestType() (moved from Common)
      • New findCommentAboveFunction()
      • New findCommentAboveOOStructure()
      • New findCommentAboveOOProperty()
      • New findCommentAboveOOConstant()
      • New findCommentAbove()
      • .... (to be determined)

    Feedback welcome.

  6. gsherwood commented on Jan 10, 2019

    @gsherwood
    Member

    First thought... wow :)

    Second is just about naming. I don't think every class under Util also needs to end with Utils. Seems redundant to me in this case. Is there a specific reason (code looks better, it's a convention im not aware of, etc)?

    I'd also change FunctionsDeclarationUtils to FunctionDeclaration to match ObjectDeclaration (unless that was a typo).

  7. jrfnl commented on Jan 10, 2019

    @jrfnl
    ContributorAuthor

    First thought... wow :)

    😀

    Second is just about naming. I don't think every class under Util also needs to end with Utils. Seems redundant to me in this case. Is there a specific reason (code looks better, it's a convention im not aware of, etc)?

    Not existing convention or anything, just felt like a way to differentiate these classes from the other/existing classes in the Util directory which don't typically offer methods for use in sniffs (other than the Common class, but the utility methods in that class will be moved out to one of the new classes anyway).

    Alternatively, another directory for these type of classes would also be an option. Just what would that directory be called in that case ?

    I'd also change FunctionsDeclarationUtils to FunctionDeclaration to match ObjectDeclaration (unless that was a typo).

    That was a typo ;-)

  8. gsherwood commented on Jan 10, 2019

    @gsherwood
    Member

    Alternatively, another directory for these type of classes would also be an option. Just what would that directory be called in that case ?

    In my head, these were under File/Util, but I think I just read your comments incorrectly.

    Maybe Util/Sniffs ?? I hate naming things.

  9. jrfnl commented on Jan 10, 2019

    @jrfnl
    ContributorAuthor

    In my head, these were under File/Util, but I think I just read your comments incorrectly.

    To prevent further confusion, they are currently in the src/Util directory.

    Maybe Util/Sniffs ?? I hate naming things.

    I hear you. Util/Sniffs doesn't feel right either though, though I haven't got a good alternative.
    I'll sleep on it and welcome more suggestions. Someone will have a good name in mind ;-)

  10. changed the title [-]Refactor suggestion / move utility functions from File to traits[/-] [+]Refactor suggestion / move utility functions from File to static classes[/+] on Jan 10, 2019
  11. jrfnl commented on Jan 26, 2019

    @jrfnl
    ContributorAuthor

    Ok, let's figure this name thing out as I will have to redo every single commit with the new names, so I can't really publish a WIP branch until we have a decision on this.

    Some suggestions of where to place these classes (in alphabetic order, not indicating any preference):

    • src/Helpers
    • src/SniffHelpers
    • src/SniffUtils
    • src/Utils/SniffHelpers

    The previously suggested src/Sniff/Utils doesn't really feel right as it would lump these classes together with the abstracts, while they are not abstracts and are useful for a lot more sniffs than just the abstracts.

    The previously suggested src/File/Utils doesn't really feel right either as these classes don't provide a File object with tokens, like the classes in File.

    My preference would be a toplevel directory as hiding these away in a deeper level makes them less easily discoverable and they are kind of useful ;-)

    Watchers of this repo, please feel free to chime in with ideas or preferences as well.

  12. jrfnl commented on Jan 26, 2019

    @jrfnl
    ContributorAuthor

    @gsherwood In practical terms, how would you like this to be pulled ?

    PR #2295 is a prerequisite for this PR so would need to be merged first and I've worked on the presumption that PR #2353 will be merged beforehand as well.

    The way I've set things up is with atomic commits with most of the time quite detailed commit messages telling "the story" (see screenshot).

    Screenshot of the first part of the WIP branch commits

    phpcs-refactor-wip


    Some parts could be pulled simultaneously, but pulling things in one go or in blocks in the order I've set them up will probably be easiest as for implementation commits, the use statements in sniffs would conflict if not pulled in this order and for commits adding files (the helper classes and/or unit tests), the package.xml file would quickly start to conflict.

  13. GaryJones commented on Jan 26, 2019

    @GaryJones
    Contributor

    src/Helpers is too generic.

    Of those choices, src/SniffUtils seems the most descriptive and accurate. These are utility classes / methods / things to help write Sniffs efficiently.

  14. jrfnl commented on Jan 28, 2019

    @jrfnl
    ContributorAuthor

    For the naming convention methods, I'm still trying to find unambiguous specifications. I've created a little comparison table of my current understanding of them.

    Input welcome! You can comment in the comparison table.
    https://docs.google.com/spreadsheets/d/1WQo7G3BRIk0dT9eMxcPJw48DShrlenQRfvlD--rFAgA/edit?usp=sharing

    namingconventionscomparison

  15. jrfnl commented on Feb 2, 2019

    @jrfnl
    ContributorAuthor

    @gsherwood Just checking in: have you had a chance to think about the naming and how to pull this ?

  16. 6 remaining items

  17. gsherwood commented on Feb 28, 2019

    @gsherwood
    Member

    Only question I still have would be whether the "long" form for true and false would be true/false or boolean ?

    Given true/false are valid return types, the long form probably remains as true/false, unless I've missed some complexity here.

  18. gsherwood commented on Feb 28, 2019

    @gsherwood
    Member

    Just checking in: have you had a chance to think about the naming and how to pull this ?

    I think pulling in the other two commits at once makes sense. I'll target them for 3.5.0 as well.

    As for naming, I really dislike multi-word directories, so my vote is for src/Util/Sniffs or src/Util/Sniff - can't decide if I like the plural form or not. Every other directory besides Util uses plurals, so I'd probably lean that way.

  19. added this to the 3.5.0 milestone on Feb 28, 2019
  20. jrfnl commented on Feb 28, 2019

    @jrfnl
    ContributorAuthor

    @gsherwood Thanks for the input. That would hide these utility classes away in a deeper level, making them a little less easily discoverable, and - aside from the Standards directory - none of the others directories in src have deeper levels.

  21. gsherwood commented on Feb 28, 2019

    @gsherwood
    Member

    That would hide these utility classes away in a deeper level, making them a little less easily discoverable

    I don't think they are hidden in there. I suspect people would look in Utils for utility functions and see another directory dedicated to sniff utility classes. It also allows for future util categories to have a place.

    aside from the Standards directory - none of the others directories in src have deeper levels

    True, but I don't think those other dirs are related to each other. This is a new collection of utility classes that will be used just as often as Util/Tokens. I'd prefer to keep them together in a Util folder and namespace.

  22. jrfnl commented on Feb 28, 2019

    @jrfnl
    ContributorAuthor

    Okidokie, src/Util/Sniffs it is.

    Give me a few days to adjust all the individual commits. After that I will set up a draft PR.

  23. jrfnl commented on Feb 28, 2019

    @jrfnl
    ContributorAuthor

    Oh and just to be sure: we were going to drop the Utils from the class names too, weren't we ?

    So, for just a sampling of the new classes:

    Old file name New file name New class name
    src\Util\ConditionUtils src\Util\Sniffs\Conditions PHP_CodeSniffer\Util\Sniffs\Conditions
    src\Util\FunctionDeclarationUtils src\Util\Sniffs\FunctionDeclarations PHP_CodeSniffer\Util\Sniffs\FunctionDeclarations
    src\Util\ObjectDeclarationUtils src\Util\Sniffs\ObjectDeclarations PHP_CodeSniffer\Util\Sniffs\ObjectDeclarations
    src\Util\NamespaceUtils src\Util\Sniffs\Namespaces PHP_CodeSniffer\Util\Sniffs\Namespaces
  24. gsherwood commented on Feb 28, 2019

    @gsherwood
    Member

    Oh and just to be sure: we were going to drop the Utils from the class names too, weren't we ?

    Yes please

  25. jrfnl commented on Mar 23, 2019

    @jrfnl
    ContributorAuthor

    Ok, done all the renames and recommits. WIP is up as a draft PR #2456

  26. mwgamble commented on Mar 26, 2019

    @mwgamble

    Note: you can not add use statements for the new classes as they won't be available in older PHPCS versions. Note: if you choose this direction, you can not (yet) use the new methods available in the new utility classes.

    I don't think this is accurate. This snippet works just fine in PHP 5.5 and up:

    <?php
    use NonExistentNamespace\NonExistentClass;
    var_dump(class_exists(NonExistentClass::class));

    https://3v4l.org/S5KTi

  27. jrfnl commented on Mar 26, 2019

    @jrfnl
    ContributorAuthor

    @mwgamble Interesting, I'll do some testing with one of the external standards, but I seem to remember this was problematic when I was making some of those cross-version compatible with PHPCS 2 and 3.

    Providing the tests are successful, I'll remove that phrase from the Upgrade Guide. Thanks for your input!

  28. jrfnl commented on Mar 26, 2019

    @jrfnl
    ContributorAuthor

    @mwgamble Tested & found working. I've updated the Upgrade Guide to reflect this and expanded the code sample as well. Thanks!

  29. removed this from the 3.5.0 milestone on Aug 27, 2019
  30. jrfnl commented on Apr 12, 2020

    @jrfnl
    ContributorAuthor

    Closing as this refactor is not going into PHPCS anymore.

    It has been published separately as PHPCSUtils. More details in this comment: #2456 (comment)

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions