Skip to content

Commit

Permalink
Merge pull request #2 from HydraWiki/develop
Browse files Browse the repository at this point in the history
Adding License Comment Sniff and unit test
  • Loading branch information
valeryan authored Sep 24, 2018
2 parents 75689b8 + c5691cf commit 128dfbf
Show file tree
Hide file tree
Showing 9 changed files with 641 additions and 11 deletions.
294 changes: 294 additions & 0 deletions HydraWiki/Sniffs/Commenting/LicenseCommentSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
<?php
/**
* Curse Inc.
* LicenseCommentSniff
*
* @author Samuel Hilson
* @license MIT
* @package HydraWiki
* @link http://www.curse.com/
*
**/
namespace HydraWiki\Sniffs\Commenting;

use Composer\Spdx\SpdxLicenses;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

class LicenseCommentSniff implements Sniff {

private $file;
private $tokens;
private $end;

/** @var SpdxLicenses */
private $spdx = null;

/**
* Common auto-fixable replacements
*
* @var array regex -> replacement
*/
private $replacements = [
'GNU General Public Licen[sc]e 2(\.0)? or later' => 'GPL-2.0-or-later',
'GNU GPL v2\+' => 'GPL-2.0-or-later',
];

/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register() {
return [ T_DOC_COMMENT_OPEN_TAG ];
}

/**
* Initialize
*
* @param File $phpcsFile
* @param int $stackPtr
* @param SpdxLicenses|null $spdx
* @return void
*/
public function initialize(File $phpcsFile, $stackPtr, SpdxLicenses $spdx = null) {
$this->file = $phpcsFile;
$this->tokens = $this->file->getTokens();
$this->end = $this->tokens[$stackPtr]['comment_closer'];
if ($spdx !== null) {
$this->spdx = $spdx;
}
}

/**
* Processes this test, when one of its tokens is encountered.
*
* @param File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the stack passed in $tokens.
* @return void
*/
public function process(File $phpcsFile, $stackPtr) {
$this->initialize($phpcsFile, $stackPtr);

foreach ($this->tokens[$stackPtr]['comment_tags'] as $tag) {
$this->processDocTag($tag);
}
}

/**
* handle a single doc line
*
* @param string $tag
* @return mixed
*/
private function processDocTag($tag) {
if ($this->isCorrectTag($tag)) {
return;
}

// Only allow license on file comments
$this->isValidTagLocation($tag);

// Validate text behind license
$next = $this->file->findNext([T_DOC_COMMENT_WHITESPACE], $tag + 1, $this->end, true);

if ($this->hasTextAfterTag($tag, $next)) {
return;
}

// Get license text
$license = $this->getLicenseFromTag($next);

// Allow for proprietary licensing to be used
if ($this->isValidProprietaryLicense($license)) {
return;
}

// Flag deprecated licenses
if ($this->isLicenseDeprecated($tag, $license)) {
return;
}

// Validate licenses
$this->handleInvalidLicense($tag, $next, $license);
}

/**
* get or initialize SpdxLicenses library
*
* @return SpdxLicenses
*/
private function getLicenseValidator() {
if ($this->spdx === null) {
$this->spdx = new SpdxLicenses();
}
return $this->spdx;
}

/**
* normalize license text
*
* @param string $next
* @return string
*/
private function getLicenseFromTag($next) {
$license = $this->tokens[$next]['content'];

// license can contain a url, use the text behind it
if (preg_match('/^https?:\/\/[^\s]+\s+(.*)/', $license, $match)) {
$license = $match[1];
}
return $license;
}

/**
* check if we are on the correct license tag
*
* @param string $tag
* @return boolean
*/
private function isCorrectTag($tag) {
$tagText = $this->tokens[$tag]['content'];
switch ($tagText) {
case '@licence':
$fix = $this->file->addFixableWarning(
'Incorrect spelling of @license',
$tag,
'LicenceTag'
);
if ($fix) {
$this->file->fixer->replaceToken($tag, '@license');
}
break;
case '@license':
break;
default:
return true;
}

return false;
}

/**
* check the tag location is in file level doc comment
*
* @param string $tag
* @return boolean
*/
private function isValidTagLocation($tag) {
if ($this->tokens[$tag]['level'] !== 0) {
$this->file->addWarning(
'@license should only be used on file comments',
$tag,
'LicenseTagNonFileComment'
);
}
}

/**
* check that tag has some text after @license
*
* @param string $tag
* @param string $next
* @return boolean
*/
private function hasTextAfterTag($tag, $next) {
if ($this->tokens[$next]['code'] !== T_DOC_COMMENT_STRING) {
$this->file->addWarning(
'@license not followed by a license',
$tag,
'LicenseTagEmpty'
);
return true;
}
}

/**
* check for a private license
*
* @param string $license
* @return boolean
*/
private function isValidProprietaryLicense($license) {
if (strtolower($license) == 'proprietary') {
return true;
}
}

/**
* check if the license is marked as deprecated
*
* @param string $tag
* @param string $license
* @return boolean
*/
private function isLicenseDeprecated($tag, $license) {
// Initialize the spdx license validator
$spdx = $this->getLicenseValidator();

$valid = $spdx->validate($license);
// Check for Deprecated licensing
if ($valid && $spdx->isDeprecatedByIdentifier($license)) {
$this->file->addWarning(
'Deprecated SPDX license identifier "%s", see <https://spdx.org/licenses/>',
$tag,
'DeprecatedLicenseTag',
[$license]
);
return true;
}
}

/**
* checks that the license is a valid license from spdx
*
* @param string $tag
* @param string $next
* @param string $license
* @return boolean
*/
private function handleInvalidLicense($tag, $next, $license) {
// Initialize the spdx license validator
$spdx = $this->getLicenseValidator();

$valid = $spdx->validate($license);

if ($valid) {
return;
}

$fixable = null;
foreach ($this->replacements as $regex => $identifier) {
// Make sure the entire license matches the regex, and
// then a sanity check that the new replacement is valid too
if (preg_match("/^$regex$/", $license) === 1
&& $spdx->validate($identifier)
) {
$fixable = $identifier;
break;
}
}

// handle fixable license problem
if ($fixable !== null) {
$fix = $this->file->addFixableWarning(
'Invalid SPDX license identifier "%s", see <https://spdx.org/licenses/>',
$tag,
'InvalidLicenseTag',
[$license]
);
if ($fix) {
$this->file->fixer->replaceToken($next, $fixable);
}
return;
}

// report un-fixable problems
$this->file->addWarning(
'Invalid SPDX license identifier "%s", see <https://spdx.org/licenses/>',
$tag,
'InvalidLicenseTag',
[$license]
);
}
}
15 changes: 10 additions & 5 deletions HydraWiki/Sniffs/WhiteSpace/NoSpaceAfterNotSniff.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
<?php

/**
* Curse Inc.
* NoSpaceAfterNotSniff
*
* @author Samuel Hilson
* @license MIT
* @package HydraWiki
* @link http://www.curse.com/
*
**/
namespace HydraWiki\Sniffs\WhiteSpace;

use PHP_CodeSniffer\Sniffs\Sniff;
Expand Down Expand Up @@ -36,10 +45,6 @@ public function register() {
* @return void
*/
public function process(File $phpcsFile, $stackPtr) {
if (!$phpcsFile) {

}

$tokens = $phpcsFile->getTokens();
$spacing = 0;
if ($tokens[($stackPtr + 1)]['code'] === T_WHITESPACE) {
Expand Down
1 change: 1 addition & 0 deletions HydraWiki/ruleset.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<!-- Apply MediaWiki ruleset without SpaceyParenthesis -->
<rule ref="./vendor/mediawiki/mediawiki-codesniffer/MediaWiki">
<exclude name="MediaWiki.WhiteSpace.SpaceyParenthesis" />
<exclude name="MediaWiki.Commenting.LicenseComment" />
</rule>
<!-- MediaWiki Adjustments -->
<rule ref="MediaWiki.NamingConventions.PrefixedGlobalFunctions">
Expand Down
17 changes: 11 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@
],
"homepage": "https://www.gamepedia.com",
"license": "MIT",
"require": {
"php": ">= 7.1",
"composer/semver": "1.4.2",
"mediawiki/mediawiki-codesniffer": "21.0.0"
},
"autoload": {
"psr-4": {
"HydraWiki\\": "HydraWiki"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"require": {
"php": ">= 7.1",
"composer/semver": "1.4.2",
"mediawiki/mediawiki-codesniffer": "21.0.0"
},
"require-dev": {
"jakub-onderka/php-parallel-lint": "1.0.0",
"jakub-onderka/php-console-highlighter": "0.3.2",
Expand All @@ -27,7 +32,7 @@
"scripts": {
"test": [
"parallel-lint . --exclude vendor",
"phpunit $PHPUNIT_ARGS",
"phpunit",
"phpcs -p -s",
"minus-x check ."
],
Expand Down
7 changes: 7 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<phpunit bootstrap="./tests/bootstrap.php">
<testsuites>
<testsuite name="Unit">
<directory>./tests/Unit</directory>
</testsuite>
</testsuites>
</phpunit>
18 changes: 18 additions & 0 deletions tests/BaseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Test;

use PHP_CodeSniffer\Fixer;
use PHP_CodeSniffer\Files\File;
use PHPUnit\Framework\TestCase;

class BaseTest extends TestCase {

protected $fileMock;

protected function setUp() {
$this->fileMock = $this->createMock(File::class);
$fixer = $this->createMock(Fixer::class);
$this->fileMock->fixer = $fixer;
}
}
Loading

0 comments on commit 128dfbf

Please sign in to comment.