Skip to content

Commit

Permalink
WIP: Components should be qualified
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewnicols committed Oct 17, 2023
1 parent 017dedd commit 6119cfc
Show file tree
Hide file tree
Showing 6 changed files with 376 additions and 0 deletions.
216 changes: 216 additions & 0 deletions moodle/Sniffs/CodeAnalysis/ComponentNameFullyQualifiedSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace MoodleHQ\MoodleCS\moodle\Sniffs\CodeAnalysis;

use MoodleHQ\MoodleCS\moodle\Util\MoodleUtil;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

// phpcs:disable moodle.NamingConventions

/**
* Checks that calls to methods which consume a component have a fully-qualified component argument.
*
* @package moodle-cs
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class ComponentNameFullyQualifiedSniff implements Sniff {
protected array $componentFunctions = [
'get_string' => 2,
];

protected array $componentClassesConstructors = [
'lang_string' => 2,
];

public function register()
{
return [
T_STRING,
];
}

public function process(File $phpcsFile, $stackPtr)
{
$this->checkFunctions($phpcsFile, $stackPtr);
$this->checkClassConstructors($phpcsFile, $stackPtr);
}

protected function checkFunctions(
File $phpcsFile,
$stackPtr,
): void {
$tokens = $phpcsFile->getTokens();

$ignore = [
T_DOUBLE_COLON => true,
T_OBJECT_OPERATOR => true,
T_NULLSAFE_OBJECT_OPERATOR => true,
T_FUNCTION => true,
T_CONST => true,
T_PUBLIC => true,
T_PRIVATE => true,
T_PROTECTED => true,
T_AS => true,
T_NEW => true,
T_INSTEADOF => true,
T_NS_SEPARATOR => true,
T_IMPLEMENTS => true,
];

$prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);

// If function call is directly preceded by a NS_SEPARATOR it points to the
// global namespace, so we should still catch it.
if ($tokens[$prevToken]['code'] === T_NS_SEPARATOR) {
$prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($prevToken - 1), null, true);
if ($tokens[$prevToken]['code'] === T_STRING) {
// Not in the global namespace.
return;
}
}

if (isset($ignore[$tokens[$prevToken]['code']]) === true) {
// Not a call to a PHP function.
return;
}

$nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
if (isset($ignore[$tokens[$nextToken]['code']]) === true) {
// Not a call to a PHP function.
return;
}

if ($tokens[$stackPtr]['code'] === T_STRING && $tokens[$nextToken]['code'] !== T_OPEN_PARENTHESIS) {
// Not a call to a PHP function.
return;
}

$function = strtolower($tokens[$stackPtr]['content']);

if (array_key_exists($function, $this->componentFunctions)) {
$this->checkFunction($phpcsFile, $stackPtr, $tokens, $function, $this->componentFunctions[$function]);
}
}

protected function checkFunction(
File $phpcsFile,
$stackPtr,
array $tokens,
string $functionName,
int $argumentPosition,
): void {
$openParens = $phpcsFile->findNext(
T_OPEN_PARENTHESIS,
$stackPtr + 1,
);

if ($openParens === false) {
return;
}

$closeParens = $tokens[$openParens]['parenthesis_closer'];

$beforeArgumentPtr = $openParens + 1;
if ($argumentPosition > 1) {
for ($i = 0; $i < $argumentPosition; $i++) {
$beforeArgumentPtr = $phpcsFile->findNext(T_COMMA, ($openParens + 1), $closeParens);
if ($beforeArgumentPtr === false) {
return;
}
}
}

$argumentPtr = $phpcsFile->findNext(
types: [
T_WHITESPACE,
T_COMMA,
],
start: $beforeArgumentPtr + 1,
end: $closeParens,
exclude: true,
);

if ($argumentPtr === false) {
return;
}

if ($tokens[$argumentPtr]['code'] !== T_CONSTANT_ENCAPSED_STRING) {
// TODO See if we can handle this a little.
return;
}

$component = $tokens[$argumentPtr]['content'];
if (str_contains($component, '_')) {
// Component carries an underscore.
return;
}
// xdebug_break();

// Extract the value from the string.
$componentValue = preg_replace([
'/^"(.*)"$/', // Double quotes.
"/^'(.*)'$/", // Single quotes.
], '$1', $component);

$canFix = false;
$componentExists = MoodleUtil::componentExists($phpcsFile, "core_{$componentValue}");
if ($componentExists === true) {
$newValue = "core_{$componentValue}";
$canFix = true;
} else if ($componentExists === false) {
$newValue = "mod_{$componentValue}";
$canFix = true;
}

$fix = false;
if ($canFix) {
$fix = $phpcsFile->addFixableWarning(
"Component name should be '%s' in call to %s: '%s'",
$argumentPtr,
'ComponentNameFullyQualified',
[
$newValue,
$functionName,
$componentValue,
]
);
} else {
$phpcsFile->addWarning(
"Component name not fully qualified in call to %s: '%s'",
$argumentPtr,
'ComponentNameFullyQualified',
[
$functionName,
$componentValue,
]
);
}

if ($fix) {
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->replaceToken($argumentPtr, "'$newValue'");
$phpcsFile->fixer->endChangeset();
}
}

protected function checkClassConstructors(
File $phpcsFile,
$stackPtr,
): void {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace MoodleHQ\MoodleCS\moodle\Tests\Sniffs\CodeAnalysis;

use MoodleHQ\MoodleCS\moodle\Tests\MoodleCSBaseTestCase;

// phpcs:disable moodle.NamingConventions

/**
* Test the NoLeadingSlash sniff.
*
* @package moodle-cs
* @category test
* @copyright 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @covers \MoodleHQ\MoodleCS\moodle\Sniffs\Namespaces\NamespaceStatementSniff
*/
class ComponentNameFullyQualifiedSniffTest extends MoodleCSBaseTestCase
{
public static function provider(): array
{
return [
[
'fixture' => 'get_string',
'warnings' => [
4 => "Component name should be 'mod_forum' in call to get_string: 'forum'",
5 => "Component name should be 'core_grades' in call to get_string: 'grades'",
7 => "Component name should be 'mod_forum' in call to get_string: 'forum'",
8 => "Component name should be 'core_grades' in call to get_string: 'grades'",
],
'errors' => [],
],
];
}
/**
* @dataProvider provider
*/
public function test_component_name(
string $fixture,
array $warnings,
array $errors
): void
{
$this->set_standard('moodle');
$this->set_sniff('moodle.CodeAnalysis.ComponentNameFullyQualified');
$this->set_fixture(sprintf("%s/fixtures/mocked/%s.php", __DIR__, $fixture));

$base = dirname(__DIR__, 3);
$this->set_component_mapping([
'core' => "{$base}/lib",
'core_grades' => "{$base}/grades",
'mod_forum' => "{$base}/mod/forum",
]);
$this->set_warnings($warnings);
$this->set_errors($errors);

$this->verify_cs_results();
}

public static function components_unavailable_provider(): array
{
return [
[
'fixture' => 'get_string',
'warnings' => [
4 => "Component name not fully qualified in call to get_string: 'forum'",
5 => "Component name not fully qualified in call to get_string: 'grades'",
7 => "Component name not fully qualified in call to get_string: 'forum'",
8 => "Component name not fully qualified in call to get_string: 'grades'",
],
'errors' => [],
],
];
}
/**
* @dataProvider components_unavailable_provider
*/
public function test_component_name_unavailable(
string $fixture,
array $warnings,
array $errors
): void
{
$this->set_standard('moodle');
$this->set_sniff('moodle.CodeAnalysis.ComponentNameFullyQualified');
$this->set_fixture(sprintf("%s/fixtures/unmocked/%s.php", __DIR__, $fixture));
$this->set_warnings($warnings);
$this->set_errors($errors);

$this->verify_cs_results();
}
}
10 changes: 10 additions & 0 deletions moodle/Tests/Sniffs/CodeAnalysis/fixtures/mocked/get_string.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

get_string('discussion', 'mod_forum');
get_string('discussion', 'forum');
get_string('grades', 'grades');
get_string('discussion', "mod_forum");
get_string('discussion', "forum");
get_string('grades', "grades");

get_string('example', 'some_component');
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

get_string('discussion', 'mod_forum');
get_string('discussion', 'mod_forum');
get_string('grades', 'core_grades');
get_string('discussion', "mod_forum");
get_string('discussion', 'mod_forum');
get_string('grades', 'core_grades');

get_string('example', 'some_component');
10 changes: 10 additions & 0 deletions moodle/Tests/Sniffs/CodeAnalysis/fixtures/unmocked/get_string.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

get_string('discussion', 'mod_forum');
get_string('discussion', 'forum');
get_string('grades', 'grades');
get_string('discussion', "mod_forum");
get_string('discussion', "forum");
get_string('grades', "grades");

get_string('example', 'some_component');
23 changes: 23 additions & 0 deletions moodle/Util/MoodleUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,29 @@ public static function getMoodleComponent(File $file, $selfPath = true) {
return null;
}

public static function componentExists(
File $file,
string $component,
): ?bool {
if (defined('PHPUNIT_TEST') && PHPUNIT_TEST && !empty(self::$mockedComponentMappings)) {
$components = self::$mockedComponentMappings; // @codeCoverageIgnore
} else {
// Verify that we are able to find a valid moodle root.
if (!$moodleRoot = self::getMoodleRoot($file, true)) {
return null;
}

// Load all components, associative array with keys as component and paths as values.
$components = self::calculateAllComponents($moodleRoot);
// Have been unable to load components, done.
if (empty($components)) {
return null;
}
}

return isset($components[$component]);
}

/**
* Try to guess moodle branch (numeric)
*
Expand Down

0 comments on commit 6119cfc

Please sign in to comment.