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    public function getDbType() {
85        return self::DB_NONE;
86    }
87
88    /**
89     * Loads the config schema from the MainConfigSchema class.
90     *
91     * @return array An associative array with a single key, 'config-schema',
92     *         containing the config schema definition.
93     */
94    private function getSettings(): array {
95        if ( !$this->settingsArray ) {
96            $source = new ReflectionSchemaSource( MainConfigSchema::class, true );
97            $this->settingsArray = $source->load();
98        }
99
100        return $this->settingsArray;
101    }
102
103    /**
104     * @param string $path
105     * @param string $content
106     */
107    private function writeOutput( $path, $content ) {
108        // ensure a single line break at the end of the file
109        $content = trim( $content ) . "\n";
110
111        file_put_contents( $path, $content );
112    }
113
114    /**
115     * @param string $name The name of the option
116     *
117     * @return ?string
118     */
119    private function getOutputPath( string $name ): ?string {
120        $outputPath = $this->getOption( $name );
121        if ( $outputPath === '-' ) {
122            $outputPath = self::STDOUT;
123        }
124        return $outputPath;
125    }
126
127    public function execute() {
128        $settings = $this->getSettings();
129        $allSchemas = $settings['config-schema'];
130        $obsolete = $settings['obsolete-config'] ?? [];
131
132        $schemaPath = $this->getOutputPath( 'schema' );
133        $varsPath = $this->getOutputPath( 'vars' );
134        $yamlPath = $this->getOutputPath( 'yaml' );
135        $namesPath = $this->getOutputPath( 'names' );
136
137        if ( $schemaPath === null && $varsPath === null &&
138            $yamlPath === null && $namesPath === null
139        ) {
140            // If no output path is specified explicitly, use the default path for all.
141            $schemaPath = self::DEFAULT_ARRAY_PATH;
142            $varsPath = self::DEFAULT_VARS_PATH;
143            $yamlPath = self::DEFAULT_SCHEMA_PATH;
144            $namesPath = self::DEFAULT_NAMES_PATH;
145        }
146
147        if ( $schemaPath === self::STDOUT || $varsPath === self::STDOUT ||
148            $yamlPath === self::STDOUT || $namesPath === self::STDOUT
149        ) {
150            // If any of the output is stdout, switch to quiet mode.
151            $this->mQuiet = true;
152        }
153
154        if ( $schemaPath !== null ) {
155            $this->output( "Writing schema array to $schemaPath\n" );
156            $this->writeOutput( $schemaPath, $this->generateSchemaArray( $allSchemas, $obsolete ) );
157        }
158
159        if ( $varsPath !== null ) {
160            $this->output( "Writing variable stubs to $varsPath\n" );
161            $this->writeOutput( $varsPath, $this->generateVariableStubs( $allSchemas ) );
162        }
163
164        if ( $yamlPath !== null ) {
165            $this->output( "Writing schema YAML to $yamlPath\n" );
166            $this->writeOutput( $yamlPath, $this->generateSchemaYaml( $allSchemas ) );
167        }
168
169        if ( $namesPath !== null ) {
170            $this->output( "Writing name constants to $namesPath\n" );
171            $this->writeOutput( $namesPath, $this->generateNames( $allSchemas ) );
172        }
173    }
174
175    public function generateSchemaArray( array $allSchemas, array $obsolete ) {
176        $aggregator = new ConfigSchemaAggregator();
177        foreach ( $allSchemas as $key => $schema ) {
178            $aggregator->addSchema( $key, $schema );
179        }
180        $schemaInverse = [
181            'default' => $aggregator->getDefaults(),
182            'type' => $aggregator->getTypes(),
183            'mergeStrategy' => $aggregator->getMergeStrategyNames(),
184            'dynamicDefault' => $aggregator->getDynamicDefaults(),
185        ];
186
187        $keyMask = array_flip( [
188            'default',
189            'type',
190            'mergeStrategy',
191            'dynamicDefault',
192            'description',
193            'properties'
194        ] );
195
196        $schemaExtra = [];
197        foreach ( $aggregator->getDefinedKeys() as $key ) {
198            $sch = $aggregator->getSchemaFor( $key );
199            $sch = array_diff_key( $sch, $keyMask );
200
201            if ( $sch ) {
202                $schemaExtra[ $key ] = $sch;
203            }
204        }
205
206        $content = ( new StaticArrayWriter() )->write(
207            [
208                'config-schema-inverse' => $schemaInverse,
209                'config-schema' => $schemaExtra,
210                'obsolete-config' => $obsolete
211            ],
212            "This file is automatically generated using maintenance/generateConfigSchema.php.\n" .
213            "Do not modify this file manually, edit includes/MainConfigSchema.php instead.\n" .
214            "phpcs:disable Generic.Files.LineLength"
215        );
216
217        return $content;
218    }
219
220    public function generateNames( array $allSchemas ) {
221        $code = "<?php\n";
222        $code .= "/**\n" .
223            " * This file is automatically generated using maintenance/generateConfigSchema.php.\n" .
224            " * Do not modify this file manually, edit includes/MainConfigSchema.php instead.\n" .
225            " * @file\n" .
226            " * @ingroup Config\n" .
227            " */\n\n";
228
229        $code .= "// phpcs:disable Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase\n";
230        $code .= "// phpcs:disable Generic.Files.LineLength.TooLong\n";
231        $code .= "namespace MediaWiki;\n\n";
232
233        $code .= "/**\n" .
234            " * A class containing constants representing the names of configuration variables.\n" .
235            " * These constants can be used in calls to Config::get() or with ServiceOptions,\n" .
236            " * to protect against typos and to make it easier to discover documentation about\n" .
237            " * the respective config setting.\n" .
238            " *\n" .
239            " * @note this class is generated automatically by maintenance/generateConfigSchema.php\n" .
240            " * @since 1.39\n" .
241            " */\n";
242
243        $code .= "class MainConfigNames {\n";
244
245        // Details about each config variable
246        foreach ( $allSchemas as $configKey => $configSchema ) {
247            $code .= "\n";
248            $code .= $this->getConstantDeclaration( $configKey, $configSchema );
249        }
250
251        $code .= "\n}\n";
252
253        return $code;
254    }
255
256    /**
257     * @param string $name
258     * @param array $schema
259     *
260     * @return string
261     */
262    private function getConstantDeclaration( string $name, array $schema ): string {
263        $chunks = [];
264
265        $chunks[] = "Name constant for the $name setting, for use with Config::get()";
266        $chunks[] = "@see MainConfigSchema::$name";
267
268        if ( isset( $schema['since'] ) ) {
269            $chunks[] = "@since {$schema['since']}";
270        }
271
272        if ( isset( $schema['deprecated'] ) ) {
273            $deprecated = str_replace( "\n", "\n\t *    ", wordwrap( $schema['deprecated'] ) );
274            $chunks[] = "@deprecated {$deprecated}";
275        }
276
277        $code = "\t/**\n\t * ";
278        $code .= implode( "\n\t * ", $chunks );
279        $code .= "\n\t */\n";
280
281        $code .= "\tpublic const $name = '$name';\n";
282        return $code;
283    }
284
285    public function generateSchemaYaml( array $allSchemas ) {
286        foreach ( $allSchemas as &$sch ) {
287            // Cast empty arrays to objects if they are declared to be of type object.
288            // This ensures they get represented in yaml as {} rather than [].
289            if ( isset( $sch['default'] ) && isset( $sch['type'] ) ) {
290                $types = (array)$sch['type'];
291                if ( $sch['default'] === [] && in_array( 'object', $types ) ) {
292                    $sch['default'] = new stdClass();
293                }
294            }
295
296            // Wrap long deprecation messages
297            if ( isset( $sch['deprecated'] ) ) {
298                $sch['deprecated'] = wordwrap( $sch['deprecated'] );
299            }
300        }
301
302        // Dynamic defaults are not relevant to yaml consumers
303        unset( $sch['dynamicDefault'] );
304
305        $yamlFlags = Yaml::DUMP_OBJECT_AS_MAP
306            | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK
307            | Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE;
308
309        $array = [ 'config-schema' => $allSchemas ];
310        $yaml = Yaml::dump( $array, 4, 4, $yamlFlags );
311
312        $header = "# This file is automatically generated using maintenance/generateConfigSchema.php.\n";
313        $header .= "# Do not modify this file manually, edit includes/MainConfigSchema.php instead.\n";
314
315        return $header . $yaml;
316    }
317
318    public function generateVariableStubs( array $allSchemas ) {
319        $content = "<?php\n";
320        $content .= "/**\n" .
321            " * This file is automatically generated using maintenance/generateConfigSchema.php.\n" .
322            " * Do not modify this file manually, edit includes/MainConfigSchema.php instead.\n" .
323            " */\n";
324
325        $content .= "// phpcs:disable\n";
326        $content .= "throw new LogicException( 'Do not load config-vars.php, " .
327            "it exists as a documentation stub only' );\n";
328
329        foreach ( $allSchemas as $name => $schema ) {
330            $content .= "\n";
331            $content .= $this->getVariableDeclaration( $name, $schema );
332        }
333
334        return $content;
335    }
336
337    /**
338     * @param string $name
339     * @param array $schema
340     *
341     * @return string
342     */
343    private function getVariableDeclaration( string $name, array $schema ): string {
344        $chunks = [];
345        $chunks[] = "Config variable stub for the $name setting, for use by phpdoc and IDEs.";
346        $chunks[] = "@see MediaWiki\\MainConfigSchema::$name";
347
348        if ( isset( $schema['since'] ) ) {
349            $chunks[] = "@since {$schema['since']}";
350        }
351
352        if ( isset( $schema['deprecated'] ) ) {
353            $deprecated = str_replace( "\n", "\n *    ", wordwrap( $schema['deprecated'] ) );
354            $chunks[] = "@deprecated {$deprecated}";
355        }
356
357        $code = "/**\n * ";
358        $code .= implode( "\n * ", $chunks );
359        $code .= "\n */\n";
360
361        $code .= "\$wg{$name} = null;\n";
362        return $code;
363    }
364}
365
366// @codeCoverageIgnoreStart
367$maintClass = GenerateConfigSchema::class;
368require_once RUN_MAINTENANCE_IF_MAIN;
369// @codeCoverageIgnoreEnd