├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── HM-Minimum └── ruleset.xml ├── HM ├── Sniffs │ ├── Classes │ │ └── OnlyClassInFileSniff.php │ ├── Debug │ │ └── ESLintSniff.php │ ├── ExtraSniffCode.php │ ├── Files │ │ ├── ClassFileNameSniff.php │ │ ├── FunctionFileNameSniff.php │ │ └── NamespaceDirectoryNameSniff.php │ ├── Functions │ │ └── NamespacedFunctionsSniff.php │ ├── Layout │ │ └── OrderSniff.php │ ├── Namespaces │ │ └── NoLeadingSlashOnUseSniff.php │ ├── PHP │ │ └── IssetSniff.php │ ├── Performance │ │ ├── SlowMetaQuerySniff.php │ │ └── SlowOrderBySniff.php │ ├── Security │ │ ├── EscapeOutputSniff.php │ │ ├── NonceVerificationSniff.php │ │ └── ValidatedSanitizedInputSniff.php │ └── Whitespace │ │ └── MultipleEmptyLinesSniff.php ├── Tests │ ├── Classes │ │ ├── OnlyClassInFileUnitTest.fail.class │ │ ├── OnlyClassInFileUnitTest.fail.function │ │ ├── OnlyClassInFileUnitTest.php │ │ └── OnlyClassInFileUnitTest.success.const │ ├── Files │ │ ├── ClassFileNameUnitTest.php │ │ ├── ClassFileNameUnitTest │ │ │ ├── class-Foo.php │ │ │ ├── class-bar.php │ │ │ ├── class-test.php │ │ │ ├── class-two-parts.php │ │ │ ├── class-two_parts.php │ │ │ ├── class-twoparts.php │ │ │ └── test.php │ │ ├── FunctionFileNameUnitTest.php │ │ ├── FunctionFileNameUnitTest │ │ │ ├── matching-namespace.php │ │ │ ├── namespace.php │ │ │ ├── not-matching-namespace.php │ │ │ └── not-namespace.php │ │ ├── NamespaceDirectoryNameUnitTest.php │ │ └── NamespaceDirectoryNameUnitTest │ │ │ ├── inc │ │ │ ├── coffee │ │ │ │ ├── more │ │ │ │ │ └── fail.php │ │ │ │ └── namespace.php │ │ │ ├── namespace.php │ │ │ └── standards │ │ │ │ ├── camelcased-namespace.php │ │ │ │ ├── coffee │ │ │ │ ├── grinder.php │ │ │ │ ├── more │ │ │ │ │ └── grinder-fail.php │ │ │ │ └── namespace.php │ │ │ │ ├── fail.php │ │ │ │ └── underscored-namespace.php │ │ │ └── tests │ │ │ ├── coffee │ │ │ ├── grinder.php │ │ │ └── more │ │ │ │ └── fail.php │ │ │ ├── namespace.php │ │ │ └── standards │ │ │ ├── coffee │ │ │ ├── coffee.php │ │ │ └── more │ │ │ │ └── fail.php │ │ │ └── fail.php │ ├── Layout │ │ ├── OrderUnitTest.inc │ │ └── OrderUnitTest.php │ ├── Namespaces │ │ ├── NoLeadingSlashOnUseUnitTest.fail │ │ ├── NoLeadingSlashOnUseUnitTest.php │ │ └── NoLeadingSlashOnUseUnitTest.success │ └── Whitespace │ │ ├── MultipleEmptyLinesUnitTest.fail │ │ ├── MultipleEmptyLinesUnitTest.php │ │ └── MultipleEmptyLinesUnitTest.success ├── bootstrap.php └── ruleset.xml ├── README.md ├── composer.json ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── README.md ├── eslint-config-humanmade │ ├── .eslintrc │ ├── fixtures │ │ ├── fail │ │ │ ├── component-jsx-parentheses.js │ │ │ ├── import-order.js │ │ │ ├── jsx-boolean-value.jsx │ │ │ ├── jsx-curly-newline.jsx │ │ │ ├── semicolon.js │ │ │ ├── template-curly-spacing.js │ │ │ └── variable-declaration.js │ │ ├── pass │ │ │ ├── component-jsx-parentheses.jsx │ │ │ ├── import-order.js │ │ │ ├── jsdoc-inline-arrow.js │ │ │ ├── jsx-boolean-value.jsx │ │ │ ├── jsx-curly-newline.jsx │ │ │ ├── semicolon.js │ │ │ └── template-curly-spacing.js │ │ └── test-lint-config.js │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── readme.md └── stylelint-config │ ├── .stylelintrc.json │ ├── fixtures │ ├── fail │ │ ├── bad-bem-syntax.css │ │ ├── max-nesting-depth.scss │ │ └── no-color-named.css │ ├── pass │ │ ├── style.css │ │ └── style.scss │ └── test-lint-config.js │ ├── package-lock.json │ ├── package.json │ └── readme.md ├── phpunit.xml.dist ├── publish.sh ├── ruleset.xml └── tests ├── AllSniffs.php ├── FixtureTests.php ├── bootstrap.php └── fixtures ├── fail ├── consecutive-empty-lines.php ├── consecutive-empty-lines.php.json ├── escape-output.php ├── escape-output.php.json ├── isset.php ├── isset.php.json ├── meta-queries.php ├── meta-queries.php.json ├── not-namespace.php ├── not-namespace.php.json ├── order-by.php ├── order-by.php.json ├── server-input.php ├── server-input.php.json ├── use-order.php └── use-order.php.json └── pass ├── escape-output.php ├── inc ├── coffee │ ├── grinder │ │ └── grounds.php │ └── pot.php └── namespace.php ├── isset.php ├── load.php ├── meta-queries.php ├── nonce-verification.php ├── order-by.php ├── plugin.php ├── server-input.php ├── tests └── namespace.php └── use-order.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore composer things. 2 | vendor/ 3 | composer.lock 4 | .idea/ 5 | 6 | # Ignore node things. 7 | node_modules/ 8 | yarn.lock 9 | 10 | # Ignore built files 11 | archives/ 12 | phpcs-standard/ 13 | 14 | # OS generated files 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Setup caching 2 | cache: 3 | directories: 4 | - $HOME/.cache/composer/files 5 | - $HOME/.npm 6 | - node_modules 7 | 8 | branches: 9 | only: 10 | - master 11 | 12 | # Test in modern and recent versions of PHP & Node. 13 | # Run each code style tool in containers specific to that tool's language. 14 | jobs: 15 | include: 16 | - language: php 17 | php: 7.2 18 | install: 19 | - composer install 20 | script: 21 | - vendor/bin/phpunit 22 | - language: php 23 | php: 7.3 24 | install: 25 | - composer install 26 | script: 27 | - vendor/bin/phpunit 28 | - language: php 29 | php: 7.4 30 | install: 31 | - composer install 32 | script: 33 | - vendor/bin/phpunit 34 | - language: php 35 | php: 8.0 36 | install: 37 | # For PHP 8.0+, we need to ignore platform reqs as PHPUnit 7 is still used. 38 | - composer install --ignore-platform-reqs 39 | script: 40 | - vendor/bin/phpunit 41 | - language: php 42 | php: 8.1 43 | install: 44 | # For PHP 8.0+, we need to ignore platform reqs as PHPUnit 7 is still used. 45 | - composer install --ignore-platform-reqs 46 | script: 47 | # For PHP 8.1+, we need to ignore the config file so that PHPUnit 7 doesn't try to read it and cause an error. 48 | # Instead, we pass all required settings as part of the phpunit command. 49 | - vendor/bin/phpunit --no-configuration --bootstrap=tests/bootstrap.php --dont-report-useless-tests tests/AllSniffs.php 50 | - vendor/bin/phpunit --no-configuration --bootstrap=tests/bootstrap.php --dont-report-useless-tests tests/FixtureTests.php 51 | - language: node_js 52 | node_js: 16 53 | install: 54 | - npm install 55 | - cd packages/eslint-config-humanmade 56 | - npm install --legacy-peer-deps 57 | - cd ../.. 58 | script: 59 | - npm run test:eslint 60 | - language: node_js 61 | node_js: 16 62 | install: 63 | - npm install 64 | - cd packages/stylelint-config 65 | - npm install 66 | - cd ../.. 67 | script: 68 | - npm run test:stylelint 69 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.2.0 (September 13, 2022) 4 | 5 | - Add new Isset sniff #236 6 | - Update custom escaping functions for clean_html #264 7 | - Require spaces in template strings #256 8 | - Ignore "use" within "class" in OrderSniff #271 9 | - Add dealerdirect/phpcodesniffer-composer-installer to allow-plugins in composer.json #284 10 | - Update PHPCS to support PHP 8+ #282 11 | 12 | ## 1.1.3 (February 3, 2021) 13 | 14 | - Open ESLint peer dependency range to accept ESLint v6 & v7 #222 15 | 16 | ## 1.1.2 (December 10, 2020) 17 | 18 | ### Removed: 19 | 20 | - Disabled requirement to align PHPDoc parameters, inherited from WordPress-Docs in July's 1.0 release #239 21 | 22 | ## 1.1.1 (October 27, 2020) 23 | 24 | ### Added: 25 | 26 | - Support Composer 2 #233 27 | 28 | ### Changed: 29 | 30 | - Allowed use of the "relation" element in meta query sniff #232 31 | 32 | ## 1.1.0 (September 18, 2020) 33 | 34 | ### Added: 35 | 36 | - Added ESLint `eslint-plugin-import` plugin to enforce consistent ordering of `import` statements in JavaScript module files #219, #84 37 | - Added ESLint `eslint-plugin-jsdoc` plugin #218 38 | - Added ESLint `eslint-plugin-sort-destructure-keys` plugin #218 39 | 40 | ### Changed: 41 | 42 | - Make JSX property sorting case-insensitive #217 43 | 44 | ## 1.0.0 (July 31, 2020) 45 | 46 | ### Added: 47 | - Added `WordPress-Docs` by default in PHPCS #177 48 | - Added ESLint rule for requiring docblocks #209 49 | - Added ESLint rule for JSX boolean values #183 50 | - Added ESLint rule for sorting JSX props #195 51 | - Added ESLInt Rules of Hooks ruleset #197 52 | - Allow `$namespace.php` in function files #99 53 | - Added Lerna for publishing packages #175 54 | 55 | ### Updated: 56 | - Adjust Stylelint class and ID selector patterns #199 57 | - Updated WPCS to 2.2.1 #151 58 | - Updated VIPCS to 2.0.0 #151 59 | - Updated DealerDirect to 0.6 #151 60 | - Fixed `FunctionCallSignature` inconsistency in phpcbf #200 61 | - Allow for multiple variable assignments #201 62 | - Allow for theme filenames when sniffing filename #202 63 | - Updated `.editorconfig` for YAML & Markdown files #175 64 | 65 | ### Changed: 66 | - Formatted `package.json` files with tabs #175 67 | - Moved ESLint `.editorconfig` to project _root_ #175 68 | - Renamed _root_ `readme.md` to `README.md` #175 69 | - Updated `composer.json` description #175 70 | - Updated `package.json` files meta #175 71 | 72 | ### Removed: 73 | - Remove ``, `` and `testVersion` from ruleset #187, #198 74 | 75 | ## 0.8.0 (January 29, 2020) 76 | 77 | ### Added: 78 | - Added PHPCS Rule to Detect Consecutive Newlines #168 79 | - Enforce semicolons in JS #169 80 | - Added `WordPress.Security.EscapeOutput` PHPCS rule #166 81 | - Added PHPCompatibilityWP standard to PHPCS #81 82 | - Disallowed usage of `!important` in CSS #164 83 | - Enforced consistent curly newlines in jsx #172 84 | - Added `eslint-plugin-sort-destructure-keys` package #179 85 | 86 | ### Updated: 87 | - Bumped PHPCS to v3.5 from v3.4 #173 88 | - Bumped `stylelint-config-wordpress` package to v15 from v13 #165 89 | - Ignore stylelint `at-rule` line break for `if/else/elseif` #170 90 | - Restricted fixture tests to load only custom HM sniffs #163 91 | 92 | ## 0.7.0 (June 5, 2019) 93 | 94 | ### Changed: 95 | - Exclude `load.php` from `NamespaceDirectoryNameSniff` #131 96 | - Allow `json_encode` / `json_decode` function usage #97 97 | - Fix NamespaceDirectoryNameUnitTest parsing the wrong namespace directory length #140 98 | - Updated location of stylelint package to reflect correct NPM name #137 99 | 100 | ### Added: 101 | - Made PHPCompatibilityWP available via Composer #146 102 | 103 | ## 0.6.0 (April 2, 2019) 104 | 105 | ### Summation: 106 | - Updated PHPCS to v3.4 #88 107 | - Updated WPCS to 1.2.0 #82 108 | - Updated eslint to 5.10 and associated deps #101 109 | 110 | ### Added: 111 | - stylelint configuration #45 112 | - Added VIP PHPCS standards dependency #122 113 | 114 | ### Changed: 115 | - Use ecmaversion 2018 #87 116 | - Require space in curly braces for React JSX children #121 117 | - Allow multiple declaration in use statement #78 118 | - Exclude the `tests` dir from the NamespaceExclusionTest #112 119 | - Set composer library type to `phpcodesniffer-standard` #116 120 | 121 | ### Removed: 122 | - Allow `trigger_error` #98 123 | - Remove assignment of equals rule #96 124 | - Remove `href-no-hash` rule exclusion #114 125 | 126 |
127 | PHPCS Core Rule Additions 128 | 129 | https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/releases 130 | * Added new Generic.CodeAnalysis.EmptyPHPStatement sniff 131 | * Warns when it finds empty PHP open/close tag combinations or superfluous semicolons 132 | * Added new Generic.Formatting.SpaceBeforeCast sniff 133 | * Ensures there is exactly 1 space before a type cast, unless the cast statement is indented or multi-line 134 | * Added new Generic.VersionControl.GitMergeConflict sniff 135 | * Detects merge conflict artifacts left in files 136 | * Added Generic.WhiteSpace.IncrementDecrementSpacing sniff 137 | * Ensures there is no space between the operator and the variable it applies to 138 | * Added PSR12.Functions.NullableTypeDeclaration sniff 139 | * Ensures there is no space after the question mark in a nullable type declaration 140 | * Added new Generic.PHP.LowerCaseType sniff-Ensures PHP types used for type hints, return types, and type casting are lowercase 141 | * Added new Generic.WhiteSpace.ArbitraryParenthesesSpacing sniff 142 | * Generates an error for whitespace inside parenthesis that don't belong to a function call/declaration or control structure 143 | * Generates a warning for any empty parenthesis found 144 | * Allows the required spacing to be set using the spacing sniff property (default is 0) 145 | * Allows newlines to be used by setting the ignoreNewlines sniff property (default is false) 146 | * Added new PSR12.Classes.ClassInstantiation sniff 147 | * Ensures parenthesis are used when instantiating a new class 148 | * Added new PSR12.Keywords.ShortFormTypeKeywords sniff 149 | * Ensures the short form of PHP types is used when type casting 150 | * Added new PSR12.Namespaces.CompundNamespaceDepth sniff 151 | * Ensures compound namespace use statements have a max depth of 2 levelsThe max depth can be changed by setting the 'maxDepth' sniff property in a ruleset.xml file 152 | * Added new PSR12.Operators.OperatorSpacing sniff-Ensures operators are preceded and followed by at least 1 space 153 |
154 | 155 | ## 0.5.0 (May 22, 2018) 156 | 157 | - Update ESLint config peer dependencies #65 158 | - Add ESLint config test script with example fixtures #42 159 | 160 | ## 0.4.2 (May 1, 2018) 161 | 162 | - Remove support for ESLint-via-phpcs #54 163 | - Ignore array item alignment rule #49 164 | - Ignore line length when checking array alignment #57 165 | - Adjust object rules for destructuring #59 166 | 167 | ## 0.4.1 (Apr 18, 2018) 168 | 169 | - Fix order error for closure `use` #53 170 | - Fix false positives for `T_USE` #12 171 | 172 | ## 0.4.0 (Apr 17, 2018) 173 | 174 | - Always allow spaces inside arrays #3 175 | - Only run PHPCS on PHP files #36 176 | - Enforce spaces inside jsx curly braces #38 177 | - Make index pass its own rules #41 178 | - Add support for a .phpcsignore file #39 179 | - Add Sniff for unused "use" statements #44 180 | - Exclude filesystem groups from checks #50 181 | - Allow inline statements to drop semicolons #51 182 | 183 | ## 0.3.0 (Jan 18, 2018) 184 | 185 | - Update license for new requirements #34 186 | - Add tests for our phpcs sniffs #32 187 | - Update phpcs to v3 #31 188 | 189 | ## 0.2.2 (Nov 6, 2017) 190 | 191 | ## 0.2.1 (Dec 8, 2016) 192 | 193 | ## 0.2.0 (Dec 8, 2016) 194 | 195 | ## 0.1.0 (Dec 7, 2016) 196 | 197 | - Initial Release 198 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The HM coding standards represent the best practices for enabling our engineering teams to work together. As the way we work evolves over time, our coding standards likewise need to evolve. 4 | 5 | 6 | ## Guidelines for Rule Changes 7 | 8 | Bugfixes are always welcomed and can be released in minor or patch versions. 9 | 10 | New rules or major changes to rules need to be carefully considered and balanced against the churn they may cause. Generally, code that exists right now should continue to pass in the future unless we are **intentionally** ratcheting up rules to be stricter. These cases need to be carefully considered, as breaking production code should be avoided in most cases. 11 | 12 | Relaxing rules can be done in minor releases, but generally should be done in major releases if it's a major change (for example, allowing different file names). Use your best judgement to decide what is a major and what is a minor change, and if in doubt, run it past @joehoyle or @rmccue. 13 | 14 | Generally, so long as changes to rules have consensus, they are fine to be published. Any controversial rules should be widely discussed, and if a tie-breaker is needed, @joehoyle can make a final call. If you're not sure, ask @rmccue. Non-controversial changes or bugfixes do not need input from @joehoyle or @rmccue provided versioning and release processes are all followed. 15 | 16 | 17 | ## Testing 18 | 19 | ### Running tests 20 | 21 | To run the tests locally, you'll need the source version of PHP CodeSniffer. 22 | 23 | If you haven't already installed your Composer dependencies: 24 | 25 | ```bash 26 | composer install --prefer-source --dev 27 | ``` 28 | 29 | If you already have, and need to convert the phpcs directory to a source version: 30 | 31 | ```bash 32 | rm -r vendor/squizlabs/php_codesniffer 33 | composer install --prefer-source --dev 34 | composer dump-autoload 35 | ``` 36 | 37 | ### Writing sniff tests 38 | 39 | To add tests you should mirror the directory structure of the sniffs. For example a test 40 | for `HM/Sniffs/Layout/OrderSniff.php` would require the following files: 41 | 42 | ``` 43 | HM/Tests/Layout/OrderUnitTest.php # Unit test code 44 | HM/Tests/Layout/OrderUnitTest.inc # Code to be tested 45 | ``` 46 | 47 | Effectively you are replacing the suffix `Sniff.php` with `UnitTest.php`. 48 | 49 | A basic unit test class looks like the following: 50 | 51 | ```php 52 | => 67 | */ 68 | public function getErrorList() { 69 | return [ 70 | 1 => 1, // line 1 expects 1 error 71 | ]; 72 | } 73 | 74 | /** 75 | * Returns the lines where warnings should occur. 76 | * 77 | * @return array => 78 | */ 79 | public function getWarningList() { 80 | return []; 81 | } 82 | 83 | } 84 | ``` 85 | 86 | 87 | ### Fixture Tests 88 | 89 | Rather than testing sniffs individually, `FixtureTests.php` also tests the files in the `tests/fixtures` directory and ensures that whole files pass. 90 | 91 | To add an expected-pass file, simply add it into `tests/fixtures/pass` in the appropriate subdirectory/file. 92 | 93 | To add an expected-fail file, add it into `tests/fixtures/fail` in the appropriate subdirectory/file. You then need to add the expected errors to the JSON file accompanying the tested file (i.e. the filename with `.json` appended). This file should contain a valid JSON object keyed by line number, with each item being a list of error objects: 94 | 95 | ```json 96 | { 97 | "1": [ 98 | { 99 | "source": "HM.Files.FunctionFileName.WrongFile", 100 | "type": "error" 101 | } 102 | ] 103 | } 104 | ``` 105 | 106 | An error object contains: 107 | 108 | * `source`: Internal phpcs error code; use the `-s` flag to `phpcs` to get the code. 109 | * `type`: One of `error` or `warning`, depending on the check's severity. 110 | 111 | 112 | ## Releasing 113 | 114 | Any changes which cause existing, working production code to fail should trigger a new major release. Only bugfixes or making rules more lenient should be in minor releases. 115 | 116 | When publishing major releases, these need to be published in a two-step process. First, publish the standards, then bump the defaults after some time. This gives projects time to assess the changes and migrate at their own pace. The time between the publish and the default bump depends on the size and scope of the major changes, but generally should be 2-4 sprints worth of time for major changes. 117 | 118 | The process for releasing is: 119 | 120 | * Ensure your working directory is clean and up-to-date on `master` 121 | * Run `lerna publish` and add the new version number. 122 | * This will prompt you for a new version number and create & push new release commits and tags which will trigger Packagist to release a new version of the Composer package. 123 | * Run `./publish.sh` to push the standards for hm-linter 124 | * If you do not already have an AWS profile with access to the Linter Bot S3 bucket, make a request to the servers team for it before beginning this process. 125 | * If you use a non-default AWS profile for the Linter Bot, you can use `AWS_DEFAULT_PROFILE={name of AWS profile} ./publish.sh` instead. 126 | * This will ask if you want to bump the latest version to the new version. Only do this for patch releases. 127 | * To verify that the changes pushed correctly, you can run `aws s3 ls s3://hm-linter/standards/ --profile {name of AWS profile}` 128 | * For major and minor releases, publish a changelog to the Dev H2 (significant bugfixes may also warrant a post) 129 | * Publish a new release in the GitHub Release UI with the changes from CHANGELOG.md and update CHANGELOG.md with the release version and date. 130 | * After a cool-off period (typically one month), bump the latest version of the standards for Linter bot. 131 | * Checkout the Git tag for the release. 132 | * Verify that your local is clean of changes. 133 | * Run `./publish.sh` and bump `latest`. 134 | 135 | If you're releasing a major version, you should also create a branch for the major version so that bugfix releases can be created. This branch should be a humanised name of the version; e.g. 0.4 would be `oh-dot-four`, 1.6 would be `one-dot-six`. 136 | -------------------------------------------------------------------------------- /HM-Minimum/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Minimum requirements for HM projects. 4 | 5 | node_modules/* 6 | vendor/* 7 | 8 | 9 | 10 | ../HM/bootstrap.php 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | error 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | error 104 | eval() is a security risk and is not allowed. 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | error 114 | The "goto" language construct should not be used. 115 | 116 | 117 | 118 | 119 | 120 | error 121 | 122 | 123 | error 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 172 | 173 | 174 | 175 | 176 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | error 200 | 201 | 202 | 203 | %s() found. Errors should be logged via error_log() or trigger_error(). 204 | 205 | 206 | %s() found. Errors should be logged via error_log() or trigger_error(). 207 | 208 | 209 | %s() found. Errors should be logged via error_log() or trigger_error(). 210 | 211 | 212 | %s() found. Use error_log( wp_debug_backtrace_summary() ) instead. 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | error 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | Errors should not be silenced. Found: %s 267 | 268 | 269 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | %s data not unslashed before sanitization. Use wp_unslash() 301 | 302 | 303 | -------------------------------------------------------------------------------- /HM/Sniffs/Classes/OnlyClassInFileSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 34 | 35 | $classish = [ T_CLASS, T_INTERFACE, T_TRAIT ]; 36 | $first_class = $phpcsFile->findNext( $classish, 0 ); 37 | if ( empty( $first_class ) ) { 38 | // No class in file. 39 | return; 40 | } 41 | 42 | // Check for classes first... 43 | $other_declaration = $phpcsFile->findNext( $classish, $first_class + 1 ); 44 | if ( empty( $other_declaration ) ) { 45 | // ...then check for functions at the top-level. 46 | $other_declaration_start = 0; 47 | do { 48 | $other_declaration = $phpcsFile->findNext( [ T_FUNCTION ], $other_declaration_start ); 49 | $other_declaration_start = $other_declaration + 1; 50 | } while ( $other_declaration && $tokens[ $other_declaration ]['level'] > 0 ); 51 | } 52 | 53 | if ( ! empty( $other_declaration ) ) { 54 | $data = [ 55 | $tokens[ $first_class ]['line'], 56 | $tokens[ $other_declaration ]['content'], 57 | $tokens[ $other_declaration ]['line'], 58 | ]; 59 | $phpcsFile->addWarning($error, 0, 'FoundMultipleDeclarations', $data); 60 | $phpcsFile->recordMetric($stackPtr, 'Multiple declarations', 'yes'); 61 | } else { 62 | $phpcsFile->recordMetric($stackPtr, 'Multiple declarations', 'no'); 63 | } 64 | 65 | // Ignore the rest of the file. 66 | return $phpcsFile->numTokens + 1; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /HM/Sniffs/Debug/ESLintSniff.php: -------------------------------------------------------------------------------- 1 | getFilename(); 53 | $eslint_path = Config::getConfigData( 'eslint_path' ); 54 | if ( $eslint_path === null ) { 55 | return; 56 | } 57 | $config_file = $this->configFile; 58 | if ( empty( $config_file ) ) { 59 | // Attempt to autodetect. 60 | $candidates = glob( '.eslintrc{.js,.yaml,.yml,.json}', GLOB_BRACE ); 61 | if ( ! empty( $candidates ) ) { 62 | $config_file = $candidates[0]; 63 | } else { 64 | $config_file = static::DEFAULT_CONFIG; 65 | } 66 | } 67 | 68 | $eslint_options = [ 69 | sprintf( '--config %s', $config_file ), 70 | '--format json', 71 | ]; 72 | 73 | $cmd = sprintf( 74 | '"%s" %s "%s"', 75 | $eslint_path, 76 | implode( ' ', $eslint_options ), 77 | $filename 78 | ); 79 | $descriptors = [ 80 | 0 => [ 'pipe', 'r' ], 81 | 1 => [ 'pipe', 'w' ], 82 | 2 => [ 'pipe', 'w' ], 83 | ]; 84 | $env = array_merge( $_ENV, [ 85 | 'NODE_PATH' => dirname( dirname( dirname( __DIR__ ) ) ) . '/packages', 86 | ] ); 87 | $process = proc_open( $cmd, $descriptors, $pipes, null, $env ); 88 | 89 | // Ignore stdin. 90 | fclose( $pipes[0] ); 91 | $stdout = stream_get_contents( $pipes[1] ); 92 | $stderr = stream_get_contents( $pipes[2] ); 93 | fclose( $pipes[1] ); 94 | fclose( $pipes[2] ); 95 | 96 | // Close, and start working! 97 | $code = proc_close( $process ); 98 | 99 | if ( $code > 0 ) { 100 | $data = json_decode( $stdout ); 101 | // Detect errors: 102 | if ( json_last_error() !== JSON_ERROR_NONE ) { 103 | $error = 'Unable to run eslint: %s'; 104 | $phpcsFile->addError( $error, $stackPtr, 'CouldNotStart', [ $stdout ] ); 105 | } else { 106 | // Data is a list of files, but we only pass a single one. 107 | $messages = $data[0]->messages; 108 | foreach ( $messages as $error ) { 109 | if ( ! empty( $error->fatal ) || $error->severity === 2 ) { 110 | $phpcsFile->addErrorOnLine( $error->message, $error->line, $error->ruleId ); 111 | } else { 112 | $phpcsFile->addWarningOnLine( $error->message, $error->line, $error->ruleId ); 113 | } 114 | } 115 | } 116 | } 117 | 118 | // Ignore the rest of the file. 119 | return ($phpcsFile->numTokens + 1); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /HM/Sniffs/ExtraSniffCode.php: -------------------------------------------------------------------------------- 1 | phpcsFile->tokenizer->ignoredLines as $line => $ignored ) { 21 | $additional = []; 22 | 23 | if ( empty( $ignored ) ) { 24 | continue; 25 | } 26 | 27 | // Find any code which matches the legacy value. 28 | foreach ( $ignored as $code => $value ) { 29 | if ( preg_match( $expression, $code, $matches ) ) { 30 | // Duplicate as the new code. 31 | $new_code = $base_code; 32 | if ( ! empty( $matches[1] ) ) { 33 | $new_code .= $matches[1]; 34 | } 35 | 36 | $additional[ $new_code ] = $value; 37 | } 38 | } 39 | 40 | if ( ! empty( $additional ) ) { 41 | $this->phpcsFile->tokenizer->ignoredLines[ $line ] = array_merge( $ignored, $additional ); 42 | } 43 | } 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /HM/Sniffs/Files/ClassFileNameSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 31 | $namespace_ptr = $phpcsFile->findNext(T_NAMESPACE, 0); 32 | if ( ! $namespace_ptr ) { 33 | // Non-namespaced, skip check. 34 | return; 35 | } 36 | 37 | $class_name_ptr = $phpcsFile->findNext( T_STRING, $stackPtr ); 38 | 39 | $class_name = $tokens[ $class_name_ptr ]['content']; 40 | 41 | // Build a filename from the class name. 42 | $class_slug = str_replace( '_', '-', strtolower( $class_name ) ); 43 | $expected_filename = 'class-' . $class_slug . '.php'; 44 | 45 | $filename = basename( $phpcsFile->getFileName() ); 46 | if ( $filename !== $expected_filename ) { 47 | $error = 'Filename %s for class %s found; use %s instead'; 48 | $phpcsFile->addError( $error, $stackPtr, 'MismatchedName', [ $filename, $class_name, $expected_filename ] ); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /HM/Sniffs/Files/FunctionFileNameSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 17 | if ( $tokens[ $stackPtr ]['level'] !== 0 ) { 18 | // Ignore methods. 19 | return; 20 | } 21 | 22 | $namespace = $phpcsFile->findNext( T_NAMESPACE , 0); 23 | if ( empty( $namespace ) ) { 24 | // Non-namespaced function. 25 | return; 26 | } 27 | 28 | $filename = basename( $phpcsFile->getFileName() ); 29 | 30 | if ( $filename === 'namespace.php' ) { 31 | return; 32 | } 33 | 34 | // Get the trailing part of the namespace to match it against the file name. 35 | $trailing_namespace = $tokens[ $phpcsFile->findPrevious( T_STRING, $phpcsFile->findNext( T_SEMICOLON, $namespace ) ) ]; 36 | $expected_filename = str_replace( '_', '-', strtolower( $trailing_namespace['content'] ) ) . '.php'; 37 | if ( $filename === $expected_filename ) { 38 | return; 39 | } 40 | 41 | $error = 'Namespaced functions must be in namespace.php or $namespace.php'; 42 | $phpcsFile->addError($error, $stackPtr, 'WrongFile'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /HM/Sniffs/Files/NamespaceDirectoryNameSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 31 | $namespace = ''; 32 | 33 | $name_ptr = $phpcsFile->findNext( T_STRING, 0); 34 | if ( ! $name_ptr ) { 35 | // Non-namespaced, skip check. 36 | return; 37 | } 38 | 39 | do { 40 | $namespace .= $tokens[ $name_ptr ]['content']; 41 | $name_ptr++; 42 | } while ( in_array( $tokens[ $name_ptr ]['code'], [ T_STRING, T_NS_SEPARATOR ] ) ); 43 | 44 | $full = $phpcsFile->getFileName(); 45 | $filename = basename( $full ); 46 | $directory = dirname( $full ); 47 | 48 | // Normalize the directory separator across operating systems 49 | if ( DIRECTORY_SEPARATOR !== '/' ) { 50 | $directory = str_replace( DIRECTORY_SEPARATOR, '/', $directory ); 51 | } 52 | 53 | if ( $filename === 'plugin.php' || $filename === 'functions.php' || $filename === 'load.php' ) { 54 | // Ignore the main file. 55 | return; 56 | } 57 | 58 | if ( ! preg_match( '#(?:.*)(?:/inc|/tests)(/.*)?#', $directory, $matches ) ) { 59 | $error = 'Namespaced classes and functions should live inside an inc directory.'; 60 | $phpcsFile->addError( $error, $stackPtr, 'NoIncDirectory' ); 61 | return; 62 | } 63 | 64 | // Find correct after namespace-base path. 65 | $after_dir = $matches[1] ?? ''; 66 | 67 | if ( empty( $after_dir ) ) { 68 | // Base inc directory, skip checks. 69 | return; 70 | } 71 | 72 | $namespace_parts = explode( '\\', $namespace ); 73 | $directory_parts = explode( '/', trim( $after_dir, '/' ) ); 74 | 75 | // If we're evaluating a {namespace}.php file, pull the last namespace item off 76 | // the array as the file should match this last item. 77 | if ( $filename !== 'namespace.php' && stripos($filename, 'class-') === false ) { 78 | array_pop( $namespace_parts ); 79 | } 80 | 81 | // Check that the path matches the namespace, allowing parts to be dropped. 82 | while ( ! empty( $directory_parts ) ) { 83 | $dir_part = array_pop( $directory_parts ); 84 | $ns_part = array_pop( $namespace_parts ); 85 | 86 | if ( empty( $ns_part ) ) { 87 | // Ran out of namespace, but directory still has parts. 88 | $error = 'Directory %s for namespace %s found; nested too deep.'; 89 | $error_data = [ $after_dir, $namespace ]; 90 | $phpcsFile->addError( $error, $stackPtr, 'ExtraDirs', $error_data ); 91 | return; 92 | } 93 | 94 | // Check that this directory bit matches the namespace bit. 95 | // We allow namespaces to be CamelCase or separated by an underscore. 96 | if ( 97 | strtolower( $ns_part ) !== str_replace( '-', '_', $dir_part ) 98 | && strtolower( $ns_part ) !== str_replace( '-', '', $dir_part ) 99 | ) { 100 | $error = 'Directory %s for namespace %s found; use %s instead'; 101 | $error_data = [ $dir_part, $namespace, strtolower( $ns_part ) ]; 102 | $phpcsFile->addError( $error, $stackPtr, 'NameMismatch', $error_data ); 103 | return; 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /HM/Sniffs/Functions/NamespacedFunctionsSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 17 | if (isset($tokens[$stackPtr]['scope_closer']) === false) { 18 | return; 19 | } 20 | 21 | $errorData = array(strtolower($tokens[$stackPtr]['content'])); 22 | $namespace = $phpcsFile->findNext(array(T_NAMESPACE, T_FUNCTION), 0); 23 | if ($tokens[$namespace]['code'] !== T_NAMESPACE) { 24 | $error = 'Each %s must be in a namespace of at least one level (a top-level vendor name)'; 25 | $phpcsFile->addError($error, $stackPtr, 'MissingNamespace', $errorData); 26 | $phpcsFile->recordMetric($stackPtr, 'Function defined in namespace', 'no'); 27 | } else { 28 | $phpcsFile->recordMetric($stackPtr, 'Function defined in namespace', 'yes'); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /HM/Sniffs/Layout/OrderSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 17 | 18 | // Things we can look for: 19 | $look_for = [ 20 | T_NAMESPACE => 0, 21 | // then: 22 | T_USE => 1, 23 | // then: 24 | T_CONST => 2, 25 | // then any of: 26 | T_REQUIRE => 3, 27 | T_REQUIRE_ONCE => 3, 28 | T_INCLUDE => 3, 29 | T_INCLUDE_ONCE => 3, 30 | ]; 31 | 32 | // Which item are we looking for now? 33 | $current_score = 0; 34 | $current_token = [ 35 | 'content' => 'namespace', 36 | 'code' => T_NAMESPACE, 37 | 'line' => 0, 38 | ]; 39 | 40 | // Start looking. 41 | $next_pos = 0; 42 | while ( true ) { 43 | $next_pos = $phpcsFile->findNext( array_keys( $look_for ), $next_pos + 1 ); 44 | if ( empty( $next_pos ) ) { 45 | return; 46 | } 47 | 48 | $next_token = $tokens[ $next_pos ]; 49 | 50 | // Ignore nested `use` eg. in lambda functions. 51 | if ( $next_token['code'] === T_USE && $phpcsFile->findPrevious( T_CLOSURE, $next_pos, null, false, null, true ) !== false ) { 52 | continue; 53 | } 54 | if ( $next_token['code'] === T_USE && $phpcsFile->findPrevious( T_CLASS, $next_pos, null, false, null, true ) !== false ) { 55 | continue; 56 | } 57 | 58 | // Must be current or higher. 59 | $next_type_score = $look_for[ $next_token['code'] ]; 60 | if ( $next_type_score < $current_score ) { 61 | // ERROR! 62 | $error = '%s found on line %s, but %s was declared on line %s.'; 63 | $error .= ' Statements should be ordered `namespace`, `use`, `const`, `require`, then code.'; 64 | $data = [ $next_token['content'], $next_token['line'], $current_token['content'], $current_token['line'] ]; 65 | $phpcsFile->addError( $error, $stackPtr, 'WrongOrder', $data ); 66 | return; 67 | } 68 | 69 | // Adjust looking for. 70 | $current_score = $next_type_score; 71 | $current_token = $next_token; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /HM/Sniffs/Namespaces/NoLeadingSlashOnUseSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 17 | $look_for = [ T_STRING, T_NS_SEPARATOR ]; 18 | $next = $phpcsFile->findNext( $look_for, $stackPtr ); 19 | if ( $tokens[ $next ]['code'] === T_NS_SEPARATOR ) { 20 | $name = ''; 21 | do { 22 | $next++; 23 | $name .= $tokens[ $next ]['content']; 24 | } while ( in_array( $tokens[ $next + 1 ]['code'], $look_for ) ); 25 | 26 | $error = '`use` statement for class %s should not prefix with a backslash'; 27 | $phpcsFile->addError( $error, $stackPtr, 'LeadingSlash', [ $name ] ); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /HM/Sniffs/PHP/IssetSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 18 | 19 | $open_parenthesis_token = $phpcsFile->findNext( T_OPEN_PARENTHESIS, $stackPtr + 1 ); 20 | if ( $open_parenthesis_token === false ) { 21 | throw new RuntimeException( '$stackPtr was not a valid T_ISSET' ); 22 | } 23 | 24 | $comma_token = $phpcsFile->findNext( T_COMMA, $open_parenthesis_token + 1 ); 25 | if ( $comma_token !== false && $comma_token < $tokens[ $open_parenthesis_token ]['parenthesis_closer'] ) { 26 | $phpcsFile->addWarning( 'Only one argument should be used per ISSET call', $stackPtr, 'MultipleArguments' ); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /HM/Sniffs/Performance/SlowMetaQuerySniff.php: -------------------------------------------------------------------------------- 1 | array( 33 | 'type' => 'warning', 34 | 'message' => 'Querying by %s is not performant.', 35 | 'keys' => array( 36 | 'meta_query', 37 | 'meta_value', 38 | ), 39 | ), 40 | ); 41 | } 42 | 43 | /** 44 | * Process a token. 45 | * 46 | * Overrides the parent to store the stackPtr for later use. 47 | * 48 | * @param int $stackPtr 49 | */ 50 | public function process_token( $stackPtr ) { 51 | $this->stackPtr = $stackPtr; 52 | parent::process_token( $stackPtr ); 53 | unset( $this->stackPtr ); 54 | } 55 | 56 | /** 57 | * Callback to process each confirmed key, to check value. 58 | * This must be extended to add the logic to check assignment value. 59 | * 60 | * @param string $key Array index / key. 61 | * @param mixed $val Assigned value. 62 | * @param int $line Token line. 63 | * @param array $group Group definition. 64 | * @return mixed FALSE if no match, TRUE if matches, STRING if matches 65 | * with custom error message passed to ->process(). 66 | */ 67 | public function callback( $key, $val, $line, $group ) { 68 | switch ( $key ) { 69 | case 'meta_value': 70 | // When meta_value is specified, the query operates on the value, 71 | // and is hence expensive. (UNLESS: meta_compare is set) 72 | return true; 73 | 74 | case 'meta_query': 75 | return $this->check_meta_query(); 76 | 77 | default: 78 | // Unknown key, assume it's an error. 79 | return true; 80 | } 81 | } 82 | 83 | /** 84 | * Recursively check a meta_query value. 85 | */ 86 | protected function check_meta_query() { 87 | // Grab the token we're detecting. 88 | $token = $this->tokens[ $this->stackPtr ]; 89 | 90 | // Find the value of meta_query, and check it. 91 | $array_open = $this->phpcsFile->findNext( array_merge( Tokens::$emptyTokens, [ T_COMMA, T_CLOSE_SHORT_ARRAY ] ), $this->stackPtr + 1, null, true ); 92 | $this->check_meta_query_item( $array_open ); 93 | 94 | // Disable the built-in warnings. 95 | return false; 96 | } 97 | 98 | /** 99 | * Check an individual meta_query item. 100 | * 101 | * @param int $array_open Token pointer for the array open token. 102 | */ 103 | protected function check_meta_query_item( int $array_open ) { 104 | $array_open_token = $this->tokens[ $array_open ]; 105 | if ( $array_open_token['code'] !== T_ARRAY && $array_open_token['code'] !== T_OPEN_SHORT_ARRAY ) { 106 | // Dynamic value, we can't check. 107 | $this->addMessage( 108 | 'meta_query is dynamic, cannot be checked.', 109 | $array_open, 110 | 'warning', 111 | 'dynamic_query' 112 | ); 113 | 114 | return; 115 | } 116 | 117 | $array_bounds = $this->find_array_open_close( $array_open ); 118 | $elements = $this->get_array_indices( $array_bounds['opener'], $array_bounds['closer'] ); 119 | 120 | // Is this a "first-order" query? 121 | // @see WP_Meta_Query::is_first_order_clause 122 | $first_order_key = $this->find_key_in_array( $elements, 'key' ); 123 | $first_order_value = $this->find_key_in_array( $elements, 'value' ); 124 | if ( $first_order_key || $first_order_value ) { 125 | $compare_element = $this->find_key_in_array( $elements, 'compare' ); 126 | if ( ! empty( $compare_element ) ) { 127 | $compare = $this->get_static_value_for_element( $compare_element ); 128 | } 129 | if ( empty( $compare ) ) { 130 | // The default is either IN or = depending on whether value is 131 | // set, but this only matters for the message. 132 | $compare = 'default'; 133 | } 134 | 135 | $this->check_compare_value( $compare, $compare_element ? $compare_element['value_start'] : null ); 136 | return; 137 | } 138 | 139 | foreach ( $elements as $element ) { 140 | if ( isset( $element['index_start'] ) ) { 141 | $index = $this->strip_quotes( $this->tokens[ $element['index_start'] ]['content'] ); 142 | if ( strtolower( $index ) === 'relation' ) { 143 | // Skip 'relation' element. 144 | continue; 145 | } 146 | } 147 | 148 | // Otherwise, recurse. 149 | $this->check_meta_query_item( $element['value_start'] ); 150 | } 151 | } 152 | 153 | /** 154 | * Get a static value from an array. 155 | * 156 | * @param array $elements Elements from the array (from get_array_indices()) 157 | * @param string $array_key Key to find in the array. 158 | * @return string|null Static value if available, null otherwise. 159 | */ 160 | protected function get_static_value_for_element( array $element ) : ?string { 161 | // Got the compare, grab the value. 162 | $value_start = $element['value_start']; 163 | if ( $this->tokens[ $value_start ]['code'] !== T_CONSTANT_ENCAPSED_STRING ) { 164 | // Dynamic value. 165 | return static::DYNAMIC_VALUE; 166 | } 167 | 168 | $maybe_value_end = $this->phpcsFile->findNext( Tokens::$emptyTokens, $value_start + 1, null, true ); 169 | $expected_next = [ 170 | T_CLOSE_PARENTHESIS, 171 | T_CLOSE_SHORT_ARRAY, 172 | T_COMMA, 173 | ]; 174 | if ( ! in_array( $this->tokens[ $maybe_value_end ]['code'], $expected_next, true ) ) { 175 | // Dynamic value. 176 | return static::DYNAMIC_VALUE; 177 | } 178 | 179 | return $this->strip_quotes( $this->tokens[ $value_start ]['content'] ); 180 | } 181 | 182 | /** 183 | * Find a given key in an array. 184 | * 185 | * Searches a list of elements for a given (static) index. 186 | * 187 | * @param array $elements Elements from the array (from get_array_indices()) 188 | * @param string $array_key Key to find in the array. 189 | * @return string|null Static value if available, null otherwise. 190 | */ 191 | protected function find_key_in_array( array $elements, string $array_key ) : ?array { 192 | foreach ( $elements as $element ) { 193 | if ( ! isset( $element['index_start'] ) ) { 194 | // Numeric item, skip. 195 | continue; 196 | } 197 | 198 | // Ensure the index is a static string first. 199 | $start = $element['index_start']; 200 | if ( $this->tokens[ $start ]['code'] !== T_CONSTANT_ENCAPSED_STRING ) { 201 | // Dynamic key. 202 | continue; 203 | } 204 | 205 | $maybe_index_end = $this->phpcsFile->findNext( Tokens::$emptyTokens, $start + 1, null, true ); 206 | if ( $this->tokens[ $maybe_index_end ]['code'] !== T_DOUBLE_ARROW ) { 207 | // Dynamic key, maybe? This is probably not valid syntax. 208 | continue; 209 | } 210 | 211 | $index = $this->strip_quotes( $this->tokens[ $start ]['content'] ); 212 | if ( $index !== $array_key ) { 213 | // Not the item we want, skip. 214 | continue; 215 | } 216 | 217 | return $element; 218 | } 219 | 220 | return null; 221 | } 222 | 223 | /** 224 | * Get array indices information. 225 | * 226 | * @internal From phpcs' AbstractArraySniff::get_array_indices 227 | * 228 | * @param integer $array_start 229 | * @param integer $array_end 230 | * @return array 231 | */ 232 | protected function get_array_indices( int $array_start, int $array_end ) : array { 233 | $indices = []; 234 | 235 | $current = $array_start; 236 | while ( ( $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $current + 1 ), $array_end, true ) ) !== false ) { 237 | $end = $this->get_next( $this->phpcsFile, $next, $array_end ); 238 | 239 | if ( $this->tokens[ $end ]['code'] === T_DOUBLE_ARROW ) { 240 | $indexEnd = $this->phpcsFile->findPrevious( T_WHITESPACE, $end - 1, null, true ); 241 | $value_start = $this->phpcsFile->findNext( Tokens::$emptyTokens, $end + 1, null, true); 242 | 243 | $indices[] = [ 244 | 'index_start' => $next, 245 | 'index_end' => $indexEnd, 246 | 'arrow' => $end, 247 | 'value_start' => $value_start, 248 | ]; 249 | } else { 250 | $value_start = $next; 251 | $indices[] = [ 252 | 'value_start' => $value_start, 253 | ]; 254 | } 255 | 256 | $current = $this->get_next( $this->phpcsFile, $value_start, $array_end ); 257 | } 258 | 259 | return $indices; 260 | } 261 | 262 | /** 263 | * Add an error if the comparison isn't allowed. 264 | * 265 | * @param string $compare Comparison value 266 | */ 267 | protected function check_compare_value( string $compare, int $stackPtr = null ) : void { 268 | if ( empty( $stackPtr ) ) { 269 | $stackPtr = $this->stackPtr; 270 | } 271 | 272 | if ( $compare === static::DYNAMIC_VALUE ) { 273 | $this->addMessage( 274 | 'meta_query is using a dynamic comparison; this cannot be checked automatically, and may be non-performant.', 275 | $stackPtr, 276 | 'warning', 277 | 'dynamic_compare' 278 | ); 279 | } elseif ( $compare !== 'EXISTS' && $compare !== 'NOT EXISTS' ) { 280 | // Add a message ourselves. 281 | $this->addMessage( 282 | 'meta_query is using %s comparison, which is non-performant.', 283 | $stackPtr, 284 | 'warning', 285 | 'nonperformant_comparison', 286 | [ $compare ] 287 | ); 288 | } 289 | } 290 | 291 | /** 292 | * Find next separator in array - either: comma or double arrow. 293 | * 294 | * @internal From phpcs' AbstractArraySniff::getNext 295 | * 296 | * @param File $phpcsFile The current file being checked. 297 | * @param int $ptr The position of current token. 298 | * @param int $arrayEnd The token that ends the array definition. 299 | * 300 | * @return int 301 | */ 302 | protected function get_next( File $phpcsFile, $ptr, $arrayEnd ) { 303 | $tokens = $phpcsFile->getTokens(); 304 | 305 | while ( $ptr < $arrayEnd ) { 306 | if ( isset( $tokens[ $ptr ]['scope_closer']) === true ) { 307 | $ptr = $tokens[ $ptr ]['scope_closer']; 308 | } elseif ( isset( $tokens[ $ptr ]['parenthesis_closer'] ) === true ) { 309 | $ptr = $tokens[ $ptr ]['parenthesis_closer']; 310 | } elseif ( isset( $tokens[ $ptr ]['bracket_closer'] ) === true ) { 311 | $ptr = $tokens[ $ptr ]['bracket_closer']; 312 | } 313 | 314 | if ( $tokens[ $ptr ]['code'] === T_COMMA || $tokens[ $ptr ]['code'] === T_DOUBLE_ARROW ) { 315 | return $ptr; 316 | } 317 | 318 | ++$ptr; 319 | } 320 | 321 | return $ptr; 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /HM/Sniffs/Performance/SlowOrderBySniff.php: -------------------------------------------------------------------------------- 1 | array( 26 | 'type' => 'warning', 27 | 'message' => 'Ordering query results by %s is not performant.', 28 | 'keys' => array( 29 | 'orderby', 30 | ), 31 | ), 32 | ); 33 | } 34 | 35 | /** 36 | * Process a token. 37 | * 38 | * Overrides the parent to store the stackPtr for later use. 39 | * 40 | * @param int $stackPtr 41 | */ 42 | public function process_token( $stackPtr ) { 43 | $this->stackPtr = $stackPtr; 44 | parent::process_token( $stackPtr ); 45 | unset( $this->stackPtr ); 46 | } 47 | 48 | /** 49 | * Callback to process each confirmed key, to check value. 50 | * This must be extended to add the logic to check assignment value. 51 | * 52 | * @param string $key Array index / key. 53 | * @param mixed $val Assigned value. 54 | * @param int $line Token line. 55 | * @param array $group Group definition. 56 | * @return mixed FALSE if no match, TRUE if matches, STRING if matches 57 | * with custom error message passed to ->process(). 58 | */ 59 | public function callback( $key, $val, $line, $group ) { 60 | switch ( $val ) { 61 | case 'rand': 62 | case 'meta_value': 63 | case 'meta_value_num': 64 | $this->addMessage( 65 | 'Ordering query results by %s is not performant.', 66 | $this->stackPtr, 67 | 'warning', 68 | 'slow_order', 69 | [ $val ] 70 | ); 71 | 72 | // Skip built-in message. 73 | return false; 74 | 75 | default: 76 | // No match. 77 | return false; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /HM/Sniffs/Security/EscapeOutputSniff.php: -------------------------------------------------------------------------------- 1 | true, 27 | '_deprecated_constructor' => true, 28 | '_deprecated_file' => true, 29 | '_deprecated_function' => true, 30 | '_deprecated_hook' => true, 31 | '_doing_it_wrong' => true, 32 | 'trigger_error' => true, 33 | 'user_error' => true, 34 | ]; 35 | 36 | /** 37 | * Printing functions that incorporate unsafe values. 38 | * 39 | * This is overridden from the parent class to allow unescaped 40 | * translated text. 41 | * 42 | * @var array 43 | */ 44 | protected $unsafePrintingFunctions = []; 45 | 46 | /** 47 | * Constructor. 48 | * 49 | * Removes non-printing functions from the property. 50 | */ 51 | public function __construct() { 52 | // Remove error logging functions from output functions. 53 | foreach ( $this->hmSafePrintingFunctions as $function => $val ) { 54 | unset( $this->printingFunctions[ $function ] ); 55 | } 56 | } 57 | 58 | /** 59 | * Override init to duplicate any ignores. 60 | * 61 | * @param PhpcsFile $phpcsFile 62 | */ 63 | protected function init( PhpcsFile $phpcsFile ) { 64 | parent::init( $phpcsFile ); 65 | 66 | $this->duplicate_ignores( 'WordPress.Security.EscapeOutput' ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /HM/Sniffs/Security/NonceVerificationSniff.php: -------------------------------------------------------------------------------- 1 | allowQueryVariables ) { 38 | unset( $this->superglobals[ '$_GET' ] ); 39 | } 40 | 41 | $this->duplicate_ignores( 'WordPress.Security.NonceVerification' ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /HM/Sniffs/Security/ValidatedSanitizedInputSniff.php: -------------------------------------------------------------------------------- 1 | duplicate_ignores( 'WordPress.Security.ValidatedSanitizedInput' ); 45 | } 46 | 47 | /** 48 | * Process a token for validation and sanitisation. 49 | * 50 | * @param int $stackPtr 51 | * @return void 52 | */ 53 | public function process_token( $stackPtr ) { 54 | // Process our custom server rules first. 55 | if ( $this->tokens[ $stackPtr ]['content'] === '$_SERVER' ) { 56 | $pass = $this->check_server_variable( $stackPtr ); 57 | if ( $pass ) { 58 | // Variable is fine, skip upstream checks. 59 | return; 60 | } 61 | } 62 | 63 | // Not an allowed usage, so run the regular check on it. 64 | return parent::process_token( $stackPtr ); 65 | } 66 | 67 | /** 68 | * Check whether a $_SERVER variable is constant and allowed. 69 | * 70 | * @param int $stackPtr Current token to check. 71 | * @return bool True if this is a $_SERVER variable and is safe, false to run regular checks. 72 | */ 73 | protected function check_server_variable( $stackPtr ) { 74 | $key = $this->get_array_access_key( $stackPtr ); 75 | 76 | // Find the next non-whitespace token. 77 | $open_bracket = $this->phpcsFile->findNext( T_WHITESPACE, ( $stackPtr + 1 ), null, true ); 78 | if ( $this->tokens[ $open_bracket ]['code'] !== T_OPEN_SQUARE_BRACKET ) { 79 | // No index access, run regular checks. 80 | return false; 81 | } 82 | 83 | $index_token = $this->phpcsFile->findNext( T_WHITESPACE, ( $open_bracket + 1 ), null, true ); 84 | if ( $this->tokens[ $index_token ]['code'] !== T_CONSTANT_ENCAPSED_STRING ) { 85 | // Dynamic string, run regular checks. 86 | return false; 87 | } 88 | 89 | // Possible constant string, check there's no further dynamic parts. 90 | $maybe_close_bracket = $this->phpcsFile->findNext( T_WHITESPACE, ( $index_token + 1 ), null, true ); 91 | if ( $this->tokens[ $maybe_close_bracket ]['code'] !== T_CLOSE_SQUARE_BRACKET ) { 92 | // Dynamic string, run regular checks. 93 | return false; 94 | } 95 | 96 | // Constant string, check if it's allowed. 97 | $key = $this->strip_quotes( $this->tokens[ $index_token ]['content'] ); 98 | if ( ! in_array( $key, $this->allowedServerKeys, true ) ) { 99 | // Unsafe key, requires sanitising. 100 | return false; 101 | } 102 | 103 | // Safe key, allow it. 104 | return true; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /HM/Sniffs/Whitespace/MultipleEmptyLinesSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 43 | 44 | // Only continue if the line position is greater than a file opener line. 45 | if ( $stackPtr <= 2 ) { 46 | return; 47 | } 48 | 49 | if ( $tokens[ $stackPtr - 1 ]['line'] >= $tokens[ $stackPtr ]['line'] ) { 50 | return; 51 | } 52 | 53 | if ( $tokens[ $stackPtr - 2 ]['line'] !== $tokens[ $stackPtr - 1 ]['line'] ) { 54 | return; 55 | } 56 | 57 | // This is the first whitespace token on a line 58 | // and the line before this one is not empty, 59 | // so this could be the start of a multiple empty line block. 60 | $next = $phpcsFile->findNext( T_WHITESPACE, $stackPtr, null, true ); 61 | $lines = ( $tokens[ $next ]['line'] - $tokens[ $stackPtr ]['line'] ); 62 | 63 | // If there's only one whitespace line, this sniff does not apply. 64 | if ( $lines <= 1 ) { 65 | return; 66 | } 67 | 68 | // If the next non T_WHITESPACE token is more than 1 line away, 69 | // then there were multiple empty lines. 70 | $error = 'Multiple empty lines should not exist in a row; found %s consecutive empty lines'; 71 | $fix = $phpcsFile->addFixableError( 72 | $error, 73 | $stackPtr, 74 | 'MultipleEmptyLines', 75 | [ $lines ] 76 | ); 77 | 78 | // Only continue if we're in fixing mode. 79 | if ( $fix !== true ) { 80 | return; 81 | } 82 | 83 | $phpcsFile->fixer->beginChangeset(); 84 | $i = $stackPtr; 85 | while ( $tokens[ $i ]['line'] !== $tokens[ $next ]['line'] ) { 86 | $phpcsFile->fixer->replaceToken( $i, '' ); 87 | $i++; 88 | } 89 | 90 | $phpcsFile->fixer->addNewlineBefore( $i ); 91 | $phpcsFile->fixer->endChangeset(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /HM/Tests/Classes/OnlyClassInFileUnitTest.fail.class: -------------------------------------------------------------------------------- 1 | => 18 | */ 19 | public function getErrorList() { 20 | return []; 21 | } 22 | 23 | /** 24 | * Returns the lines where warnings should occur. 25 | * 26 | * @return array => 27 | */ 28 | public function getWarningList() { 29 | $file = func_get_arg( 0 ); 30 | list( $_, $type, $variant ) = explode( '.', $file, 3 ); 31 | if ( $type !== 'fail' ) { 32 | return []; 33 | } 34 | 35 | return [ 36 | 1 => 1, 37 | ]; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /HM/Tests/Classes/OnlyClassInFileUnitTest.success.const: -------------------------------------------------------------------------------- 1 | isDot() ) { 27 | continue; 28 | } 29 | 30 | $test_files[] = $file->getPathname(); 31 | } 32 | 33 | // Put them in order. 34 | sort( $test_files ); 35 | 36 | return $test_files; 37 | } 38 | 39 | /** 40 | * Returns the lines where errors should occur. 41 | * 42 | * @return array => 43 | */ 44 | public function getErrorList() { 45 | $file = func_get_arg( 0 ); 46 | $pass = [ 47 | 'class-test.php', 48 | 'class-two-parts.php', 49 | ]; 50 | if ( in_array( $file, $pass, true ) ) { 51 | return []; 52 | } 53 | return [ 54 | 5 => 1, 55 | ]; 56 | } 57 | 58 | /** 59 | * Returns the lines where warnings should occur. 60 | * 61 | * @return array => 62 | */ 63 | public function getWarningList() { 64 | return []; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /HM/Tests/Files/ClassFileNameUnitTest/class-Foo.php: -------------------------------------------------------------------------------- 1 | isDot() ) { 27 | continue; 28 | } 29 | 30 | $test_files[] = $file->getPathname(); 31 | } 32 | 33 | // Put them in order. 34 | sort( $test_files ); 35 | 36 | return $test_files; 37 | } 38 | 39 | /** 40 | * Returns the lines where errors should occur. 41 | * 42 | * @return array => 43 | */ 44 | public function getErrorList() { 45 | $file = func_get_arg( 0 ); 46 | $pass = [ 47 | 'namespace.php', 48 | 'matching-namespace.php', 49 | ]; 50 | if ( in_array( $file, $pass, true ) ) { 51 | return []; 52 | } 53 | return [ 54 | 5 => 1, 55 | ]; 56 | } 57 | 58 | /** 59 | * Returns the lines where warnings should occur. 60 | * 61 | * @return array => 62 | */ 63 | public function getWarningList() { 64 | return []; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /HM/Tests/Files/FunctionFileNameUnitTest/matching-namespace.php: -------------------------------------------------------------------------------- 1 | isFile() ) { 30 | continue; 31 | } 32 | 33 | $test_files[] = $file->getPathname(); 34 | } 35 | 36 | // Put them in order. 37 | sort( $test_files ); 38 | 39 | return $test_files; 40 | } 41 | 42 | /** 43 | * Returns the lines where errors should occur. 44 | * 45 | * @return array => 46 | */ 47 | public function getErrorList() { 48 | $file = func_get_arg( 0 ); 49 | $pass = [ 50 | 'grinder.php', 51 | 'namespace.php', 52 | 'camelcased-namespace.php', 53 | 'underscored-namespace.php', 54 | ]; 55 | if ( in_array( $file, $pass, true ) ) { 56 | return []; 57 | } else { 58 | return [ 59 | 3 => 1, 60 | ]; 61 | } 62 | } 63 | 64 | /** 65 | * Returns the lines where warnings should occur. 66 | * 67 | * @return array => 68 | */ 69 | public function getWarningList() { 70 | return []; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /HM/Tests/Files/NamespaceDirectoryNameUnitTest/inc/coffee/more/fail.php: -------------------------------------------------------------------------------- 1 | => 18 | */ 19 | public function getErrorList() { 20 | return [ 21 | 1 => 1, 22 | ]; 23 | } 24 | 25 | /** 26 | * Returns the lines where warnings should occur. 27 | * 28 | * @return array => 29 | */ 30 | public function getWarningList() { 31 | return []; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /HM/Tests/Namespaces/NoLeadingSlashOnUseUnitTest.fail: -------------------------------------------------------------------------------- 1 | => 18 | */ 19 | public function getErrorList() { 20 | $file = func_get_arg( 0 ); 21 | switch ( $file ) { 22 | case 'NoLeadingSlashOnUseUnitTest.success': 23 | return []; 24 | 25 | case 'NoLeadingSlashOnUseUnitTest.fail': 26 | return [ 27 | 5 => 1, 28 | 6 => 1, 29 | ]; 30 | } 31 | } 32 | 33 | /** 34 | * Returns the lines where warnings should occur. 35 | * 36 | * @return array => 37 | */ 38 | public function getWarningList() { 39 | return []; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /HM/Tests/Namespaces/NoLeadingSlashOnUseUnitTest.success: -------------------------------------------------------------------------------- 1 | => 18 | */ 19 | public function getErrorList() { 20 | $file = func_get_arg( 0 ); 21 | switch ( $file ) { 22 | case 'MultipleEmptyLinesUnitTest.success': 23 | return []; 24 | 25 | case 'MultipleEmptyLinesUnitTest.fail': 26 | return [ 27 | 4 => 1, 28 | 11 => 1, 29 | 17 => 1, 30 | 22 => 1, 31 | ]; 32 | } 33 | } 34 | 35 | /** 36 | * Returns the lines where warnings should occur. 37 | * 38 | * @return array => 39 | */ 40 | public function getWarningList() { 41 | return []; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /HM/Tests/Whitespace/MultipleEmptyLinesUnitTest.success: -------------------------------------------------------------------------------- 1 | config->files; 57 | $ignored = $runner->config->ignored; 58 | 59 | // Find exclusion files. 60 | $did_change = false; 61 | foreach ( $paths as $path ) { 62 | // Only use ignore files for directories. 63 | if ( ! is_dir( $path ) ) { 64 | continue; 65 | } 66 | 67 | // Find an ignore file. 68 | $directory = $path; 69 | $ignore_file = $directory . '/.phpcsignore'; 70 | if ( ! file_exists( $ignore_file ) ) { 71 | continue; 72 | } 73 | if ( PHP_CODESNIFFER_VERBOSITY > 1 ) { 74 | echo "\tAdding exclusion rules from $ignore_file\n"; 75 | } 76 | 77 | $extra_ignores = get_ignores_from_file( $ignore_file, $directory ); 78 | if ( PHP_CODESNIFFER_VERBOSITY > 1 ) { 79 | foreach ( $extra_ignores as $rule ) { 80 | echo "\t\t=> $rule\n"; 81 | } 82 | } 83 | 84 | $did_change = true; 85 | $ignored = array_merge( $ignored, $extra_ignores ); 86 | } 87 | 88 | if ( $did_change ) { 89 | $runner->config->ignored = $ignored; 90 | } 91 | } 92 | 93 | if ( ! empty( $GLOBALS['runner'] ) ) { 94 | attach_to_runner( $GLOBALS['runner'] ); 95 | } 96 | -------------------------------------------------------------------------------- /HM/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Human Made coding standards. 4 | 5 | node_modules/* 6 | vendor/* 7 | 8 | 9 | 10 | bootstrap.php 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | error 94 | 95 | 96 | 97 | 98 | error 99 | Scheduling crons at %s sec ( less than %s minutes ) is prohibited. 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 0 123 | 124 | 125 | 0 126 | 127 | 128 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 13 | 14 | 15 | 18 | 21 | 22 |
4 | Human Made Coding Standards
5 | WordPress coding standards, enhanced for modern development. 6 |
8 | 9 | 10 | 11 | Build Status 12 |
16 | A Human Made project. 17 | 19 | 20 |
23 | 24 | This is a codified version of [the Human Made style guide](http://engineering.hmn.md/how-we-work/style/). We include phpcs, ESLint, and stylelint rules. 25 | 26 | ## Contributing 27 | 28 | We welcome contributions to these standards and want to make the experience as seamless as possible. To learn more about contributing, please reference the [CONTRIBUTING.md](CONTRIBUTING.md) file. 29 | 30 | ## Setup 31 | 32 | Each ruleset is available individually via Composer or NPM. To install the needed ruleset, use one of the following commands: 33 | 34 | - PHPCS: `composer require --dev humanmade/coding-standards` 35 | - ESLint: `npx install-peerdeps --dev @humanmade/eslint-config@latest` 36 | - stylelint: `npm install --save-dev stylelint @humanmade/stylelint-config` 37 | 38 | ## Using PHPCS 39 | 40 | Run the following command to run the standards checks: 41 | 42 | ``` 43 | vendor/bin/phpcs --standard=vendor/humanmade/coding-standards . 44 | ``` 45 | 46 | We use the [DealerDirect phpcodesniffer-composer-installer](https://github.com/Dealerdirect/phpcodesniffer-composer-installer) package to handle `installed_paths` for PHPCS when first installing the HM ruleset. If you an error such as `ERROR: Referenced sniff "WordPress-Core" does not exist`, delete the `composer.lock` file and `vendor` directories and re-install Composer dependencies. 47 | 48 | The final `.` here specifies the files you want to test; this is typically the current directory (`.`), but you can also selectively check files or directories by specifying them instead. 49 | 50 | You can add this to your Travis YAML file as a test: 51 | 52 | ```yaml 53 | script: 54 | - phpunit 55 | - vendor/bin/phpcs --standard=vendor/humanmade/coding-standards . 56 | ``` 57 | 58 | ### Excluding Files 59 | 60 | This standard includes special support for a `.phpcsignore` file (in the future, this should be [built into phpcs itself](https://github.com/squizlabs/PHP_CodeSniffer/issues/1884)). Simply place a `.phpcsignore` file in your root directory (wherever you're going to run `phpcs` from). 61 | 62 | The format of this file is similar to `.gitignore` and similar files: one pattern per line, comment lines should start with a `#`, and whitespace-only lines are ignored: 63 | 64 | ``` 65 | # Exclude our tests directory. 66 | tests/ 67 | 68 | # Exclude any file ending with ".inc" 69 | *\.inc 70 | ``` 71 | 72 | Note that the patterns should match [the PHP_CodeSniffer style](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Advanced-Usage#ignoring-files-and-folders): `*` is translated to `.*` for convenience, but all other characters work like a regular expression. 73 | 74 | Patterns are relative to the directory that the `.phpcsignore` file lives in. On load, they are translated to absolute patterns: e.g. `*/tests/*` in `/your/dir/.phpcsignore` will become `/your/dir/.*/tests/.*` as a regular expression. **This differs from the regular PHP_CodeSniffer practice.** 75 | 76 | 77 | ### Advanced/Extending 78 | 79 | If you want to add further rules (such as WordPress.com VIP-specific rules) or customize PHPCS defaults, you can create your own custom standard file (e.g. `phpcs.ruleset.xml`): 80 | 81 | ```xml 82 | 83 | 84 | 85 | . 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | ``` 100 | 101 | You can then reference this file when running phpcs: 102 | 103 | ``` 104 | vendor/bin/phpcs --standard=phpcs.ruleset.xml . 105 | ``` 106 | 107 | 108 | #### Excluding/Disabling Checks 109 | 110 | You can also customise the rule to exclude elements if they aren't applicable to the project: 111 | 112 | ```xml 113 | 114 | 115 | 116 | 117 | ``` 118 | 119 | Rules can also be disabled inline. [phpcs rules can be disabled](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Advanced-Usage#ignoring-parts-of-a-file) with a `// @codingStandardsIgnoreLine` comment, and [ESLint rules can be disabled](http://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-comments) with a `/* eslint disable ... */` comment. 120 | 121 | To find out what these codes are, specify `-s` when running `phpcs`, and the code will be output as well. You can specify a full code, or a partial one to disable groups of errors. 122 | 123 | 124 | ## Included Checks 125 | 126 | The phpcs standard is based upon the `WordPress-VIP` standard from [WordPress Coding Standards](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards), with [customisation and additions](HM/ruleset.xml) to match our style guide. 127 | 128 | ## Using ESLint 129 | 130 | The ESLint package contains an [ESLint](https://eslint.org/) configuration which you can use to validate your JavaScript code style. While it is possible to run ESLint via phpcs, we recommend you install and use eslint via npm directly or use [linter-bot](https://github.com/humanmade/linter-bot). See [the `@humanmade/eslint-config` package README](packages/eslint-config-humanmade/readme.md) for more information on configuring ESLint to use the Human Made coding standards. 131 | 132 | Once you have installed the [`@humanmade/eslint-config` npm package](https://www.npmjs.com/package/@humanmade/eslint-config), you may simply specify that your own project-level ESLint file extends the `humanmade` configuration. If you install this globally (`npm install -g @humanmade/eslint-config`) you can also reference the configuration directly from the command line via `eslint -c humanmade .` 133 | 134 | Alternatively, you can create your own configuration and extend these rules: 135 | 136 | `.eslintrc` 137 | ```json 138 | { 139 | "extends": "@humanmade" 140 | } 141 | ``` 142 | 143 | ## Using stylelint 144 | 145 | The stylelint package contains a [stylelint](https://stylelint.io/) configuration which you can use to validate your CSS and SCSS code style. We recommend you install and use stylelint via npm directly or use [linter-bot](https://github.com/humanmade/linter-bot). See [the `@humanmade/stylelint` package README](packages/stylelint-config/readme.md) for more information on configuring stylelint to use the Human Made coding standards. 146 | 147 | To integrate the Human Made rules into your project, add a `.stylelintrc` file and extend these rules. You can also add your own rules and overrides for further customization. 148 | 149 | ```json 150 | { 151 | "extends": "@humanmade/stylelint-config", 152 | "rules": { 153 | ... 154 | } 155 | } 156 | ``` 157 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "humanmade/coding-standards", 3 | "description": "Human Made Coding Standards", 4 | "type": "phpcodesniffer-standard", 5 | "license": "GPL-2.0-or-later", 6 | "require": { 7 | "php": ">=7.1", 8 | "wp-coding-standards/wpcs": "2.3.0", 9 | "automattic/vipwpcs": "2.0.0", 10 | "fig-r/psr2r-sniffer": "^0.5.0", 11 | "phpcompatibility/phpcompatibility-wp": "^2.0.0", 12 | "squizlabs/php_codesniffer": "~3.5", 13 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0" 14 | }, 15 | "require-dev": { 16 | "phpunit/phpunit": "^7" 17 | }, 18 | "scripts": { 19 | "test": "phpunit" 20 | }, 21 | "config": { 22 | "allow-plugins": { 23 | "dealerdirect/phpcodesniffer-composer-installer": true 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "command": { 3 | "publish": { 4 | "message": "chore(release): publish" 5 | } 6 | }, 7 | "ignoreChanges": [ 8 | "**/CHANGELOG.md", 9 | "**/{fixtures,test}/**" 10 | ], 11 | "packages": [ 12 | "packages/*" 13 | ], 14 | "version": "1.2.1" 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@humanmade/coding-standards", 3 | "version": "1.1.0", 4 | "private": true, 5 | "description": "Human Made Coding Standards.", 6 | "author": "Human Made", 7 | "license": "GPL-2.0-or-later", 8 | "keywords": [ 9 | "scripts", 10 | "eslint", 11 | "stylelint", 12 | "npm-package-json-lint", 13 | "lint", 14 | "linter", 15 | "humanmade" 16 | ], 17 | "homepage": "https://github.com/humanmade/coding-standards", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/humanmade/coding-standards.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/humanmade/coding-standards/issues" 24 | }, 25 | "engines" : { 26 | "npm" : ">=7.0.0", 27 | "node" : ">=16.0.0" 28 | }, 29 | "npmpackagejsonlint": { 30 | "extends": "@wordpress/npm-package-json-lint-config", 31 | "rules": { 32 | "description-format": [ 33 | "error", 34 | { 35 | "requireCapitalFirstLetter": true, 36 | "requireEndingPeriod": true 37 | } 38 | ], 39 | "prefer-no-devDependencies": "warning", 40 | "require-publishConfig": "error", 41 | "require-repository-directory": "error", 42 | "valid-values-author": [ 43 | "error", 44 | [ 45 | "Human Made" 46 | ] 47 | ], 48 | "valid-values-publishConfig": [ 49 | "error", 50 | [ 51 | { 52 | "access": "public" 53 | } 54 | ] 55 | ] 56 | }, 57 | "overrides": [ 58 | { 59 | "patterns": [ 60 | "./package.json" 61 | ], 62 | "rules": { 63 | "require-publishConfig": "off", 64 | "require-repository-directory": "off", 65 | "prefer-no-devDependencies": "off" 66 | } 67 | } 68 | ] 69 | }, 70 | "devDependencies": { 71 | "@wordpress/npm-package-json-lint-config": "^4.0.5", 72 | "lerna": "^5.5.0", 73 | "npm-package-json-lint": "^5.1.0" 74 | }, 75 | "scripts": { 76 | "lint-pkg-json": "npmPkgJsonLint . 'packages/*/package.json'", 77 | "publish:check": "lerna updated", 78 | "publish:dev": "lerna publish --dist-tag next", 79 | "publish:legacy": "lerna publish --dist-tag legacy", 80 | "publish:prod": "lerna publish", 81 | "test": "npm run lint-pkg-json && npm run test:eslint && npm run test:stylelint", 82 | "test:eslint": "cd packages/eslint-config-humanmade && npm test", 83 | "test:stylelint": "cd packages/stylelint-config && npm test" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | # Managing Packages 2 | 3 | This repository uses [lerna] to manage Human Made modules and publish them as packages to [npm]. This enforces certain steps in the workflow which are described below. 4 | 5 | ## Managing Dependencies 6 | 7 | There are two types of dependencies that you might want to add to one of the existing Human Made packages. 8 | 9 | ### Production Dependencies 10 | 11 | Production dependencies are stored in the `dependencies` section of the package’s `package.json` file. 12 | 13 | #### Adding New Dependencies 14 | 15 | The simplest way to add a production dependency to one of the packages is to run a very convenient [lerna add](https://github.com/lerna/lerna/tree/master/commands/add#readme) command from the root of the project. 16 | 17 | _Example:_ 18 | 19 | ```shell 20 | lerna add stylelint-csstree-validator packages/stylelint-config 21 | ``` 22 | 23 | This command adds the latest version of `stylelint-csstree-validator` as a dependency to the `@humanmade/stylelint-config` package, which is located in `packages/stylelint-config` folder. 24 | 25 | #### Removing Existing Dependencies 26 | 27 | Removing a dependency from one of the Human Made packages requires some manual work. You need to remove the line in the corresponding `dependencies` section of the `package.json` file. 28 | 29 | _Example:_ 30 | ```diff 31 | +++ b/packages/eslint-config-humanmade/package.json 32 | @@ -33,7 +33,6 @@ 33 | "chalk": "^2.4.1", 34 | "eslint": "^5.10.0", 35 | "eslint-config-react-app": "^3.0.5", 36 | - "eslint-plugin-flowtype": "^3.2.0", 37 | "eslint-plugin-import": "^2.14.0", 38 | "eslint-plugin-jsx-a11y": "^6.1.2", 39 | "eslint-plugin-react": "^7.11.1" 40 | ``` 41 | 42 | Next, you need to run `npm install` in the root of the project to ensure that `package-lock.json` file gets properly regenerated. 43 | 44 | #### Updating Existing Dependencies 45 | 46 | This is the most confusing part of working with [lerna] which causes a lot of hassles for contributors. The most successful strategy so far is to do the following: 47 | 1. First, remove the existing dependency as described in the previous section. 48 | 2. Next, add the same dependency back as described in the first section of this chapter. This time it wil get the latest version applied unless you enforce a different version explicitly. 49 | 50 | ### Development Dependencies 51 | 52 | In contrast to production dependencies, development dependencies shouldn't be stored in individual Human Made packages. Instead they should be installed in the project's `package.json` file using the usual `npm install` command. In effect, all development tools are configured to work with every package at the same time to ensure they share the same characteristics and integrate correctly with each other. 53 | 54 | _Example:_ 55 | 56 | ```shell 57 | npm install glob --save-dev 58 | ``` 59 | 60 | This commands adds the latest version of `glob` as a development dependency to the `package.json` file. It has to be executed from the root of the project. 61 | 62 | ## Maintaining Changelogs 63 | 64 | In maintaining npm packages it can be tough to keep track of changes. To simplify the release process the project root contains a `CHANGELOG.md` file which details all published releases and the unreleased ("Master") changes, if any exist. 65 | 66 | For each pull request, you should always include relevant changes in a "Master" heading at the top of the file. You should add the heading if it doesn't already exist. 67 | 68 | _Example:_ 69 | 70 | ```md 71 | ## Unreleased (1.2.3) 72 | 73 | ### Added: 74 | - Added stylelint `stylelint-csstree-validator` plugin to `@humanmade/stylelint-config` 75 | 76 | ### Removed 77 | - Removed ESLint `eslint-plugin-flowtype` config from `eslint-plugin-humanmade` 78 | ``` 79 | 80 | There are a number of common release subsections you can follow. Each is intended to align to a specific meaning in the context of the [Semantic Versioning (`semver`) specification](https://semver.org/) the project adheres to. It is important that you describe your changes accurately, since this is used in the packages release process to help determine the version of the next release. 81 | 82 | - "Breaking Change" - A backwards-incompatible change which requires specific attention of the impacted developers to reconcile (requires a major version bump). 83 | - "New Feature" - The addition of a new backwards-compatible function or feature to the existing public API (requires a minor verison bump). 84 | - "Enhancement" - Backwards-compatible improvements to existing functionality (requires a minor version bump). 85 | - "Bug Fix" - Resolutions to existing buggy behavior (requires a patch version bump). 86 | - "Internal" - Changes which do not have an impact on the public interface or behavior of the module (requires a patch version bump). 87 | 88 | While other section naming can be used when appropriate, it's important that the subsections are expressed clearly to avoid confusion for both the packages releaser and third-party consumers. 89 | 90 | When in doubt, refer to [Semantic Versioning specification](https://semver.org/). 91 | 92 | ## Releasing Packages 93 | 94 | Lerna automatically releases all outdated packages. To check which packages are outdated and will be released, type `npm run publish:check`. 95 | 96 | If you have the ability to publish packages, you _must_ have [2FA enabled](https://docs.npmjs.com/getting-started/using-two-factor-authentication) on your [npm account][npm]. 97 | 98 | ### Before Releasing 99 | 100 | Confirm that you're logged in to [npm], by running `npm whoami`. If you're not logged in, run `npm adduser` to login. 101 | 102 | If you're publishing a new package, ensure that its `package.json` file contains the correct `publishConfig` settings: 103 | 104 | ```json 105 | { 106 | "publishConfig": { 107 | "access": "public" 108 | } 109 | } 110 | ``` 111 | 112 | You can check your package configs by running `npm run lint-pkg-json`. 113 | 114 | ### Development Release 115 | 116 | Run the following command to release a dev version of the outdated packages. 117 | 118 | ```shell 119 | npm run publish:dev 120 | ``` 121 | 122 | Lerna will ask you which version number you want to choose for each package. For a `dev` release, you'll more likely want to choose the "prerelease" option. Repeat the same for all the outdated packages and confirm your version updates. 123 | 124 | Lerna will then publish to [npm], commit the `package.json` changes and create the git tags. 125 | 126 | ### Production Release 127 | 128 | To release a production version for the outdated packages, run the following command: 129 | 130 | ```shell 131 | npm run publish:prod 132 | ``` 133 | 134 | Choose the correct version based on `CHANGELOG.md` files, confirm your choices and let Lerna do its magic. 135 | 136 | ## Credits 137 | 138 | Thanks to the WordPress and Gutenberg contributors to which this [document is based on](https://github.com/WordPress/gutenberg/blob/master/packages/README.md). 139 | 140 | [lerna]: https://lerna.js.org/ 141 | [npm]: https://www.npmjs.com/ 142 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./index.js" 3 | } 4 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/fail/component-jsx-parentheses.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/require-jsdoc */ 2 | import React from 'react'; 3 | 4 | const World = () => World; 5 | 6 | const Hello = () => (
7 |

Hello

8 |
); 9 | 10 | export default class Test extends React.Component { 11 | render() { 12 | return
13 | 14 |
; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/fail/import-order.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import apiFetch from '@wordpress/api-fetch'; 4 | import chalk from 'chalk'; 5 | import eslint from 'eslint'; 6 | import path from 'path'; 7 | import './'; 8 | import Test from './component-jsx-parentheses'; 9 | import './style.scss'; 10 | import index from '../../index'; 11 | import '../test-lint-config'; 12 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/fail/jsx-boolean-value.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/require-jsdoc */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | import React from 'react'; 5 | 6 | const A = () => ( 7 |
8 | ); 9 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/fail/jsx-curly-newline.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/require-jsdoc */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | import React from 'react'; 5 | 6 | const foo = 'foo'; 7 | 8 | const A = () => ( 9 |
10 | { foo 11 | } 12 |
13 | ); 14 | 15 | const B = () => ( 16 |
17 | { 18 | foo } 19 |
20 | ); 21 | 22 | const C = () => ( 23 |
24 | { foo && 25 | foo.bar 26 | } 27 |
28 | ); 29 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/fail/semicolon.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/require-jsdoc */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | // Missing semicolon. 5 | const testMissingSemicolon = 'foo' 6 | 7 | // Incorrect space before semicolon. 8 | const testSemiSpacing = 'bar' ; 9 | 10 | // Double semicolon. 11 | const testDoubleSemiColon = 5;; 12 | 13 | const testFunc = function () { 14 | // ... 15 | } 16 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/fail/template-curly-spacing.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | 3 | const foo = 42; 4 | 5 | `${foo}`; 6 | `${ foo}`; 7 | `${foo }`; 8 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/fail/variable-declaration.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/require-jsdoc */ 2 | let obj = [ 'only assigned once' ]; 3 | 4 | var str = 'default value'; 5 | const str2 = `${ str }, but declared with const`; 6 | if ( obj[0] === global.condition ) { 7 | str2 = 'other value maybe assigned later'; 8 | } 9 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/pass/component-jsx-parentheses.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * @returns {React.ReactNode} Rendered component. 5 | */ 6 | const World = () => World; 7 | 8 | /** 9 | * @returns {React.ReactNode} Rendered component. 10 | */ 11 | const Hello = () => ( 12 |
13 |

Hello

14 |
15 | ); 16 | 17 | /** 18 | * Test class. 19 | */ 20 | export default class Test extends React.Component { 21 | render() { 22 | return ( 23 |
24 | 25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/pass/import-order.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import path from 'path'; 4 | 5 | import chalk from 'chalk'; 6 | import eslint from 'eslint'; 7 | 8 | import apiFetch from '@wordpress/api-fetch'; 9 | 10 | import index from '../../index'; 11 | import '../test-lint-config'; 12 | 13 | import Test from './component-jsx-parentheses'; 14 | 15 | import './'; 16 | import './style.scss'; 17 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/pass/jsdoc-inline-arrow.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | const result = [].map( val => val.subval ); 4 | 5 | const filteredThing = [] 6 | .filter( item => item.isIncludedInSet ) 7 | .reduce( ( sum, item ) => ( sum + item.immenseValue ), 0 ); 8 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/pass/jsx-boolean-value.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import React from 'react'; 4 | 5 | /** 6 | * @returns {React.ReactNode} Rendered component. 7 | */ 8 | const A = () => ( 9 |
10 | ); 11 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/pass/jsx-curly-newline.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import React from 'react'; 4 | 5 | const foo = 'foo'; 6 | const bar = 'bar'; 7 | 8 | /** 9 | * @returns {React.ReactNode} Rendered component. 10 | */ 11 | const A = () => ( 12 |
13 | { foo } 14 |
15 | ); 16 | 17 | /** 18 | * @returns {React.ReactNode} Rendered component. 19 | */ 20 | const B = () => ( 21 |
22 | { 23 | foo 24 | } 25 |
26 | ); 27 | 28 | /** 29 | * @returns {React.ReactNode} Rendered component. 30 | */ 31 | const C = () => ( 32 |
33 | { foo && ( 34 | foo.bar 35 | ) } 36 | 37 | { foo ? ( 38 | foo.bar 39 | ) : null } 40 | 41 | { foo ? ( 42 | foo.bar 43 | ) : bar } 44 | 45 | { foo ? ( 46 | foo.bar 47 | ) : ( 48 | bar 49 | ) } 50 |
51 | ); 52 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/pass/semicolon.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | // Missing semicolon. 4 | const testMissingSemicolon = 'foo'; 5 | 6 | // Double semicolon. 7 | const testDoubleSemiColon = 5; 8 | 9 | /** */ 10 | const testFunc = function () { 11 | // ... 12 | }; 13 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/pass/template-curly-spacing.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | 3 | const foo = 42; 4 | 5 | `${ foo }`; 6 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/fixtures/test-lint-config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | process.on( 'unhandledRejection', err => { 3 | throw err; 4 | } ); 5 | 6 | const chalk = require( 'chalk' ); 7 | const join = require( 'path' ).join; 8 | const { CLIEngine } = require( 'eslint' ); 9 | 10 | const cli = new CLIEngine( { useEslintrc: true } ); 11 | const formatter = CLIEngine.getFormatter(); 12 | 13 | const verbose = process.argv.indexOf( '--verbose' ) > -1; 14 | 15 | /** Utility to count errors, warnings & processed file count. */ 16 | const count = results => results.reduce( ( counts, file ) => ( { 17 | errors: counts.errors + file.errorCount, 18 | warnings: counts.warnings + file.warningCount, 19 | files: counts.files + 1, 20 | } ), { 21 | errors: 0, 22 | warnings: 0, 23 | files: 0, 24 | } ); 25 | 26 | console.log( 'Running ESLint on fixture directories. Use --verbose for a detailed report.' ); 27 | console.log( '\nLinting `fixtures/fail/**`...' ); 28 | 29 | const antipatternReport = cli.executeOnFiles( [ join( __dirname, 'fail/**' ) ] ); 30 | const antipatternCounts = count( antipatternReport.results ); 31 | const allFail = antipatternReport.results.reduce( ( didFail, file ) => didFail = didFail && ( file.errorCount > 0 || file.warningCount > 0 ), true ); 32 | 33 | if ( allFail ) { 34 | console.log( chalk.green( 'ESLint logs errors as expected.\n' ) ); 35 | } else if ( antipatternCounts.errors ) { 36 | console.log( chalk.bold.red( 'The following files did not produce errors:' ) ); 37 | antipatternReport.results.forEach( file => { 38 | if ( file.errorCount > 0 || file.warningCount > 0 ) { 39 | return; 40 | } 41 | 42 | console.log( ' ' + file.filePath ); 43 | } ); 44 | console.log( '' ); 45 | 46 | process.exitCode = 1; 47 | } else { 48 | console.log( chalk.bold.red( 'Errors expected, but none encountered!\n' ) ); 49 | process.exitCode = 1; 50 | } 51 | 52 | // Log full report when --verbose, or when no errors are reported. 53 | if ( verbose || ! antipatternCounts.errors ) { 54 | console.log( formatter( antipatternReport.results ) ); 55 | } 56 | 57 | console.log( 'Linting `fixtures/pass/**`...' ); 58 | 59 | const exampleReport = cli.executeOnFiles( [ join( __dirname, 'pass/**' ) ] ); 60 | const exampleCounts = count( exampleReport.results ); 61 | 62 | // Log full report when --verbose, or whenever errors are unexpectedly reported. 63 | if ( verbose || exampleCounts.errors || exampleCounts.warnings ) { 64 | console.log( formatter( exampleReport.results ) ); 65 | } 66 | 67 | if ( exampleCounts.errors ) { 68 | const { errors } = exampleCounts; 69 | console.log( chalk.bold.red( `${ errors } unexpected error${ errors !== 1 ? 's' : '' }!\n` ) ); 70 | process.exitCode = 1; 71 | } else { 72 | const { files } = exampleCounts; 73 | console.log( chalk.green( `${ files } files pass lint.` ) ); 74 | } 75 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'root': true, 3 | 'env': { 4 | 'browser': true, 5 | 'es6': true, 6 | }, 7 | 'extends': [ 8 | 'eslint:recommended', 9 | 'react-app', 10 | 'plugin:import/errors', 11 | 'plugin:jsdoc/recommended', 12 | 'plugin:react-hooks/recommended', 13 | ], 14 | 'parserOptions': { 15 | 'ecmaVersion': 2018, 16 | 'ecmaFeatures': { 17 | 'jsx': true, 18 | }, 19 | 'sourceType': 'module', 20 | }, 21 | 'rules': { 22 | 'array-bracket-spacing': [ 'error', 'always' ], 23 | 'arrow-parens': [ 'error', 'as-needed' ], 24 | 'arrow-spacing': [ 'error', { 25 | 'before': true, 26 | 'after': true, 27 | } ], 28 | 'block-spacing': [ 'error' ], 29 | 'brace-style': [ 'error', '1tbs' ], 30 | 'comma-dangle': [ 'error', { 31 | 'arrays': 'always-multiline', 32 | 'objects': 'always-multiline', 33 | 'imports': 'always-multiline', 34 | 'exports': 'always-multiline', 35 | 'functions': 'never', 36 | } ], 37 | 'comma-spacing': [ 'error', { 38 | 'before': false, 39 | 'after': true, 40 | } ], 41 | 'eol-last': [ 'error', 'unix' ], 42 | 'eqeqeq': [ 'error' ], 43 | 'func-call-spacing': [ 'error' ], 44 | 'import/no-unresolved': [ 'off' ], 45 | 'import/order': [ 'error', { 46 | 'alphabetize': { 47 | 'order': 'asc', 48 | 'caseInsensitive': true 49 | }, 50 | 'groups': [ 'builtin', 'external', 'parent', 'sibling', 'index' ], 51 | 'newlines-between': 'always', 52 | 'pathGroups': [ 53 | { 54 | 'pattern': '@wordpress/**', 55 | 'group': 'external', 56 | 'position': 'after' 57 | } 58 | ], 59 | 'pathGroupsExcludedImportTypes': [ 'builtin' ] 60 | } ], 61 | 'indent': [ 'error', 'tab', { 62 | 'SwitchCase': 1, 63 | } ], 64 | 'key-spacing': [ 'error', { 65 | 'beforeColon': false, 66 | 'afterColon': true, 67 | } ], 68 | 'keyword-spacing': [ 'error', { 69 | 'after': true, 70 | 'before': true, 71 | } ], 72 | 'linebreak-style': [ 'error', 'unix' ], 73 | 'no-console': [ 'warn' ], 74 | 'no-mixed-spaces-and-tabs': [ 'error', 'smart-tabs' ], 75 | 'no-multiple-empty-lines': [ 'error', { 76 | 'max': 1, 77 | } ], 78 | 'no-trailing-spaces': [ 'error' ], 79 | 'no-var': [ 'warn' ], 80 | 'object-curly-newline': [ 'error', { 81 | 'ObjectExpression': { 82 | 'consistent': true, 83 | 'minProperties': 2, 84 | 'multiline': true, 85 | }, 86 | 'ObjectPattern': { 87 | 'consistent': true, 88 | 'multiline': true, 89 | }, 90 | 'ImportDeclaration': { 91 | 'consistent': true, 92 | 'multiline': true, 93 | }, 94 | 'ExportDeclaration': { 95 | 'consistent': true, 96 | 'minProperties': 2, 97 | 'multiline': true, 98 | }, 99 | } ], 100 | 'object-curly-spacing': [ 'error', 'always' ], 101 | 'object-property-newline': [ 'error' ], 102 | 'quotes': [ 'error', 'single' ], 103 | 'semi': [ 'error', 'always' ], 104 | 'semi-spacing': [ 'error', { 105 | 'before': false, 106 | 'after': true, 107 | } ], 108 | 'space-before-function-paren': [ 'error', { 109 | 'anonymous': 'always', 110 | 'asyncArrow': 'always', 111 | 'named': 'never', 112 | } ], 113 | 'space-in-parens': [ 'warn', 'always', { 114 | 'exceptions': [ 'empty' ], 115 | } ], 116 | 'space-unary-ops': [ 'error', { 117 | 'words': true, 118 | 'nonwords': false, 119 | 'overrides': { 120 | '!': true, 121 | }, 122 | } ], 123 | 'template-curly-spacing': [ 'error', 'always' ], 124 | 'yoda': [ 'error', 'never' ], 125 | 'jsdoc/require-jsdoc': [ 'error', { 126 | 'require': { 127 | 'FunctionDeclaration': true, 128 | 'ClassDeclaration': true, 129 | 'ArrowFunctionExpression': true, 130 | 'FunctionExpression': true, 131 | }, 132 | } ], 133 | 'react/jsx-curly-spacing': [ 'error', { 134 | 'when': 'always', 135 | 'children': true, 136 | } ], 137 | 'react/jsx-wrap-multilines': [ 'error' ], 138 | 'react/jsx-curly-newline': [ 'warn', { 139 | 'multiline': 'consistent', 140 | 'singleline': 'consistent', 141 | } ], 142 | 'react/jsx-boolean-value': [ 'error', 'never' ], 143 | 'react/jsx-sort-props': [ 'warn', { 144 | 'reservedFirst': [ 'key', 'ref' ], 145 | 'callbacksLast': true, 146 | 'ignoreCase': true, 147 | } ], 148 | 'jsx-a11y/anchor-is-valid': [ 'error' ], 149 | }, 150 | }; 151 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@humanmade/eslint-config", 3 | "version": "1.2.1", 4 | "description": "Human Made Coding Standards for JavaScript.", 5 | "author": "Human Made", 6 | "license": "GPL-2.0-or-later", 7 | "keywords": [ 8 | "eslint", 9 | "eslint-config", 10 | "js", 11 | "react", 12 | "lint", 13 | "linter", 14 | "humanmade" 15 | ], 16 | "homepage": "https://github.com/humanmade/coding-standards/tree/master/packages/eslint-config-humanmade#readme", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/humanmade/coding-standards.git", 20 | "directory": "packages/eslint-config-humanmade" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/humanmade/coding-standards/issues" 24 | }, 25 | "engines": { 26 | "npm": ">=7.0.0", 27 | "node": ">=16.0.0" 28 | }, 29 | "files": [ 30 | ".eslintrc", 31 | "index.js" 32 | ], 33 | "devDependencies": { 34 | "babel-eslint": "^10.0.0", 35 | "chalk": "^2.4.1", 36 | "eslint": "^5.10.0", 37 | "eslint-config-react-app": "^3.0.5", 38 | "eslint-plugin-flowtype": "^3.2.0", 39 | "eslint-plugin-import": "^2.14.0", 40 | "eslint-plugin-jsdoc": "^29.1.3", 41 | "eslint-plugin-jsx-a11y": "^6.1.2", 42 | "eslint-plugin-react": "^7.14.3", 43 | "eslint-plugin-react-hooks": "^4.0.2", 44 | "eslint-plugin-sort-destructure-keys": "^1.3.3" 45 | }, 46 | "peerDependencies": { 47 | "babel-eslint": "^10.0.0", 48 | "eslint": "^5.10.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", 49 | "eslint-config-react-app": "^3.0.5", 50 | "eslint-plugin-flowtype": "^3.2.0", 51 | "eslint-plugin-import": "^2.14.0", 52 | "eslint-plugin-jsdoc": "^29.1.3", 53 | "eslint-plugin-jsx-a11y": "^6.1.2", 54 | "eslint-plugin-react": "^7.11.1", 55 | "eslint-plugin-react-hooks": "^4.0.2", 56 | "eslint-plugin-sort-destructure-keys": "^1.3.3" 57 | }, 58 | "publishConfig": { 59 | "access": "public" 60 | }, 61 | "scripts": { 62 | "test": "node fixtures/test-lint-config" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/eslint-config-humanmade/readme.md: -------------------------------------------------------------------------------- 1 | # @humanmade/eslint-config 2 | 3 | Human Made coding standards for JavaScript. 4 | 5 | ## Installation 6 | 7 | This package is an ESLint shareable configuration, and requires: `babel-eslint`, `eslint`, `eslint-config-react-app`, `eslint-plugin-flowtype`, `eslint-plugin-import`, `eslint-plugin-jsx-a11y`, `eslint-plugin-jsdoc`, `eslint-plugin-react`, `eslint-plugin-react-hooks`, `eslint-plugin-sort-destructure-keys`. 8 | 9 | To install this config and the peerDependencies when using **npm 5+**: 10 | 11 | ``` 12 | npx install-peerdeps --dev @humanmade/eslint-config@latest 13 | ``` 14 | 15 | (Thanks to [Airbnb's package](https://www.npmjs.com/package/eslint-config-airbnb) for the command.) 16 | 17 | You can then use it directly on the command line: 18 | 19 | ```shell 20 | ./node_modules/.bin/eslint -c @humanmade/eslint-config MyFile.js 21 | ``` 22 | 23 | Alternatively, you can create your own configuration and extend these rules: 24 | ```yaml 25 | extends: 26 | - @humanmade/eslint-config 27 | ``` 28 | 29 | ### Working with TypeScript 30 | 31 | If you desire to use TypeScript for your project, you will need to add another dependency: 32 | 33 | ```shell 34 | npm install --save-dev @typescript-eslint/parser 35 | ``` 36 | 37 | Once it's installed, update your configuration with the `parser` parameter: 38 | 39 | ```yml 40 | parser: "@typescript-eslint/parser" 41 | extends: 42 | - @humanmade/eslint-config 43 | ``` 44 | 45 | ## Global Installation 46 | 47 | When installing globally, you need to ensure the peer dependencies are also installed globally. 48 | 49 | Run the same command as above, but instead with `--global`: 50 | 51 | ```shell 52 | npx install-peerdeps --global @humanmade/eslint-config@latest 53 | ``` 54 | 55 | This allows you to use `eslint -c humanmade MyFile.js` anywhere on your filesystem. 56 | 57 | ## Integration with Altis build script. 58 | 59 | We require the use of Node v16+ and npm v7+, however the Altis build container ships with Node 12.18 and npm 6.14 so it will not work out of the box. 60 | 61 | As per the Altis documentation, [you can install other versions of Node using nvm](https://docs.altis-dxp.com/cloud/build-scripts/#included-build-tools), so we recommend that you add the following to your build script. 62 | 63 | ``` 64 | nvm install 16 65 | nvm use 16 66 | ``` 67 | -------------------------------------------------------------------------------- /packages/stylelint-config/.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@wordpress/stylelint-config/scss" 4 | ], 5 | "rules": { 6 | "comment-empty-line-before": [ 7 | "always", 8 | { 9 | "except": [ "first-nested"], 10 | "ignore": [ "stylelint-commands" ] 11 | } 12 | ], 13 | "block-closing-brace-newline-before": "always-multi-line", 14 | "declaration-block-semicolon-newline-after": "always-multi-line", 15 | "declaration-block-single-line-max-declarations": 4, 16 | "declaration-no-important": true, 17 | "declaration-property-unit-disallowed-list": { 18 | "font-size": [ "px" ] 19 | }, 20 | "declaration-property-unit-allowed-list": { 21 | "animation": [ "ms" ], 22 | "animation-delay": [ "ms" ], 23 | "animation-duration": [ "ms" ], 24 | "line-height": [], 25 | "transition": [ "ms" ], 26 | "transition-delay": [ "ms" ], 27 | "transition-duration": [ "ms" ] 28 | }, 29 | "function-parentheses-space-inside": "always-single-line", 30 | "function-comma-space-after": "always-single-line", 31 | "function-url-quotes": "always", 32 | "max-line-length": 100, 33 | "max-empty-lines": 1, 34 | "max-nesting-depth": [ 2, { 35 | "ignore": [ "blockless-at-rules" ] 36 | } ], 37 | "media-feature-parentheses-space-inside": "always", 38 | "rule-empty-line-before" : [ 39 | "always", 40 | { 41 | "except": [ "first-nested" ], 42 | "ignore": [ "after-comment" ] 43 | } 44 | ], 45 | "at-rule-empty-line-before" : [ 46 | "always", 47 | { 48 | "except": [ "blockless-after-blockless", "first-nested" ], 49 | "ignore": [ "after-comment" ], 50 | "ignoreAtRules": [ "if", "else if", "else" ] 51 | } 52 | ], 53 | "number-max-precision": 3, 54 | "selector-class-pattern": [ 55 | "^(?(?:[a-z][a-z0-9]*)(?:-[a-z0-9]+)*)(?(?:__[a-z][a-z0-9]*(?:-[a-z0-9]+)*))?(?(?:--[a-z][a-z0-9]*)(?:-[a-z0-9]+)*)?$", 56 | { 57 | "resolveNestedSelectors": true 58 | } 59 | ], 60 | "selector-id-pattern": null 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/stylelint-config/fixtures/fail/bad-bem-syntax.css: -------------------------------------------------------------------------------- 1 | /* Bad BEM syntax */ 2 | .foo__bar__foo, 3 | .foo--bar--foo, 4 | .foo--bar__foo { 5 | display: block; 6 | } 7 | -------------------------------------------------------------------------------- /packages/stylelint-config/fixtures/fail/max-nesting-depth.scss: -------------------------------------------------------------------------------- 1 | // Max nesting depth. 2 | .element { 3 | display: block; 4 | 5 | .child-element { 6 | display: block; 7 | 8 | .child-element { 9 | display: block; 10 | 11 | .child-element { 12 | display: block; 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/stylelint-config/fixtures/fail/no-color-named.css: -------------------------------------------------------------------------------- 1 | .element { 2 | background: red; 3 | } 4 | -------------------------------------------------------------------------------- /packages/stylelint-config/fixtures/pass/style.css: -------------------------------------------------------------------------------- 1 | /* No color-named */ 2 | .element { 3 | background: #f00; 4 | } 5 | 6 | /* Valid BEM syntax */ 7 | .block__element, 8 | .block__element--modifier, 9 | .block--modifier { 10 | display: block; 11 | } 12 | -------------------------------------------------------------------------------- /packages/stylelint-config/fixtures/pass/style.scss: -------------------------------------------------------------------------------- 1 | // Max nesting depth. 2 | .element { 3 | display: block; 4 | 5 | // Nesting depth: 1 6 | .child-element { 7 | display: block; 8 | 9 | // Nesting depth: 2. Maximum allowed. 10 | .child-element { 11 | display: block; 12 | } 13 | 14 | // Allowed to be nested deeper because it's within a media query. 15 | @media screen { 16 | .child-element { 17 | display: block; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/stylelint-config/fixtures/test-lint-config.js: -------------------------------------------------------------------------------- 1 | const stylelint = require( 'stylelint' ); 2 | const chalk = require( 'chalk' ); 3 | const path = require( 'path' ); 4 | 5 | stylelint.lint( { 6 | files: 'fixtures/pass/**/*.{css,scss}' 7 | } ).then( (resultObject) => { 8 | if ( resultObject.errored ) { 9 | console.log( chalk.bold.red( 'Stylelint detected the following errors in the test files that are expected to pass.' ) ); 10 | resultObject.results.forEach( result => { 11 | if ( ! result.errored ) { 12 | return; 13 | } 14 | 15 | console.log( '• ' + path.relative( process.cwd(), result.source ) ); 16 | result.warnings.forEach( result => { 17 | console.log( ` • ${ result.text }. Line: ${ result.line }.` ); 18 | } ); 19 | } ); 20 | process.exitCode = 1; 21 | } else { 22 | console.log( chalk.green( 'No errors detected in files that are expected to pass.' ) ); 23 | } 24 | }); 25 | 26 | stylelint.lint( { 27 | files: 'fixtures/fail/**/*.{css,scss}' 28 | } ).then( (resultObject) => { 29 | if ( ! resultObject.errored ) { 30 | console.log( chalk.bold.red( 'The following files did not produce errors:' ) ); 31 | resultObject.results.forEach( result => { 32 | if ( result.errored ) { 33 | return; 34 | } 35 | 36 | console.log( ' ' + result.source ); 37 | } ); 38 | process.exitCode = 1; 39 | } else { 40 | console.log( chalk.green( 'All files that should fail log errors as expected.' ) ); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /packages/stylelint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@humanmade/stylelint-config", 3 | "version": "1.2.1", 4 | "description": "Human Made Coding Standards for CSS and SCSS.", 5 | "author": "Human Made", 6 | "license": "GPL-2.0-or-later", 7 | "keywords": [ 8 | "stylelint", 9 | "stylelint-config", 10 | "css", 11 | "scss", 12 | "lint", 13 | "linter", 14 | "humanmade" 15 | ], 16 | "homepage": "https://github.com/humanmade/coding-standards/tree/master/packages/stylelint-config#stylelint-config-humanmade", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/humanmade/coding-standards.git", 20 | "directory": "packages/stylelint-config" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/humanmade/coding-standards/issues" 24 | }, 25 | "engines": { 26 | "npm": ">=7.0.0", 27 | "node": ">=16.0.0" 28 | }, 29 | "main": ".stylelintrc.json", 30 | "dependencies": { 31 | "@wordpress/stylelint-config": "^21.0.0" 32 | }, 33 | "devDependencies": { 34 | "chalk": "^4.1.2" 35 | }, 36 | "peerDependencies": { 37 | "stylelint": "^14.2.0" 38 | }, 39 | "publishConfig": { 40 | "access": "public" 41 | }, 42 | "scripts": { 43 | "test": "node fixtures/test-lint-config" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/stylelint-config/readme.md: -------------------------------------------------------------------------------- 1 | # @humanmade/stylelint-config 2 | 3 | Human Made coding standards for CSS and SCSS. 4 | 5 | ## Installation 6 | 7 | This package is a stylelint shareable configuration, and requires the `stylelint` library. 8 | 9 | To install this config and dependencies: 10 | 11 | ```bash 12 | npm install --save-dev stylelint @humanmade/stylelint-config 13 | ``` 14 | 15 | Then, add a `.stylelintrc` file and extend these rules. You can also add your own rules and overrides for further customization. 16 | 17 | ```json 18 | { 19 | "extends": "@humanmade/stylelint-config", 20 | "rules": { 21 | ... 22 | } 23 | } 24 | ``` 25 | 26 | ## Integration with Altis build script. 27 | 28 | We require the use of Node v16+ and npm v7+, however the Altis build container ships with Node 12.18 and npm 6.14 so it will not work out of the box. 29 | 30 | As per the Altis documentation, [you can install other versions of Node using nvm](https://docs.altis-dxp.com/cloud/build-scripts/#included-build-tools), so we recommend that you add the following to your build script. 31 | 32 | ``` 33 | nvm install 16 34 | nvm use 16 35 | ``` 36 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | tests/AllSniffs.php 13 | tests/FixtureTests.php 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # 3 | # Publishes the release to S3 for hmlinter. 4 | 5 | CURRENT_VERSION=$(node -p "require('./packages/eslint-config-humanmade/package.json').version") 6 | 7 | echo "Creating archives of $CURRENT_VERSION for hmlinter" 8 | test -d archives || mkdir archives 9 | 10 | # Prepare phpcs standard 11 | test -d phpcs-standard && rm -r phpcs-standard 12 | mkdir phpcs-standard 13 | echo '{"require": {"humanmade/coding-standards": "'$CURRENT_VERSION'"}}' > phpcs-standard/composer.json 14 | composer install -d phpcs-standard 15 | cp ruleset.xml phpcs-standard/ 16 | tar czvf "archives/phpcs-$CURRENT_VERSION.tar.gz" -C phpcs-standard --exclude '*/tests/*' --exclude '*/fixtures/*' ruleset.xml vendor/ 17 | 18 | # Prepare eslint 19 | yarn install --cwd packages/eslint-config-humanmade 20 | tar czvf "archives/eslint-$CURRENT_VERSION.tar.gz" -C packages/eslint-config-humanmade .eslintrc index.js package.json node_modules/ 21 | 22 | # Prepare stylelint 23 | yarn install --cwd packages/stylelint-config 24 | tar czvf "archives/stylelint-$CURRENT_VERSION.tar.gz" -C packages/stylelint-config .stylelintrc.json package.json node_modules/ 25 | 26 | read -p "Bump 'latest' to $CURRENT_VERSION [Y/n]? " choice 27 | case "$choice" in 28 | y|Y|"") 29 | echo "Bumping 'latest' to $CURRENT_VERSION" 30 | cp "archives/eslint-$CURRENT_VERSION.tar.gz" "archives/eslint-latest.tar.gz" 31 | cp "archives/phpcs-$CURRENT_VERSION.tar.gz" "archives/phpcs-latest.tar.gz" 32 | cp "archives/stylelint-$CURRENT_VERSION.tar.gz" "archives/stylelint-latest.tar.gz" 33 | ;; 34 | n|N ) 35 | echo "Skipping 'latest'" 36 | ;; 37 | * ) 38 | echo "Invalid choice, exiting" 39 | exit 1 40 | ;; 41 | esac 42 | 43 | echo "Publishing archives to S3..." 44 | aws s3 sync --acl=public-read archives/ s3://hm-linter/standards/ 45 | -------------------------------------------------------------------------------- /ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Proxy to the actual Human Made coding standards. 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/AllSniffs.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) 7 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 8 | */ 9 | 10 | namespace HM\CodingStandards\Tests; 11 | 12 | use PHP_CodeSniffer\Util\Standards; 13 | use PHP_CodeSniffer\Autoload; 14 | use PHPUnit\TextUI\TestRunner; 15 | use PHPUnit\Framework\TestSuite; 16 | use RecursiveDirectoryIterator; 17 | use RecursiveIteratorIterator; 18 | 19 | /** 20 | * Class AllSniffs 21 | */ 22 | class AllSniffs { 23 | const TEST_SUFFIX = 'UnitTest.php'; 24 | 25 | /** 26 | * Prepare the test runner. 27 | * 28 | * @return void 29 | */ 30 | public static function main() { 31 | TestRunner::run( self::suite() ); 32 | } 33 | 34 | /** 35 | * Add all sniff unit tests into a test suite. 36 | * 37 | * Sniff unit tests are found by recursing through the 'Tests' directory 38 | * of each installed coding standard. 39 | * 40 | * @return \PHPUnit\Framework\TestSuite 41 | */ 42 | public static function suite() { 43 | $GLOBALS['PHP_CODESNIFFER_SNIFF_CODES'] = array(); 44 | $GLOBALS['PHP_CODESNIFFER_FIXABLE_CODES'] = array(); 45 | $GLOBALS['PHP_CODESNIFFER_SNIFF_CASE_FILES'] = array(); 46 | 47 | $suite = new TestSuite( 'HM Standards' ); 48 | 49 | $standards_dir = dirname( __DIR__ ) . '/HM'; 50 | $all_details = Standards::getInstalledStandardDetails( false, $standards_dir ); 51 | $details = $all_details['HM']; 52 | 53 | Autoload::addSearchPath( $details['path'], $details['namespace'] ); 54 | 55 | $test_dir = $details['path'] . '/Tests/'; 56 | if ( is_dir( $test_dir ) === false ) { 57 | // No tests for this standard. 58 | return $suite; 59 | } 60 | 61 | $di = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $test_dir ) ); 62 | 63 | foreach ( $di as $file ) { 64 | $filename = $file->getFilename(); 65 | 66 | // Skip hidden files. 67 | if ( substr( $filename, 0, 1 ) === '.' ) { 68 | continue; 69 | } 70 | 71 | // Tests must end with "UnitTest.php" 72 | if ( substr( $filename, -1 * strlen( static::TEST_SUFFIX ) ) !== static::TEST_SUFFIX ) { 73 | continue; 74 | } 75 | 76 | $className = Autoload::loadFile( $file->getPathname() ); 77 | $GLOBALS['PHP_CODESNIFFER_STANDARD_DIRS'][ $className ] = $details['path']; 78 | $GLOBALS['PHP_CODESNIFFER_TEST_DIRS'][ $className ] = $test_dir; 79 | $suite->addTestSuite( $className ); 80 | } 81 | 82 | return $suite; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/FixtureTests.php: -------------------------------------------------------------------------------- 1 | $file ) { 53 | if ( ! $file->isFile() || $file->getExtension() === 'json' ) { 54 | continue; 55 | } 56 | 57 | $files[] = [ $path ]; 58 | } 59 | 60 | return $files; 61 | } 62 | 63 | /** 64 | * Get files from the pass fixtures directory. 65 | * 66 | * @return array List of parameters to provide. 67 | */ 68 | public static function failing_files() { 69 | $directory = __DIR__ . '/fixtures/fail'; 70 | 71 | return static::get_files_from_dir( $directory ); 72 | } 73 | 74 | /** 75 | * Get files from the pass fixtures directory. 76 | * 77 | * @return array List of parameters to provide. 78 | */ 79 | public static function passing_files() { 80 | $directory = __DIR__ . '/fixtures/pass'; 81 | 82 | return static::get_files_from_dir( $directory ); 83 | } 84 | 85 | /** 86 | * Setup our ruleset. 87 | */ 88 | public function setUp() { 89 | $this->config = new Config(); 90 | $this->config->cache = false; 91 | $this->config->standards = [ 'HM' ]; 92 | 93 | // Keeping the tabWidth set inline with WPCS. 94 | // See: https://github.com/humanmade/coding-standards/pull/88#issuecomment-464076803 95 | $this->config->tabWidth = 4; 96 | 97 | // We want to setup our tests to only load our standards in for testing. 98 | $this->config->sniffs = [ 99 | 'HM.Classes.OnlyClassInFile', 100 | 'HM.Debug.ESLint', 101 | 'HM.Files.ClassFileName', 102 | 'HM.Files.FunctionFileName', 103 | 'HM.Files.NamespaceDirectoryName', 104 | 'HM.Functions.NamespacedFunctions', 105 | 'HM.Layout.Order', 106 | 'HM.Namespaces.NoLeadingSlashOnUse', 107 | 'HM.Performance.SlowMetaQuery', 108 | 'HM.Performance.SlowOrderBy', 109 | 'HM.PHP.Isset', 110 | 'HM.Security.EscapeOutput', 111 | 'HM.Security.NonceVerification', 112 | 'HM.Security.ValidatedSanitizedInput', 113 | 'HM.Whitespace.MultipleEmptyLines', 114 | ]; 115 | 116 | $this->ruleset = new Ruleset( $this->config ); 117 | 118 | // Set configuration as needed too. 119 | $this->ruleset->setSniffProperty( 'HM\\Sniffs\\Security\\EscapeOutputSniff', 'customAutoEscapedFunctions', [ 120 | 'my_custom_func', 121 | 'another_func', 122 | ] ); 123 | $this->ruleset->setSniffProperty( 'HM\\Sniffs\\Security\\NonceVerificationSniff', 'allowQueryVariables', true ); 124 | } 125 | 126 | /** 127 | * @dataProvider passing_files 128 | */ 129 | public function test_passing_files( $file ) { 130 | $phpcsFile = new LocalFile( $file, $this->ruleset, $this->config ); 131 | $phpcsFile->process(); 132 | 133 | $rel_file = substr( $file, strlen( __DIR__ ) ); 134 | $foundErrors = $phpcsFile->getErrors(); 135 | $this->assertEquals( [], $foundErrors, sprintf( 'File %s should not contain any errors', $rel_file ) ); 136 | $foundWarnings = $phpcsFile->getWarnings(); 137 | $this->assertEquals( [], $foundWarnings, sprintf( 'File %s should not contain any warnings', $rel_file ) ); 138 | } 139 | 140 | /** 141 | * @dataProvider failing_files 142 | */ 143 | public function test_failing_files( $file ) { 144 | $phpcsFile = new LocalFile( $file, $this->ruleset, $this->config ); 145 | $phpcsFile->process(); 146 | 147 | $rel_file = substr( $file, strlen( __DIR__ ) ); 148 | $foundErrors = $phpcsFile->getErrors(); 149 | $foundWarnings = $phpcsFile->getWarnings(); 150 | 151 | $expected_file = $file . '.json'; 152 | $expected = json_decode( file_get_contents( $expected_file ), true ); 153 | 154 | $this->assertEquals( 155 | JSON_ERROR_NONE, 156 | json_last_error(), 157 | sprintf( 158 | 'Expected JSON should be correctly parsed: %s', 159 | json_last_error_msg() 160 | ) 161 | ); 162 | 163 | $found = []; 164 | foreach ( $foundErrors as $line => $columns ) { 165 | foreach ( $columns as $column => $errors ) { 166 | foreach ( $errors as $error ) { 167 | $found[ $line ][] = [ 168 | 'source' => $error['source'], 169 | 'type' => 'error', 170 | ]; 171 | } 172 | } 173 | } 174 | foreach ( $foundWarnings as $line => $columns ) { 175 | foreach ( $columns as $column => $errors ) { 176 | foreach ( $errors as $error ) { 177 | $found[ $line ][] = [ 178 | 'source' => $error['source'], 179 | 'type' => 'warning', 180 | ]; 181 | } 182 | } 183 | } 184 | 185 | $this->assertEquals( $expected, $found, sprintf( 'File %s should only contain specified errors', $rel_file ) ); 186 | // var_dump( $foundErrors ); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 'foo', 6 | 'meta_value' => 'bar', 7 | ] ); 8 | 9 | // Advanced meta query. 10 | new WP_Query( [ 11 | 'meta_query' => [ 12 | 'key' => 'foo', 13 | 'value' => 'bar', 14 | ], 15 | ] ); 16 | 17 | // Custom compares. 18 | new WP_Query( [ 19 | 'meta_query' => [ 20 | 'key' => 'foo', 21 | 'value' => 'bar', 22 | 'compare' => '!=', 23 | ], 24 | ] ); 25 | 26 | new WP_Query( [ 27 | 'meta_query' => [ 28 | 'key' => 'foo', 29 | 'value' => 'bar', 30 | 'compare' => '>', 31 | ], 32 | ] ); 33 | 34 | new WP_Query( [ 35 | 'meta_query' => [ 36 | 'key' => 'foo', 37 | 'value' => 'bar', 38 | 'compare' => 'LIKE', 39 | ], 40 | ] ); 41 | 42 | // Variables. 43 | $compare = 'LIKE'; 44 | new WP_Query( [ 45 | 'meta_query' => [ 46 | 'key' => 'foo', 47 | 'value' => 'bar', 48 | 'compare' => $compare, 49 | ], 50 | ] ); 51 | $meta_query = [ 52 | 'key' => 'foo', 53 | 'value' => 'bar', 54 | ]; 55 | new WP_Query( [ 56 | 'meta_query' => $meta_query, 57 | ] ); 58 | -------------------------------------------------------------------------------- /tests/fixtures/fail/meta-queries.php.json: -------------------------------------------------------------------------------- 1 | { 2 | "6": [ 3 | { 4 | "source": "HM.Performance.SlowMetaQuery.slow_query_meta_value", 5 | "type": "warning" 6 | } 7 | ], 8 | "11": [ 9 | { 10 | "source": "HM.Performance.SlowMetaQuery.nonperformant_comparison", 11 | "type": "warning" 12 | } 13 | ], 14 | "22": [ 15 | { 16 | "source": "HM.Performance.SlowMetaQuery.nonperformant_comparison", 17 | "type": "warning" 18 | } 19 | ], 20 | "30": [ 21 | { 22 | "source": "HM.Performance.SlowMetaQuery.nonperformant_comparison", 23 | "type": "warning" 24 | } 25 | ], 26 | "38": [ 27 | { 28 | "source": "HM.Performance.SlowMetaQuery.nonperformant_comparison", 29 | "type": "warning" 30 | } 31 | ], 32 | "48": [ 33 | { 34 | "source": "HM.Performance.SlowMetaQuery.dynamic_compare", 35 | "type": "warning" 36 | } 37 | ], 38 | "56": [ 39 | { 40 | "source": "HM.Performance.SlowMetaQuery.dynamic_query", 41 | "type": "warning" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /tests/fixtures/fail/not-namespace.php: -------------------------------------------------------------------------------- 1 | 'rand', 5 | ] ); 6 | new WP_Query( [ 7 | 'orderby' => 'meta_value', 8 | ] ); 9 | new WP_Query( [ 10 | 'orderby' => 'meta_value_num', 11 | ] ); 12 | -------------------------------------------------------------------------------- /tests/fixtures/fail/order-by.php.json: -------------------------------------------------------------------------------- 1 | { 2 | "4": [ 3 | { 4 | "source": "HM.Performance.SlowOrderBy.slow_order", 5 | "type": "warning" 6 | } 7 | ], 8 | "7": [ 9 | { 10 | "source": "HM.Performance.SlowOrderBy.slow_order", 11 | "type": "warning" 12 | } 13 | ], 14 | "10": [ 15 | { 16 | "source": "HM.Performance.SlowOrderBy.slow_order", 17 | "type": "warning" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /tests/fixtures/fail/server-input.php: -------------------------------------------------------------------------------- 1 | &$y ) { 14 | if ( ! $y ) { 15 | continue; 16 | } 17 | 18 | echo esc_html( $y ); 19 | } 20 | 21 | return $foo; 22 | } 23 | 24 | /** 25 | * Anonymous functions with `use` should not trigger an order warning. 26 | */ 27 | function anonymous_function() { 28 | $x = 0; 29 | 30 | return function () use ( $x ) { 31 | $x++; 32 | return new WP_Post( $x ); 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /tests/fixtures/pass/isset.php: -------------------------------------------------------------------------------- 1 | 'foo', 6 | ] ); 7 | 8 | // EXISTS/NOT EXISTS are performant. 9 | new WP_Query( [ 10 | 'meta_query' => [ 11 | 'key' => 'foo', 12 | 'compare' => 'EXISTS', 13 | ], 14 | ] ); 15 | new WP_Query( [ 16 | 'meta_query' => [ 17 | 'key' => 'foo', 18 | 'compare' => 'NOT EXISTS', 19 | ], 20 | ] ); 21 | 22 | // Specifying relation is OK. 23 | new WP_Query( [ 24 | 'meta_query' => [ 25 | 'relation' => 'AND', 26 | [ 27 | 'key' => 'foo', 28 | 'compare' => 'EXISTS', 29 | ], 30 | [ 31 | 'key' => 'bar', 32 | 'compare' => 'NOT EXISTS', 33 | ], 34 | ], 35 | ] ); 36 | new WP_Query( [ 37 | 'meta_query' => [ 38 | 'relation' => 'OR', 39 | [ 40 | 'key' => 'foo', 41 | 'compare' => 'NOT EXISTS', 42 | ], 43 | [ 44 | 'key' => 'bar', 45 | 'compare' => 'EXISTS', 46 | ], 47 | ], 48 | ] ); 49 | $relation = 'OR'; 50 | new WP_Query( [ 51 | 'meta_query' => [ 52 | 'relation' => $relation, 53 | [ 54 | 'key' => 'foo', 55 | 'compare' => 'EXISTS', 56 | ], 57 | ], 58 | ] ); 59 | 60 | // Ignores should work. 61 | new WP_Query( [ 62 | 'meta_query' => [ 63 | 'key' => 'foo', 64 | 'value' => 'bar', 65 | // phpcs:ignore HM.Performance.SlowMetaQuery.nonperformant_comparison -- Only a few records, so performant. 66 | 'compare' => '!=', 67 | ], 68 | ] ); 69 | 70 | $meta_query = [ 71 | 'key' => 'foo', 72 | 'compare' => 'EXISTS', 73 | ]; 74 | new WP_Query( [ 75 | // phpcs:ignore HM.Performance.SlowMetaQuery.dynamic_query -- See above, performant. 76 | 'meta_query' => $query, 77 | ] ); 78 | 79 | -------------------------------------------------------------------------------- /tests/fixtures/pass/nonce-verification.php: -------------------------------------------------------------------------------- 1 | 'menu_order', 6 | ] ); 7 | 8 | // Manual ignores are OK too. 9 | new WP_Query( [ 10 | // phpcs:ignore HM.Performance.SlowOrderBy.slow_order -- Only a few values. 11 | 'orderby' => 'meta_value', 12 | ] ); 13 | -------------------------------------------------------------------------------- /tests/fixtures/pass/plugin.php: -------------------------------------------------------------------------------- 1 | &$y ) { 14 | if ( ! $y ) { 15 | continue; 16 | } 17 | 18 | echo esc_html( $y ); 19 | } 20 | 21 | return $foo; 22 | } 23 | 24 | /** 25 | * Anonymous functions with `use` should not trigger an order warning. 26 | */ 27 | function anonymous_function() { 28 | $x = 0; 29 | 30 | return function () use ( $x ) { 31 | $x++; 32 | return new WP_Post( $x ); 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /tests/fixtures/pass/use-order.php: -------------------------------------------------------------------------------- 1 |