Skip to content

Commit

Permalink
TODO: Improve the fixer to save passes
Browse files Browse the repository at this point in the history
need to accumulate everything in each pass in mamory array
and then execute all the moves in a row, to avoid going 1 by 1.
  • Loading branch information
stronk7 committed Feb 24, 2024
1 parent 1828de0 commit cfcf146
Show file tree
Hide file tree
Showing 5 changed files with 456 additions and 0 deletions.
216 changes: 216 additions & 0 deletions moodle/Sniffs/Files/LangFilesOrderingSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<?php

// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.

/**
* This sniff verifies that lang files are sorted alphabetically by string key.
*
* @copyright 2024 onwards Eloy Lafuente (stronk7) {@link https://stronk7.com}
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace MoodleHQ\MoodleCS\moodle\Sniffs\Files;

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

class LangFilesOrderingSniff implements Sniff
{
/**
* @var int The pointer to the previous string key found.
*/
protected int $previousStringPtr = 0;

/**
* @var bool Whether fixing is enabled or not. Some stuff in lang files can lead to disabling it.
*/
protected $fixingEnabled = true;

public function __construct() {
static $times = 0;
$times++;
error_log('-----');
error_log('LangFilesOrderingSniff initialized (' . $times . ') times');
}

public function register(): array {
return [
T_VARIABLE, // We are interested in all the variables in the file.
T_COMMENT, // and, also, some comments to find the different "sections".
];
}

public function process(File $phpcsFile, $stackPtr): void {
// If the file is not a lang file, return.
if (!MoodleUtil::isLangFile($phpcsFile)) {
return;
}

// Only for Moodle 4.4dev (404) and up.
// Make and exception for unit tests, so they are run always.
if (!MoodleUtil::meetsMinimumMoodleVersion($phpcsFile, 404) && !MoodleUtil::isUnitTestRunning()) {
return; // @codeCoverageIgnore
}

// Get the file tokens, for ease of use.
$tokens = $phpcsFile->getTokens();

// If we have arrived to a "Deprecated since" T_COMMENT, we are starting a new section to sort.
if (
$tokens[$stackPtr]['code'] === T_COMMENT &&
strpos($tokens[$stackPtr]['content'], '// Deprecated since Moodle') !== false
) {
// Start over within the new section, reset the previous string pointer.
$this->previousStringPtr = 0;
return;
}

// If fixing is still enabled, and we find some unexpected (different from the "Deprecated since"
// ones) comment, we will disable fixing for the rest of the file. Because it can be
// problematic to properly find the deprecation sections.
if (
$this->previousStringPtr > 0 &&
$tokens[$stackPtr]['code'] === T_COMMENT &&
strpos($tokens[$stackPtr]['content'], '// Deprecated since Moodle') === false
) {
$phpcsFile->addError(
'Unexpected comment found in the lang file, only "// Deprecated since" comments are allowed',
$stackPtr,
'UnexpectedComment'
);
$this->fixingEnabled = false;
}

// No further checks are needed for T_COMMENT tokens.
if ($tokens[$stackPtr]['code'] === T_COMMENT) {
return;
}

// From now on, we are only working with T_VARIABLE tokens.

// If the variable is not "$string", that's completely unexpected, error.
if ($tokens[$stackPtr]['content'] !== '$string') {
$phpcsFile->addError(
'Variable "%s" is not expected in a lang file',
$stackPtr,
'UnexpectedVariable',
[$tokens[$stackPtr]['content']]
);
return;
}

// Get the string key, if any.
if (!$stringKey = $this->getStringKey($phpcsFile, $stackPtr)) {
return;
}

// Compare the current string key with the previous one.
if ($this->previousStringPtr > 0) {
$previousStringKey = $this->getStringKey($phpcsFile, $this->previousStringPtr);

// Detect duplicates and add error on them.
if ($previousStringKey === $stringKey) {
$phpcsFile->addError(
'The string key "%s" is duplicated',
$stackPtr,
'DuplicatedKey',
[$stringKey]
);
}

// Manage unordered keys and fix them by simple swapping.
if (strcmp($previousStringKey, $stringKey) > 0) {
// We are going to get the current string, delete it and insert it before the previous one.
// Find where (with which token) the current string ends (T_SEMICOLON + line feed).
$currentEndToken = $phpcsFile->findNext(T_SEMICOLON, $stackPtr + 1) + 1;

// Verify that the current end token is a line feed, if not, we won't be able to fix (swap).
$swappable = true;
if (
!isset($tokens[$currentEndToken]) ||
$tokens[$currentEndToken]['code'] !== T_WHITESPACE ||
$tokens[$currentEndToken]['content'] !== "\n"
) {
$swappable = false;
}
$fixable = $this->fixingEnabled && $swappable;

$fix = $phpcsFile->addWarning(
'The string key "%s" is not in the correct order, it should be before "%s"',
$stackPtr,
'IncorrectOrder',
[$stringKey, $previousStringKey],
0,
$fixable
);

if ($fix && $fixable) {
error_Log(' - Swap ' . $stringKey . ' and ' . $previousStringKey);
// Note that the swapping technique used here (bubbling) for the fixes is
// not the best one, and it will lead to phpcbf having to
// perform a lot of passes until the whole file is fixed. But
// it's the easiest one to implement using exclusively the CodeSniffer
// API (and assuming that the majority of strings are already sorted
// implies that we won't face worst-case scenarios). A better approach
// would be to use a custom fixer, loading the whole file and applying
// the changes in a single pass.

// Now we are going to swap the strings.
$phpcsFile->fixer->beginChangeset();
// For every token in the current string.
foreach (range($stackPtr, $currentEndToken) as $tokenIndex) {
$tempToken = $tokens[$tokenIndex]; // Store the token.
$phpcsFile->fixer->replaceToken($tokenIndex, ''); // Delete the current string token.
$phpcsFile->fixer->addContent(
$this->previousStringPtr - 1,
$tempToken['content'] // Insert the token before the previous string.
);
}
$phpcsFile->fixer->endChangeset();
}
}
}
$this->previousStringPtr = $stackPtr;
}

/**
* Return the string key corresponding to the string at the pointer.
* Note that the key has got any quote (single or double) trimmed.
*
* return string|null
*/
protected function getStringKey(File $phpcsFile, $stackPtr): ?string {
$tokens = $phpcsFile->getTokens();

// If the structure is not exactly: $string[KEY], add error and return null.
if (
$tokens[$stackPtr + 1]['code'] !== T_OPEN_SQUARE_BRACKET ||
$tokens[$stackPtr + 2]['code'] !== T_CONSTANT_ENCAPSED_STRING ||
$tokens[$stackPtr + 3]['code'] !== T_CLOSE_SQUARE_BRACKET
) {
$phpcsFile->addError(
"Unexpected string syntax, it should be `\$string['key']`",
$stackPtr,
'UnexpectedSyntax'
);
return null;
}

// Now we can safely extract the string key and return it.
return trim($tokens[$stackPtr + 2]['content'], "'\"");
}
}
87 changes: 87 additions & 0 deletions moodle/Tests/Sniffs/Files/LangFilesOrderingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.

namespace MoodleHQ\MoodleCS\moodle\Tests\Files;

use MoodleHQ\MoodleCS\moodle\Tests\MoodleCSBaseTestCase;

/**
* Test the LangFilesOrderingSniff sniff.
*
* @copyright 2024 onwards Eloy Lafuente (stronk7) {@link https://stronk7.com}
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @covers \MoodleHQ\MoodleCS\moodle\Sniffs\Files\LangFilesOrderingSniff
*/
class LangFilesOrderingTest extends MoodleCSBaseTestCase
{
/**
* @dataProvider filesOrderingProvider
*/
public function testLangFilesOrdering(
string $fixture,
array $warnings,
array $errors
) {
$this->setStandard('moodle');
$this->setSniff('moodle.Files.LangFilesOrdering');
$this->setFixture(__DIR__ . '/fixtures/langFilesOrdering/' . $fixture);
$this->setWarnings($warnings);
$this->setErrors($errors);

$this->verifyCsResults();
}

/**
* Data provider for testLangFilesOrdering tests.
*
* @return array
*/
public static function filesOrderingProvider(): array {
return [
'processed' => [
'lang/en/langFilesOrdering.php',
[
27 => '"modvisiblewithstealth_help" is not in the correct order',
30 => 1,
34 => 1,
37 => 1,
38 => 1,
42 => 1,
45 => 1,
46 => 1,
47 => 1,
51 => 1,
61 => 1,
63 => 1,
64 => 1,
],
[
31 => 'Variable "$anothervar" is not expected',
33 => 'Unexpected string syntax, it should be',
40 => 'The string key "yourself" is duplicated',
60 => 'Unexpected comment found in the lang file',
],
],
'not processed' => [
'lang/en@wrong/langFilesOrdering.php',
[],
[],
],
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.

/**
* A fixture lang-like file in correct place to test the LangFilesOrderingSniff.
*
* @package core
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

$string['abouttobeinstalled'] = 'about to be installed';
$string['yourlastlogin'] = 'Your last login was';
$string['modvisiblewithstealth_help'] = '* Show on course page: Available to students (subject to any access restrictions which may be set).
* Hide on course page: Not available to students.
* Make available but don\'t show on course page: Available to students if you provide a link. Activities will still appear in the gradebook and other reports.';
$string['action'] = 'Action';
$anothervar['choco'] = 'kk';
$string['youneedtoenrol'] = 'To perform that action you need to enrol in this course.';
$string['bad' ] = 'Bad';
$string['actionchoice'] = 'What do you want to do with the file \'{$a}\'?';
$string['actions'] = 'Actions';
$string['moodlenet:sharesuccesstext'] = "Almost done! Visit your drafts in MoodleNet to finish sharing your content.";
$string['actionsmenu2'] = "Actions menu2";
$string["actionsmenu"] = 'Actions menu';
$string['yourself'] = 'yourself';
$string['yourself'] = 'yourself';
$string['yourteacher'] = 'your {$a}';
$string['withoutlinefeed'] = 'Without line feed';Any content after semicolon prevents the fixer to move this line.
$string['yourwordforx'] = 'Your word for \'{$a}\'';
$string['zippingbackup'] = 'Zipping backup';
$string['deprecatedeventname'] = '{$a} (no longer in use)';
$string['accept'] = 'Accept';
$string['1111'] = '1111';

// Deprecated since Moodle 4.0.
$string['cupplyinfo'] = 'More details';
$string['bcreateuserandpass'] = 'Choose your username and password';

// Deprecated since Moodle 4.3.
$string['clicktochangeinbrackets'] = '{$a} (Click to change)';

// Deprecated since Moodle 4.4.
$string['asocialheadline'] = 'Social forum - latest topics';
$string['topicshow'] = 'Show this topic to {$a}';

$string['zzzz'] = 'zzzz'; // This comment shouldn't be here. No more auto-fixing after this line.
$string['yyyy'] = 'yyyy';

$string['bbbb'] = 'bbbb';
$string['aaaa'] = 'aaaa';
Loading

0 comments on commit cfcf146

Please sign in to comment.