Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 187
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
GenerateConfigSchema
0.00% covered (danger)
0.00%
0 / 187
0.00% covered (danger)
0.00%
0 / 13
1806
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
2
 canExecuteWithoutLocalSettings
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDbType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSettings
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 writeOutput
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getOutputPath
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 execute
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
182
 generateSchemaArray
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
20
 generateNames
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
6
 getConstantDeclaration
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 generateSchemaYaml
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 generateVariableStubs
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getVariableDeclaration
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3use MediaWiki\MainConfigSchema;
4use MediaWiki\Maintenance\Maintenance;
5use MediaWiki\Settings\Config\ConfigSchemaAggregator;
6use MediaWiki\Settings\Source\ReflectionSchemaSource;
7use Symfony\Component\Yaml\Yaml;
8use Wikimedia\StaticArrayWriter;
9
10// @codeCoverageIgnoreStart
11require_once __DIR__ . '/Maintenance.php';
12
13// Tell Setup.php to load the config schema from MainConfigSchema rather than
14// any generated file, so we can use this script to re-generate a broken schema file.
15define( 'MW_USE_CONFIG_SCHEMA_CLASS', 1 );
16// @codeCoverageIgnoreEnd
17
18/**
19 * Maintenance script that generates configuration schema files:
20 * - includes/MainConfigNames.php: name constants for config settings
21 * - docs/config-vars.php: dummy variable declarations for config settings
22 * - includes/config-schema.php: an optimized config schema for use by Setup.php
23 * - docs/config-schema.yaml: a JSON Schema of the config settings
24 *
25 * @ingroup Maintenance
26 */
27class GenerateConfigSchema extends Maintenance {
28
29    private const DEFAULT_NAMES_PATH = __DIR__ . '/../includes/MainConfigNames.php';
30    private const DEFAULT_VARS_PATH = __DIR__ . '/../docs/config-vars.php';
31    private const DEFAULT_ARRAY_PATH = __DIR__ . '/../includes/config-schema.php';
32    private const DEFAULT_SCHEMA_PATH = __DIR__ . '/../docs/config-schema.yaml';
33    private const STDOUT = 'php://stdout';
34
35    /** @var array */
36    private $settingsArray;
37
38    public function __construct() {
39        parent::__construct();
40
41        $this->addDescription( 'Generates various config schema files.' );
42
43        $this->addOption(
44            'vars',
45            'Path to output variable stubs to. ' .
46                'Default if none of the options is given: ' .
47                self::DEFAULT_VARS_PATH,
48            false,
49            true
50        );
51
52        $this->addOption(
53            'schema',
54            'Path to output the schema array to. ' .
55                'Default if none of the options is given: ' .
56                self::DEFAULT_ARRAY_PATH,
57            false,
58            true
59        );
60
61        $this->addOption(
62            'names',
63            'Path to output the name constants to. ' .
64                'Default if none of the options is given: ' .
65                self::DEFAULT_NAMES_PATH,
66            false,
67            true
68        );
69
70        $this->addOption(
71            'yaml',
72            'Path to output the schema YAML to. ' .
73                'Default if none of the options is given: ' .
74                self::DEFAULT_SCHEMA_PATH,
75            false,
76            true
77        );
78    }
79
80    public function canExecuteWithoutLocalSettings(): bool {
81        return true;
82    }
83
84    /** @inheritDoc */
85    public function getDbType() {
86        return self::DB_NONE;
87    }
88
89    /**
90     * Loads the config schema from the MainConfigSchema class.
91     *
92     * @return array An associative array with a single key, 'config-schema',
93     *         containing the config schema definition.
94     */
95    private function getSettings(): array {
96        if ( !$this->settingsArray ) {
97            $source = new ReflectionSchemaSource( MainConfigSchema::class, true );
98            $this->settingsArray = $source->load();
99        }
100
101        return $this->settingsArray;
102    }
103
104    /**
105     * @param string $path
106     * @param string $content
107     */
108    private function writeOutput( $path, $content ) {
109        // ensure a single line break at the end of the file
110        $content = trim( $content ) . "\n";
111
112        file_put_contents( $path, $content );
113    }
114
115    /**
116     * @param string $name The name of the option
117     *
118     * @return ?string
119     */
120    private function getOutputPath( string $name ): ?string {
121        $outputPath = $this->getOption( $name );
122        if ( $outputPath === '-' ) {
123            $outputPath = self::STDOUT;
124        }
125        return $outputPath;
126    }
127
128    public function execute() {
129        $settings = $this->getSettings();
130        $allSchemas = $settings['config-schema'];
131        $obsolete = $settings['obsolete-config'] ?? [];
132
133        $schemaPath = $this->getOutputPath( 'schema' );
134        $varsPath = $this->getOutputPath( 'vars' );
135        $yamlPath = $this->getOutputPath( 'yaml' );
136        $namesPath = $this->getOutputPath( 'names' );
137
138        if ( $schemaPath === null && $varsPath === null &&
139            $yamlPath === null && $namesPath === null
140        ) {
141            // If no output path is specified explicitly, use the default path for all.
142            $schemaPath = self::DEFAULT_ARRAY_PATH;
143            $varsPath = self::DEFAULT_VARS_PATH;
144            $yamlPath = self::DEFAULT_SCHEMA_PATH;
145            $namesPath = self::DEFAULT_NAMES_PATH;
146        }
147
148        if ( $schemaPath === self::STDOUT || $varsPath === self::STDOUT ||
149            $yamlPath === self::STDOUT || $namesPath === self::STDOUT
150        ) {
151            // If any of the output is stdout, switch to quiet mode.
152            $this->mQuiet = true;
153        }
154
155        if ( $schemaPath !== null ) {
156            $this->output( "Writing schema array to $schemaPath\n" );
157            $this->writeOutput( $schemaPath, $this->generateSchemaArray( $allSchemas, $obsolete ) );
158        }
159
160        if ( $varsPath !== null ) {
161            $this->output( "Writing variable stubs to $varsPath\n" );
162            $this->writeOutput( $varsPath, $this->generateVariableStubs( $allSchemas ) );
163        }
164
165        if ( $yamlPath !== null ) {
166            $this->output( "Writing schema YAML to $yamlPath\n" );
167            $this->writeOutput( $yamlPath, $this->generateSchemaYaml( $allSchemas ) );
168        }
169
170        if ( $namesPath !== null ) {
171            $this->output( "Writing name constants to $namesPath\n" );
172            $this->writeOutput( $namesPath, $this->generateNames( $allSchemas ) );
173        }
174    }
175
176    public function generateSchemaArray( array $allSchemas, array $obsolete ): string {
177        $aggregator = new ConfigSchemaAggregator();
178        foreach ( $allSchemas as $key => $schema ) {
179            $aggregator->addSchema( $key, $schema );
180        }
181        $schemaInverse = [
182            'default' => $aggregator->getDefaults(),
183            'type' => $aggregator->getTypes(),
184            'mergeStrategy' => $aggregator->getMergeStrategyNames(),
185            'dynamicDefault' => $aggregator->getDynamicDefaults(),
186        ];
187
188        $keyMask = array_flip( [
189            'default',
190            'type',
191            'mergeStrategy',
192            'dynamicDefault',
193            'description',
194            'properties'
195        ] );
196
197        $schemaExtra = [];
198        foreach ( $aggregator->getDefinedKeys() as $key ) {
199            $sch = $aggregator->getSchemaFor( $key );
200            $sch = array_diff_key( $sch, $keyMask );
201
202            if ( $sch ) {
203                $schemaExtra[ $key ] = $sch;
204            }
205        }
206
207        $content = ( new StaticArrayWriter() )->write(
208            [
209                'config-schema-inverse' => $schemaInverse,
210                'config-schema' => $schemaExtra,
211                'obsolete-config' => $obsolete
212            ],
213            "This file is automatically generated using maintenance/generateConfigSchema.php.\n" .
214            "Do not modify this file manually, edit includes/MainConfigSchema.php instead.\n" .
215            "phpcs:disable Generic.Files.LineLength"
216        );
217
218        return $content;
219    }
220
221    public function generateNames( array $allSchemas ): string {
222        $code = "<?php\n";
223        $code .= "/**\n" .
224            " * This file is automatically generated using maintenance/generateConfigSchema.php.\n" .
225            " * Do not modify this file manually, edit includes/MainConfigSchema.php instead.\n" .
226            " * @file\n" .
227            " * @ingroup Config\n" .
228            " */\n\n";
229
230        $code .= "// phpcs:disable Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase\n";
231        $code .= "// phpcs:disable Generic.Files.LineLength.TooLong\n";
232        $code .= "namespace MediaWiki;\n\n";
233
234        $code .= "/**\n" .
235            " * A class containing constants representing the names of configuration variables.\n" .
236            " * These constants can be used in calls to Config::get() or with ServiceOptions,\n" .
237            " * to protect against typos and to make it easier to discover documentation about\n" .
238            " * the respective config setting.\n" .
239            " *\n" .
240            " * @note this class is generated automatically by maintenance/generateConfigSchema.php\n" .
241            " * @since 1.39\n" .
242            " */\n";
243
244        $code .= "class MainConfigNames {\n";
245
246        // Details about each config variable
247        foreach ( $allSchemas as $configKey => $configSchema ) {
248            $code .= "\n";
249            $code .= $this->getConstantDeclaration( $configKey, $configSchema );
250        }
251
252        $code .= "\n}\n";
253
254        return $code;
255    }
256
257    /**
258     * @param string $name
259     * @param array $schema
260     *
261     * @return string
262     */
263    private function getConstantDeclaration( string $name, array $schema ): string {
264        $chunks = [];
265
266        $chunks[] = "Name constant for the $name setting, for use with Config::get()";
267        $chunks[] = "@see MainConfigSchema::$name";
268
269        if ( isset( $schema['since'] ) ) {
270            $chunks[] = "@since {$schema['since']}";
271        }
272
273        if ( isset( $schema['deprecated'] ) ) {
274            $deprecated = str_replace( "\n", "\n\t *    ", wordwrap( $schema['deprecated'] ) );
275            $chunks[] = "@deprecated {$deprecated}";
276        }
277
278        $code = "\t/**\n\t * ";
279        $code .= implode( "\n\t * ", $chunks );
280        $code .= "\n\t */\n";
281
282        $code .= "\tpublic const $name = '$name';\n";
283        return $code;
284    }
285
286    public function generateSchemaYaml( array $allSchemas ): string {
287        foreach ( $allSchemas as &$sch ) {
288            // Cast empty arrays to objects if they are declared to be of type object.
289            // This ensures they get represented in yaml as {} rather than [].
290            if ( isset( $sch['default'] ) && isset( $sch['type'] ) ) {
291                $types = (array)$sch['type'];
292                if ( $sch['default'] === [] && in_array( 'object', $types ) ) {
293                    $sch['default'] = new stdClass();
294                }
295            }
296
297            // Wrap long deprecation messages
298            if ( isset( $sch['deprecated'] ) ) {
299                $sch['deprecated'] = wordwrap( $sch['deprecated'] );
300            }
301        }
302
303        // Dynamic defaults are not relevant to yaml consumers
304        unset( $sch['dynamicDefault'] );
305
306        $yamlFlags = Yaml::DUMP_OBJECT_AS_MAP
307            | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK
308            | Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE;
309
310        $array = [ 'config-schema' => $allSchemas ];
311        $yaml = Yaml::dump( $array, 4, 4, $yamlFlags );
312
313        $header = "# This file is automatically generated using maintenance/generateConfigSchema.php.\n";
314        $header .= "# Do not modify this file manually, edit includes/MainConfigSchema.php instead.\n";
315
316        return $header . $yaml;
317    }
318
319    public function generateVariableStubs( array $allSchemas ): string {
320        $content = "<?php\n";
321        $content .= "/**\n" .
322            " * This file is automatically generated using maintenance/generateConfigSchema.php.\n" .
323            " * Do not modify this file manually, edit includes/MainConfigSchema.php instead.\n" .
324            " */\n";
325
326        $content .= "// phpcs:disable\n";
327        $content .= "throw new LogicException( 'Do not load config-vars.php, " .
328            "it exists as a documentation stub only' );\n";
329
330        foreach ( $allSchemas as $name => $schema ) {
331            $content .= "\n";
332            $content .= $this->getVariableDeclaration( $name, $schema );
333        }
334
335        return $content;
336    }
337
338    /**
339     * @param string $name
340     * @param array $schema
341     *
342     * @return string
343     */
344    private function getVariableDeclaration( string $name, array $schema ): string {
345        $chunks = [];
346        $chunks[] = "Config variable stub for the $name setting, for use by phpdoc and IDEs.";
347        $chunks[] = "@see MediaWiki\\MainConfigSchema::$name";
348
349        if ( isset( $schema['since'] ) ) {
350            $chunks[] = "@since {$schema['since']}";
351        }
352
353        if ( isset( $schema['deprecated'] ) ) {
354            $deprecated = str_replace( "\n", "\n *    ", wordwrap( $schema['deprecated'] ) );
355            $chunks[] = "@deprecated {$deprecated}";
356        }
357
358        $code = "/**\n * ";
359        $code .= implode( "\n * ", $chunks );
360        $code .= "\n */\n";
361
362        $code .= "\$wg{$name} = null;\n";
363        return $code;
364    }
365}
366
367// @codeCoverageIgnoreStart
368$maintClass = GenerateConfigSchema::class;
369require_once RUN_MAINTENANCE_IF_MAIN;
370// @codeCoverageIgnoreEnd