Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
LicenseCommentSniff
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 4
462
0.00% covered (danger)
0.00%
0 / 1
 register
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 process
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 processDocTag
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
272
 getLicenseValidator
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Sniffs\Commenting;
22
23use Composer\Spdx\SpdxLicenses;
24use PHP_CodeSniffer\Files\File;
25use PHP_CodeSniffer\Sniffs\Sniff;
26
27class LicenseCommentSniff implements Sniff {
28
29    /** @var SpdxLicenses */
30    private static $licenses = null;
31
32    /**
33     * Common auto-fixable replacements
34     */
35    private const REPLACEMENTS = [
36        'GNU General Public Licen[sc]e 2(\.0)? or later' => 'GPL-2.0-or-later',
37        'GNU GPL v2\+' => 'GPL-2.0-or-later',
38    ];
39
40    /**
41     * Returns an array of tokens this test wants to listen for.
42     *
43     * @return array
44     */
45    public function register(): array {
46        return [ T_DOC_COMMENT_OPEN_TAG ];
47    }
48
49    /**
50     * Processes this test, when one of its tokens is encountered.
51     *
52     * @param File $phpcsFile The file being scanned.
53     * @param int $stackPtr The position of the current token in the stack passed in $tokens.
54     * @return void
55     */
56    public function process( File $phpcsFile, $stackPtr ) {
57        $tokens = $phpcsFile->getTokens();
58        $end = $tokens[$stackPtr]['comment_closer'];
59        foreach ( $tokens[$stackPtr]['comment_tags'] as $tag ) {
60            $this->processDocTag( $phpcsFile, $tokens, $tag, $end );
61        }
62    }
63
64    /**
65     * @param File $phpcsFile
66     * @param array[] $tokens
67     * @param int $tag Token position of the tag
68     * @param int $end Token position of the end of the doc comment
69     */
70    private function processDocTag( File $phpcsFile, array $tokens, int $tag, int $end ): void {
71        $tagText = $tokens[$tag]['content'];
72
73        if ( $tagText === '@licence' ) {
74            $fix = $phpcsFile->addFixableWarning(
75                'Incorrect wording of @license', $tag, 'LicenceTag'
76            );
77            if ( $fix ) {
78                $phpcsFile->fixer->replaceToken( $tag, '@license' );
79            }
80        } elseif ( $tagText !== '@license' ) {
81            return;
82        }
83
84        if ( $tokens[$tag]['level'] !== 0 ) {
85            $phpcsFile->addWarning(
86                '@license should only be used on the top level',
87                $tag, 'LicenseTagNonFileComment'
88            );
89        }
90
91        // It is okay to have more than one @license
92
93        // Validate text behind @license
94        $next = $phpcsFile->findNext( [ T_DOC_COMMENT_WHITESPACE ], $tag + 1, $end, true );
95        if ( $tokens[$next]['code'] !== T_DOC_COMMENT_STRING ) {
96            $phpcsFile->addWarning(
97                '@license not followed by a license',
98                $tag, 'LicenseTagEmpty'
99            );
100            return;
101        }
102        $license = rtrim( $tokens[$next]['content'] );
103
104        // @license can contain a url, use the text behind it
105        $m = [];
106        if ( preg_match( '/^https?:\/\/[^\s]+\s+(.*)/', $license, $m ) ) {
107            $license = $m[1];
108        }
109
110        $licenseValidator = self::getLicenseValidator();
111        if ( !$licenseValidator->validate( $license ) ) {
112            $fixable = null;
113            foreach ( self::REPLACEMENTS as $regex => $identifier ) {
114                // Make sure the entire license matches the regex, and
115                // then check that the new replacement is valid too
116                if ( preg_match( "/^$regex$/", $license ) === 1
117                    && $licenseValidator->validate( $identifier )
118                ) {
119                    $fixable = $identifier;
120                    break;
121                }
122            }
123            if ( $fixable !== null ) {
124                $fix = $phpcsFile->addFixableWarning(
125                    'Invalid SPDX license identifier "%s", see <https://spdx.org/licenses/>',
126                    $tag, 'InvalidLicenseTag', [ $license ]
127                );
128                if ( $fix ) {
129                    $phpcsFile->fixer->replaceToken( $next, $fixable );
130                }
131            } else {
132                $phpcsFile->addWarning(
133                    'Invalid SPDX license identifier "%s", see <https://spdx.org/licenses/>',
134                    $tag, 'InvalidLicenseTag', [ $license ]
135                );
136            }
137        } else {
138            // Split list to check each license for deprecation
139            $singleLicenses = preg_split( '/\s+(?:AND|OR)\s+/i', $license );
140            foreach ( $singleLicenses as $singleLicense ) {
141                // Check if the split license is known to the validator - T195429
142                if ( !is_array( $licenseValidator->getLicenseByIdentifier( $singleLicense ) ) ) {
143                    // @codeCoverageIgnoreStart
144                    $phpcsFile->addWarning(
145                        'An error occurred during processing SPDX license identifier "%s"',
146                        $tag, 'ErrorLicenseTag', [ $license ]
147                    );
148                    break;
149                    // @codeCoverageIgnoreEnd
150                }
151                if ( $licenseValidator->isDeprecatedByIdentifier( $singleLicense ) ) {
152                    $phpcsFile->addWarning(
153                        'Deprecated SPDX license identifier "%s", see <https://spdx.org/licenses/>',
154                        $tag, 'DeprecatedLicenseTag', [ $singleLicense ]
155                    );
156                }
157            }
158        }
159    }
160
161    /**
162     * @return SpdxLicenses
163     */
164    private static function getLicenseValidator(): SpdxLicenses {
165        if ( self::$licenses === null ) {
166            self::$licenses = new SpdxLicenses();
167        }
168        return self::$licenses;
169    }
170
171}