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