Skip to content

Commit dd9f119

Browse files
Add Missing Strings Validation for Moodle Plugin CI
- Add a comprehensive language string validation tool for Moodle plugins - Validates required strings, detects missing/unused strings from PHP, JavaScript, templates, and database files - Includes automatic subplugin discovery and validation
1 parent d5e4885 commit dd9f119

File tree

64 files changed

+17214
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+17214
-0
lines changed

bin/moodle-plugin-ci

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use MoodlePluginCI\Command\CoverallsUploadCommand;
2121
use MoodlePluginCI\Command\GruntCommand;
2222
use MoodlePluginCI\Command\InstallCommand;
2323
use MoodlePluginCI\Command\MessDetectorCommand;
24+
use MoodlePluginCI\Command\MissingStringsCommand;
2425
use MoodlePluginCI\Command\MustacheCommand;
2526
use MoodlePluginCI\Command\ParallelCommand;
2627
use MoodlePluginCI\Command\PHPDocCommand;
@@ -89,6 +90,7 @@ $application->add(new CoverallsUploadCommand());
8990
$application->add(new GruntCommand());
9091
$application->add(new InstallCommand(ENV_FILE));
9192
$application->add(new MessDetectorCommand());
93+
$application->add(new MissingStringsCommand());
9294
$application->add(new MustacheCommand());
9395
$application->add(new ParallelCommand());
9496
$application->add(new PHPDocCommand());

src/Command/MissingStringsCommand.php

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Moodle Plugin CI package.
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*
9+
* Copyright (c) 2025 Volodymyr Dovhan (https://github.com/volodymyrdovhan)
10+
* License http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
11+
*/
12+
13+
namespace MoodlePluginCI\Command;
14+
15+
use MoodlePluginCI\MissingStrings\StringValidator;
16+
use MoodlePluginCI\MissingStrings\ValidationConfig;
17+
use MoodlePluginCI\MissingStrings\ValidationResult;
18+
use MoodlePluginCI\PluginValidate\Plugin;
19+
use Symfony\Component\Console\Input\InputInterface;
20+
use Symfony\Component\Console\Input\InputOption;
21+
use Symfony\Component\Console\Output\OutputInterface;
22+
23+
/**
24+
* Find missing language strings in a plugin.
25+
*/
26+
class MissingStringsCommand extends AbstractMoodleCommand
27+
{
28+
/**
29+
* Configure the command.
30+
*
31+
* @return void
32+
*/
33+
protected function configure(): void
34+
{
35+
parent::configure();
36+
37+
$this->setName('missingstrings')
38+
->setAliases(['missing-strings'])
39+
->setDescription('Find missing language strings in a plugin')
40+
->addOption(
41+
'lang',
42+
'l',
43+
InputOption::VALUE_REQUIRED,
44+
'Language to validate against',
45+
'en'
46+
)
47+
->addOption(
48+
'strict',
49+
null,
50+
InputOption::VALUE_NONE,
51+
'Strict mode - treat warnings as errors'
52+
)
53+
->addOption(
54+
'unused',
55+
'u',
56+
InputOption::VALUE_NONE,
57+
'Report unused strings as warnings'
58+
)
59+
->addOption(
60+
'exclude-patterns',
61+
null,
62+
InputOption::VALUE_REQUIRED,
63+
'Comma-separated list of string patterns to exclude from validation',
64+
''
65+
)
66+
->addOption(
67+
'debug',
68+
'd',
69+
InputOption::VALUE_NONE,
70+
'Enable debug mode for detailed error information'
71+
);
72+
}
73+
74+
/**
75+
* Execute the command.
76+
*
77+
* @param InputInterface $input the input interface
78+
* @param OutputInterface $output the output interface
79+
*
80+
* @return int the exit code
81+
*/
82+
protected function execute(InputInterface $input, OutputInterface $output): int
83+
{
84+
$this->outputHeading($output, 'Checking for missing language strings in %s');
85+
86+
// Create configuration from command line options
87+
$config = ValidationConfig::fromOptions([
88+
'lang' => $input->getOption('lang'),
89+
'strict' => $input->getOption('strict'),
90+
'unused' => $input->getOption('unused'),
91+
'exclude-patterns' => $input->getOption('exclude-patterns'),
92+
'debug' => $input->getOption('debug'),
93+
]);
94+
95+
// Convert MoodlePlugin to Plugin object
96+
list($type, $name) = $this->moodle->normalizeComponent($this->plugin->getComponent());
97+
$plugin = new Plugin($this->plugin->getComponent(), $type, $name, $this->plugin->directory);
98+
99+
$validator = new StringValidator(
100+
$plugin,
101+
$this->moodle,
102+
$config
103+
);
104+
105+
$result = $validator->validate();
106+
107+
// Show only errors and warnings
108+
foreach ($result->getMessages() as $message) {
109+
$output->writeln($message);
110+
}
111+
112+
// Show summary statistics
113+
$this->outputSummary($output, $result);
114+
115+
return $result->isValid() ? 0 : 1;
116+
}
117+
118+
/**
119+
* Output summary statistics.
120+
*
121+
* @param OutputInterface $output the output interface
122+
* @param ValidationResult $result the validation result
123+
*/
124+
private function outputSummary(OutputInterface $output, ValidationResult $result): void
125+
{
126+
$output->writeln('');
127+
$output->writeln('<comment>Summary:</comment>');
128+
129+
$summary = $result->getSummary();
130+
131+
if ($summary['errors'] > 0) {
132+
$output->writeln(sprintf('- <fg=red>Errors: %d</>', $summary['errors']));
133+
}
134+
135+
if ($summary['warnings'] > 0) {
136+
$output->writeln(sprintf('- <comment>Warnings: %d</comment>', $summary['warnings']));
137+
}
138+
139+
if ($summary['total_issues'] === 0) {
140+
$output->writeln('- <info>No issues found</info>');
141+
} else {
142+
$output->writeln(sprintf('- <comment>Total issues: %d</comment>', $summary['total_issues']));
143+
}
144+
145+
$output->writeln('');
146+
147+
if ($summary['is_valid']) {
148+
$output->writeln('<info>✓ All language strings are valid</info>');
149+
} else {
150+
$output->writeln('<error>✗ Language string validation failed</error>');
151+
}
152+
}
153+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Moodle Plugin CI package.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* Copyright (c) 2025 Volodymyr Dovhan (https://github.com/volodymyrdovhan)
12+
* License http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
13+
*/
14+
15+
namespace MoodlePluginCI\MissingStrings\Cache;
16+
17+
/**
18+
* Simple file content cache to avoid repeated file reads during validation.
19+
*
20+
* This cache is designed to be lightweight and session-scoped, improving
21+
* performance when multiple checkers need to read the same files.
22+
*/
23+
class FileContentCache
24+
{
25+
/**
26+
* Cache storage for file contents.
27+
*/
28+
private static array $contentCache = [];
29+
30+
/**
31+
* Cache storage for file modification times.
32+
*/
33+
private static array $mtimeCache = [];
34+
35+
/**
36+
* Maximum number of files to cache (memory limit protection).
37+
*/
38+
private const MAX_CACHED_FILES = 100;
39+
40+
/**
41+
* Get file content with caching.
42+
*
43+
* @param string $filePath absolute path to the file
44+
*
45+
* @return string|false file content or false on failure
46+
*/
47+
/**
48+
* @return string|false
49+
*/
50+
public static function getContent(string $filePath)
51+
{
52+
// Normalize path for consistent caching
53+
$normalizedPath = realpath($filePath);
54+
if (false === $normalizedPath) {
55+
return false;
56+
}
57+
58+
// Check if file exists and is readable
59+
if (!is_file($normalizedPath) || !is_readable($normalizedPath)) {
60+
return false;
61+
}
62+
63+
$currentMtime = filemtime($normalizedPath);
64+
if (false === $currentMtime) {
65+
return false;
66+
}
67+
68+
// Check if we have a cached version that's still valid
69+
if (
70+
isset(self::$contentCache[$normalizedPath], self::$mtimeCache[$normalizedPath])
71+
&& self::$mtimeCache[$normalizedPath] === $currentMtime
72+
) {
73+
return self::$contentCache[$normalizedPath];
74+
}
75+
76+
// Read file content
77+
$content = file_get_contents($normalizedPath);
78+
if (false === $content) {
79+
return false;
80+
}
81+
82+
// Store in cache (with size limit)
83+
if (count(self::$contentCache) >= self::MAX_CACHED_FILES) {
84+
// Remove oldest cache entry (simple FIFO)
85+
$oldestKey = array_key_first(self::$contentCache);
86+
if (null !== $oldestKey) {
87+
unset(self::$contentCache[$oldestKey], self::$mtimeCache[$oldestKey]);
88+
}
89+
}
90+
91+
self::$contentCache[$normalizedPath] = $content;
92+
self::$mtimeCache[$normalizedPath] = $currentMtime;
93+
94+
return $content;
95+
}
96+
97+
/**
98+
* Check if a file exists and is readable (with caching).
99+
*
100+
* @param string $filePath absolute path to the file
101+
*
102+
* @return bool true if file exists and is readable
103+
*/
104+
public static function fileExists(string $filePath): bool
105+
{
106+
$normalizedPath = realpath($filePath);
107+
if (false === $normalizedPath) {
108+
return file_exists($filePath) && is_readable($filePath);
109+
}
110+
111+
// If we have the file in cache, it exists and is readable
112+
if (isset(self::$contentCache[$normalizedPath])) {
113+
return true;
114+
}
115+
116+
return is_file($normalizedPath) && is_readable($normalizedPath);
117+
}
118+
119+
/**
120+
* Get file lines as array with caching.
121+
*
122+
* @param string $filePath absolute path to the file
123+
* @param int $flags flags for file() function
124+
*
125+
* @return array|false array of lines or false on failure
126+
*/
127+
/**
128+
* @return array|false
129+
*/
130+
public static function getLines(string $filePath, int $flags = FILE_IGNORE_NEW_LINES)
131+
{
132+
$content = self::getContent($filePath);
133+
if (false === $content) {
134+
return false;
135+
}
136+
137+
if ($flags & FILE_IGNORE_NEW_LINES) {
138+
return explode("\n", rtrim($content, "\n"));
139+
}
140+
141+
return explode("\n", $content);
142+
}
143+
144+
/**
145+
* Clear the entire cache.
146+
*
147+
* Useful for testing or when memory usage needs to be reduced.
148+
*/
149+
public static function clearCache(): void
150+
{
151+
self::$contentCache = [];
152+
self::$mtimeCache = [];
153+
}
154+
155+
/**
156+
* Get cache statistics for debugging.
157+
*
158+
* @return array cache statistics
159+
*/
160+
public static function getStats(): array
161+
{
162+
return [
163+
'cached_files' => count(self::$contentCache),
164+
'max_files' => self::MAX_CACHED_FILES,
165+
'memory_usage' => array_sum(array_map('strlen', self::$contentCache)),
166+
];
167+
}
168+
}

0 commit comments

Comments
 (0)